Test Failed
Push — master ( f07b2d...569899 )
by stéphane
03:01
created

Node   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 3
dl 0
loc 343
rs 4.08
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A setParent() 0 5 1
B getParent() 0 14 7
A add() 0 24 5
A getDeepestNode() 0 17 5
A parse() 0 16 4
B identify() 0 24 7
A onQuoted() 0 5 2
A onSetElement() 0 9 3
A onKey() 0 21 5
B onCompact() 0 36 6
B onHyphen() 0 22 6
A onNodeAction() 0 13 4
A __debugInfo() 0 7 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Dallgoot\Yaml;
4
5
use Dallgoot\Yaml\{Yaml as Y, Regex as R};
6
7
/**
8
 *
9
 * @author  Stéphane Rebai <[email protected]>
10
 * @license Apache 2.0
11
 * @link    TODO : url to specific online doc
12
 */
13
final class Node
14
{
15
    /** @var int */
16
    public $indent = -1;
17
    /** @var int */
18
    public $line;
19
    /** @var int */
20
    public $type;
21
    /** @var null|string|boolean */
22
    public $identifier;
23
    /** @var Node|NodeList|null|string */
24
    public $value = null;
25
    /** @var null|string */
26
    public $raw;
27
28
    /** @var null|Node */
29
    private $parent;
30
31
    /**
32
     * Create the Node object and parses $nodeString IF not null (else assume a root type Node)
33
     *
34
     * @param string|null $nodeString The node string
35
     * @param int|null    $line       The line
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $line a bit more specific; maybe use integer.
Loading history...
36
     */
37
    public function __construct($nodeString = null, $line = 0)
38
    {
39
        $this->line = (int) $line;
40
        if (is_null($nodeString)) {
41
            $this->type = Y::ROOT;
42
        } else {
43
            $this->raw = $nodeString;
44
            $this->parse($nodeString);
45
        }
46
    }
47
48
    /**
49
     * Sets the parent of the current Node
50
     *
51
     * @param Node $node The node
52
     *
53
     * @return Node|self The currentNode
54
     */
55
    public function setParent(Node $node):Node
56
    {
57
        $this->parent = $node;
58
        return $this;
59
    }
60
61
    /**
62
     * Gets the ancestor with specified $indent or the direct $parent OR the current Node itself
63
     *
64
     * @param int|null $indent The indent
65
     * @param int $type  first ancestor of this YAML::type is returned
66
     *
67
     * @return Node|self   The parent.
68
     */
69
    public function getParent(int $indent = null, $type = 0):Node
70
    {
71
        if ($this->type === Y::ROOT) return $this;
72
        if (!is_int($indent)) return $this->parent ?? $this;
73
        $cursor = $this;
74
        while ($cursor instanceof Node && $cursor->indent >= $indent) {
75
            if ($cursor->indent === $indent && $cursor->type !== $type) {
76
                $cursor = $cursor->parent ?? $cursor;
77
                break;
78
            }
79
            $cursor = $cursor->parent;
80
        }
81
        return $cursor;
82
    }
83
84
    /**
85
     * Set the value for the current Node :
86
     * - if value is null , then value = $child (Node)
87
     * - if value is Node, then value is a NodeList with (previous value AND $child)
88
     * - if value is a NodeList, push $child into and set NodeList type accordingly
89
     *
90
     * @param Node $child The child
91
     * @todo  refine the conditions when Y::LITTERALS
92
     */
93
    public function add(Node $child)
94
    {
95
        if ($this->type & (Y::SCALAR|Y::QUOTED)) {
96
            $this->getParent()->add($child);
97
            return;
98
        }
99
        $child->setParent($this);
100
        $current = $this->value;
101
        if (is_null($current)) {
102
            $this->value = $child;
103
            return;
104
        }elseif ($current instanceof Node) {
105
            $this->value = new NodeList();
106
            if ($current->type & Y::LITTERALS) {
107
                $this->value->type = $current->type;
108
            } else {
109
                $this->value->push($current);
110
            }
111
        }
112
        // if (is_null($this->value->type)) {
113
        //     $this->adjustValueType($child);
114
        // }
115
        $this->value->push($child);
0 ignored issues
show
Bug introduced by
The method push does only exist in Dallgoot\Yaml\NodeList, but not in Dallgoot\Yaml\Node.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
116
    }
117
118
    //modify value type according to child
119
    // private function adjustValueType(Node $child)
120
    // {
121
    //     if ($child->type & Y::SET_KEY)   $this->value->type = Y::SET;
122
    //     if ($child->type & Y::KEY)       $this->value->type = Y::MAPPING;
123
    //     if ($child->type & Y::ITEM)      $this->value->type = Y::SEQUENCE;
124
    // }
125
126
    /**
127
     * Gets the deepest node.
128
     *
129
     * @return Node|self  The deepest node.
130
     */
131
    public function getDeepestNode():Node
132
    {
133
        $cursor = $this;
134
        while ($cursor->value instanceof Node || $cursor->value instanceof NodeList) {
135
            if ($cursor->value instanceof NodeList) {
136
                if ($cursor->value->count() === 1) {
137
                    $cursor = $cursor->value->OffsetGet(0);
138
                } else {
139
                    $cursor = $cursor;
0 ignored issues
show
Bug introduced by
Why assign $cursor to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
140
                    break;
141
                }
142
            } else {
143
                $cursor = $cursor->value;
144
            }
145
        }
146
        return $cursor;
147
    }
148
149
    /**
150
     * Parses the string (assumed to be a line from a valid YAML)
151
     *
152
     * @param string $nodeString The node string
153
     *
154
     * @return Node|self
155
     */
156
    public function parse(string $nodeString):Node
157
    {
158
        $nodeValue = preg_replace("/^\t+/m", " ", $nodeString); //permissive to tabs but replacement
159
        $this->indent = strspn($nodeValue, ' ');
160
        $nodeValue = ltrim($nodeValue);
161
        if ($nodeValue === '') {
162
            $this->type = Y::BLANK;
163
        } elseif (substr($nodeValue, 0, 3) === '...') {//TODO: can have something on same line ?
164
            $this->type = Y::DOC_END;
165
        } elseif (preg_match(R::KEY, $nodeValue, $matches)) {
166
            $this->onKey($matches);
167
        } else {
168
            $this->identify($nodeValue);
169
        }
170
        return $this;
171
    }
172
173
    /**
174
     *  Set the type and value according to first character
175
     *
176
     * @param string $nodeValue The node value
177
     */
178
    private function identify($nodeValue)
179
    {
180
        $v = ltrim(substr($nodeValue, 1));
181
        $first = $nodeValue[0];
182
        if ($first === "-")                        $this->onHyphen($nodeValue);
183
        elseif (in_array($first, ['"', "'"]))      $this->onQuoted($nodeValue);
184
        elseif (in_array($first, ['{', '[']))      $this->onCompact($nodeValue);
185
        elseif (in_array($first, ['?', ':']))      $this->onSetElement($nodeValue);
186
        elseif (in_array($first, ['!', '&', '*'])) $this->onNodeAction($nodeValue);
187
        else {
188
            $characters = [ '#' =>  [Y::COMMENT, $v],
189
                            '%' =>  [Y::DIRECTIVE, $v],
190
                            '>' =>  [Y::LITT_FOLDED, null],
191
                            '|' =>  [Y::LITT, null]
192
                            ];
193
            if (isset($characters[$first])) {
194
                $this->type  = $characters[$first][0];
0 ignored issues
show
Documentation Bug introduced by
It seems like $characters[$first][0] can also be of type string. However, the property $type is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
195
                $this->value = $characters[$first][1];
196
            } else {
197
                $this->type  = Y::SCALAR;
198
                $this->value = $nodeValue;
199
            }
200
        }
201
    }
202
203
    private function onQuoted($nodeValue)
204
    {
205
        $this->type = R::isProperlyQuoted($nodeValue) ? Y::QUOTED : Y::PARTIAL;
206
        $this->value = $nodeValue;
207
    }
208
209
    private function onSetElement($nodeValue)
210
    {
211
        $this->type = $nodeValue[0] === '?' ? Y::SET_KEY : Y::SET_VALUE;
212
        $v = trim(substr($nodeValue, 1));
213
        if (!empty($v)) {
214
            $this->value = new NodeList;
215
            $this->add(new Node($v, $this->line));
216
        }
217
    }
218
219
    /**
220
     * Process when a "key: value" syntax is found in the parsed string
221
     * Note : key is match 1, value is match 2 as per regex from R::KEY
222
     *
223
     * @param array $matches The matches provided by 'preg_match' function in Node::parse
224
     */
225
    private function onKey(array $matches)
226
    {
227
        $this->type = Y::KEY;
228
        $this->identifier = trim($matches[1], '"\' ');
229
        $value = $matches[2] ? trim($matches[2]) : null;
230
        if (!empty($value)) {
231
            $hasComment = strpos($value, ' #');
232
            if (is_bool($hasComment)) {
233
                $n = new Node($value, $this->line);
234
            } else {
235
                $n = new Node(trim(substr($value, 0, $hasComment)), $this->line);
236
                if ($n->type !== Y::PARTIAL) {
237
                    $comment = new Node(trim(substr($value, $hasComment + 1)), $this->line);
238
                    $comment->identifier = true; //to specify it is NOT a fullline comment
239
                    $this->add($comment);
240
                }
241
            }
242
            $n->indent = $this->indent + strlen($this->identifier);
243
            $this->add($n);
244
        }
245
    }
246
247
    /**
248
     * Determines the correct type and value when a compact object/array syntax is found
249
     *
250
     * @param string $value The value assumed to start with { or [ or characters
251
     *
252
     * @see Node::identify
253
     */
254
    private function onCompact($value)
255
    {
256
        $this->value = json_decode($value, false, 512, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
257
        if (json_last_error() === JSON_ERROR_NONE){
258
            $this->type = Y::JSON;
259
            return;
260
        }
261
        $this->value = new NodeList();
262
        if (preg_match(R::MAPPING, $value)){
263
            $this->type = Y::COMPACT_MAPPING;
264
            $this->value->type = Y::COMPACT_MAPPING;
265
            preg_match_all(R::MAPPING_VALUES, trim(substr($value, 1,-1)), $matches);
266
            foreach ($matches['k'] as $index => $property) {
267
                $n = new Node('', $this->line);
268
                $n->type = Y::KEY;
269
                $n->identifier = trim($property, '"\' ');//TODO : maybe check for proper quoting first ?
270
                $n->value = new Node($matches['v'][$index], $this->line);
271
                $this->value->push($n);
272
            }
273
            return;
274
        }
275
        if (preg_match(R::SEQUENCE, $value)){
276
            $this->type = Y::COMPACT_SEQUENCE;
277
            $this->value->type = Y::COMPACT_SEQUENCE;
278
            $count = preg_match_all(R::SEQUENCE_VALUES, trim(substr($value, 1,-1)), $matches);
0 ignored issues
show
Unused Code introduced by
$count is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
279
            foreach ($matches['item'] as $key => $item) {
280
                $i = new Node('', $this->line);
281
                $i->type = Y::ITEM;
282
                $i->add(new Node($item, $this->line));
283
                $this->value->push($i);
284
            }
285
            return;
286
        }
287
        $this->value = $value;
288
        $this->type  = Y::PARTIAL;
289
    }
290
291
    /**
292
     * Determines type and value when an hyphen "-" is found
293
     *
294
     * @param string $nodeValue The node value
295
     *
296
     * @see Node::identify
297
     */
298
    private function onHyphen($nodeValue)
299
    {
300
        if (substr($nodeValue, 0, 3) === '---') {
301
            $this->type = Y::DOC_START;
302
            $rest = trim(substr($nodeValue, 3));
303
            if (!empty($rest)) {
304
                $n = new Node($rest, $this->line);
305
                $n->indent = $this->indent + 4;
306
                $this->value = $n->setParent($this);
307
            }
308
        } elseif (preg_match(R::ITEM, $nodeValue, $matches)) {
309
            $this->type = Y::ITEM;
310
            if (isset($matches[1]) && !empty(trim($matches[1]))) {
311
                $n = new Node(trim($matches[1]), $this->line);
312
                $n->indent = $this->indent + 2;
313
                $this->value = $n->setParent($this);
314
            }
315
        } else {
316
            $this->type  = Y::SCALAR;
317
            $this->value = $nodeValue;
318
        }
319
    }
320
321
    /**
322
     * Determines the type and value according to $nodeValue when one of these characters is found : !,&,*
323
     *
324
     * @param string $nodeValue The node value
325
     *
326
     * @see  Node::identify
327
     * @todo handle tags like  <tag:clarkevans.com,2002:invoice>
328
     */
329
    private function onNodeAction($nodeValue)
330
    {
331
        $v = substr($nodeValue, 1);
332
        $this->type = ['!' => Y::TAG, '&' => Y::REF_DEF, '*' => Y::REF_CALL][$nodeValue[0]];
333
        $this->identifier = $v;
334
        $pos = strpos($v, ' ');
335
        if ($this->type & (Y::TAG|Y::REF_DEF) && is_int($pos)) {
336
            $this->identifier = strstr($v, ' ', true);
337
            $value = trim(substr($nodeValue, $pos + 1));
338
            $value = R::isProperlyQuoted($value) ? trim($value, "\"'") : $value;
339
            $this->add((new Node($value, $this->line))->setParent($this));
340
        }
341
    }
342
343
    /**
344
     * PHP internal function for debugging purpose : simplify output provided by 'var_dump'
345
     *
346
     * @return array  the Node properties and respective values displayed by 'var_dump'
347
     */
348
    public function __debugInfo():array
349
    {
350
        return ['line'  => $this->line,
351
                'indent'=> $this->indent,
352
                'type'  => Y::getName($this->type).($this->identifier ? "($this->identifier)" : ''),
353
                'value' => $this->value];
354
    }
355
}
356