Test Failed
Branch master (44c6e4)
by stéphane
09:11
created

Node::onHyphen()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9137
c 0
b 0
f 0
cc 6
nc 5
nop 1
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 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) {
72
            return $this;
73
        }
74
        if (!is_int($indent)) return $this->parent ?? $this;
75
        $cursor = $this;
76
        while ($cursor instanceof Node && $cursor->indent >= $indent) {
77
            if ($cursor->indent === $indent && $cursor->type !== $type) {
78
                $cursor = $cursor->parent ?? $cursor;
79
                break;
80
            }
81
            $cursor = $cursor->parent;
82
        }
83
        return $cursor;
84
    }
85
86
    /**
87
     * Set the value for the current Node :
88
     * - if value is null , then value = $child (Node)
89
     * - if value is Node, then value is a NodeList with (previous value AND $child)
90
     * - if value is a NodeList, push $child into and set NodeList type accordingly
91
     *
92
     * @param Node $child The child
93
     */
94
    public function add(Node $child)
95
    {
96
        if ($this->type & (Y::SCALAR|Y::QUOTED)) {
97
            $this->getParent()->add($child);
98
            return;
99
        }
100
        $child->setParent($this);
101
        $current = $this->value;
102
        if (is_null($current)) {
103
            $this->value = $child;
104
            return;
105
        }elseif ($current instanceof Node) {
106
            $this->value = new NodeList();
107
            if ($current->type & Y::LITTERALS) {
108
                $this->value->type = $current->type;
109
            } else {
110
                $this->value->push($current);
111
            }
112
        }
113
        //modify type according to child
114
        if (is_null($this->value->type)) {
115
            if ($child->type & Y::SET_KEY)   $this->value->type = Y::SET;
116
            if ($child->type & Y::KEY)       $this->value->type = Y::MAPPING;
117
            if ($child->type & Y::ITEM)      $this->value->type = Y::SEQUENCE;
118
        }
119
        $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...
120
121
        if ($this->type & Y::LITTERALS)  $this->value->type = $this->type;
122
    }
123
124
    /**
125
     * Gets the deepest node.
126
     *
127
     * @return Node|self  The deepest node.
128
     */
129
    public function getDeepestNode():Node
130
    {
131
        $cursor = $this;
132
        while ($cursor->value instanceof Node || $cursor->value instanceof NodeList) {
133
            if ($cursor->value instanceof NodeList) {
134
                if ($cursor->value->count() === 1) {
135
                    $cursor = $cursor->value->OffsetGet(0);
136
                } else {
137
                    $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...
138
                    break;
139
                }
140
            } else {
141
                $cursor = $cursor->value;
142
            }
143
        }
144
        return $cursor;
145
    }
146
147
    /**
148
     * Parses the string (assumed to be a line from a valid YAML)
149
     *
150
     * @param string $nodeString The node string
151
     *
152
     * @return Node|self
153
     */
154
    public function parse(string $nodeString):Node
155
    {
156
        $nodeValue = preg_replace("/^\t+/m", " ", $nodeString); //permissive to tabs but replacement
157
        $this->indent = strspn($nodeValue, ' ');
158
        $nodeValue = ltrim($nodeValue);
159
        if ($nodeValue === '') {
160
            $this->type = Y::BLANK;
161
        } elseif (substr($nodeValue, 0, 3) === '...') {//TODO: can have something on same line ?
162
            $this->type = Y::DOC_END;
163
        } elseif (preg_match(R::KEY, $nodeValue, $matches)) {
164
            $this->onKey($matches);
165
        } else {
166
            $this->identify($nodeValue);
167
        }
168
        return $this;
169
    }
170
171
    /**
172
     *  Set the type and value according to first character
173
     *
174
     * @param string $nodeValue The node value
175
     */
176
    private function identify($nodeValue)
177
    {
178
        $v = substr($nodeValue, 1);
179
        $first = $nodeValue[0];
180
        if (in_array($first, ['"', "'"])) {
181
            $this->type = R::isProperlyQuoted($nodeValue) ? Y::QUOTED : Y::PARTIAL;
182
            $this->value = $nodeValue;
183
            return;
184
        }
185
        if (in_array($first, ['{', '['])) {
186
             $this->onCompact(trim($nodeValue));
187
             return;
188
         }
189
        if (in_array($first, ['!', '&', '*'])) {
190
            $this->onNodeAction($nodeValue);
191
            return;
192
        }
193
        // Note : php don't like '?' as an array key -_-'
194
        if(in_array($first, ['?', ':'])) {
195
            $this->type = $first === '?' ? Y::SET_KEY : Y::SET_VALUE;
196
            if (!empty(trim($v))) {
197
                $this->value = new NodeList;
198
                $this->add(new Node(ltrim($v), $this->line));
199
            }
200
            return;
201
        }
202
        if ($first === "-") {
203
            $this->onHyphen($nodeValue);
204
            return;
205
        }
206
        $characters = [ '#' =>  [Y::COMMENT, ltrim($v)],
207
                        '%' =>  [Y::DIRECTIVE, ltrim($v)],
208
                        '>' =>  [Y::LITT_FOLDED, null],
209
                        '|' =>  [Y::LITT, null]
210
                        ];
211
        if (isset($characters[$first])) {
212
            $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...
213
            $this->value = $characters[$first][1];
214
        } else {
215
            $this->type  = Y::SCALAR;
216
            $this->value = $nodeValue;
217
        }
218
    }
219
220
    /**
221
     * Process when a "key: value" syntax is found in the parsed string
222
     * Note : key is match 1, value is match 2 as per regex from R::KEY
223
     *
224
     * @param array $matches The matches provided by 'preg_match' function in Node::parse
225
     */
226
    private function onKey(array $matches)
227
    {
228
        $this->type = Y::KEY;
229
        $this->identifier = trim($matches[1], '"\' ');
230
        $value = $matches[2] ? trim($matches[2]) : null;
231
        if (!empty($value)) {
232
            $hasComment = strpos($value, ' #');
233
            if (is_bool($hasComment)) {
234
                $n = new Node($value, $this->line);
235
            } else {
236
                $n = new Node(trim(substr($value, 0, $hasComment)), $this->line);
237
                if ($n->type !== Y::PARTIAL) {
238
                    $comment = new Node(trim(substr($value, $hasComment + 1)), $this->line);
239
                    $comment->identifier = true; //to specify it is NOT a fullline comment
240
                    $this->add($comment);
241
                    // var_dump($n, $comment);
242
                }
243
            }
244
            $n->indent = $this->indent + strlen($this->identifier);
245
            $this->add($n);
246
        }
247
    }
248
249
    /**
250
     * Determines the correct type and value when a compact object/array syntax is found
251
     *
252
     * @param string $value The value assumed to start with { or [ or characters
253
     *
254
     * @see Node::identify
255
     */
256
    private function onCompact($value)
257
    {
258
        $this->value = json_decode($value, false, 512, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
259
        if (json_last_error() === JSON_ERROR_NONE){
260
            $this->type = Y::JSON;
261
            return;
262
        }
263
        $this->value = new NodeList();
264
        if (preg_match(R::MAPPING, $value)){
265
            $this->type = Y::COMPACT_MAPPING;
266
            $this->value->type = Y::COMPACT_MAPPING;
267
            preg_match_all(R::MAPPING_VALUES, trim(substr($value, 1,-1)), $matches);
268
            foreach ($matches['k'] as $index => $property) {
269
                $n = new Node('', $this->line);
270
                $n->type = Y::KEY;
271
                $n->identifier = trim($property, '"\' ');//TODO : maybe check for proper quoting first ?
272
                $n->value = new Node($matches['v'][$index], $this->line);
273
                $this->value->push($n);
274
            }
275
            return;
276
        }
277
        if (preg_match(R::SEQUENCE, $value)){
278
            $this->type = Y::COMPACT_SEQUENCE;
279
            $this->value->type = Y::COMPACT_SEQUENCE;
280
            $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...
281
            foreach ($matches['item'] as $key => $item) {
282
                $i = new Node('', $this->line);
283
                $i->type = Y::ITEM;
284
                $i->add(new Node($item, $this->line));
285
                $this->value->push($i);
286
            }
287
            return;
288
        }
289
        $this->value = $value;
290
        $this->type  = Y::PARTIAL;
291
    }
292
293
    /**
294
     * Determines type and value when an hyphen "-" is found
295
     *
296
     * @param string $nodeValue The node value
297
     *
298
     * @see Node::identify
299
     */
300
    private function onHyphen($nodeValue)
301
    {
302
        if (substr($nodeValue, 0, 3) === '---') {
303
            $this->type = Y::DOC_START;
304
            $rest = trim(substr($nodeValue, 3));
305
            if (empty($rest)) {
306
                return;
307
            }
308
            $n = new Node($rest, $this->line);
309
            $n->indent = $this->indent + 4;
310
            $this->value = $n->setParent($this);
311
            return;
312
        }
313
        if (preg_match(R::ITEM, $nodeValue, $matches)) {
314
            $this->type = Y::ITEM;
315
            if (isset($matches[1]) && !empty(trim($matches[1]))) {
316
                $n = new Node(trim($matches[1]), $this->line);
317
                $n->indent = $this->indent + 2;
318
                $this->value = $n->setParent($this);
319
            }
320
            return;
321
        }
322
        list($this->type, $this->value) = [Y::SCALAR, $nodeValue];
323
    }
324
325
    /**
326
     * Determines the type and value according to $nodeValue when one of these characters is found : !,&,*
327
     *
328
     * @param string $nodeValue The node value
329
     *
330
     * @see  Node::identify
331
     * @todo handle tags like  <tag:clarkevans.com,2002:invoice>
332
     */
333
    private function onNodeAction($nodeValue)
334
    {
335
        $v = substr($nodeValue, 1);
336
        $this->type = ['!' => Y::TAG, '&' => Y::REF_DEF, '*' => Y::REF_CALL][$nodeValue[0]];
337
        $this->identifier = $v;
338
        $pos = strpos($v, ' ');
339
        // $this->value = new NodeList;
340
        if ($this->type & (Y::TAG|Y::REF_DEF) && is_int($pos)) {
341
            $this->identifier = strstr($v, ' ', true);
342
            $value = trim(substr($nodeValue, $pos + 1));
343
            $value = R::isProperlyQuoted($value) ? trim($value, "\"'") : $value;
344
            $this->add((new Node($value, $this->line))->setParent($this));
345
        }
346
    }
347
348
    /**
349
     * PHP internal function for debugging purpose : simplify output provided by 'var_dump'
350
     *
351
     * @return array  the Node properties and respective values displayed by 'var_dump'
352
     */
353
    public function __debugInfo():array
354
    {
355
        return ['line'  => $this->line,
356
                'indent'=> $this->indent,
357
                'type'  => Y::getName($this->type).($this->identifier ? "($this->identifier)" : ''),
358
                'value' => $this->value];
359
    }
360
}
361