Passed
Pull Request — master (#1486)
by Michael
08:32
created

DomPlugin::parseBegin()   B

Complexity

Conditions 11
Paths 4

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 7
nc 4
nop 2
dl 0
loc 17
rs 7.3166
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2013 Jonathan Vollebregt ([email protected]), Rokas Šleinius ([email protected])
9
 *
10
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
11
 * this software and associated documentation files (the "Software"), to deal in
12
 * the Software without restriction, including without limitation the rights to
13
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
 * the Software, and to permit persons to whom the Software is furnished to do so,
15
 * subject to the following conditions:
16
 *
17
 * The above copyright notice and this permission notice shall be included in all
18
 * copies or substantial portions of the Software.
19
 *
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
 */
27
28
namespace Kint\Parser;
29
30
use Dom\Attr;
0 ignored issues
show
Bug introduced by
The type Dom\Attr was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
use Dom\CharacterData;
0 ignored issues
show
Bug introduced by
The type Dom\CharacterData was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
32
use Dom\Document;
0 ignored issues
show
Bug introduced by
The type Dom\Document was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use Dom\DocumentType;
0 ignored issues
show
Bug introduced by
The type Dom\DocumentType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
use Dom\Element;
0 ignored issues
show
Bug introduced by
The type Dom\Element was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
use Dom\HTMLElement;
0 ignored issues
show
Bug introduced by
The type Dom\HTMLElement was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
use Dom\NamedNodeMap;
0 ignored issues
show
Bug introduced by
The type Dom\NamedNodeMap was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
use Dom\Node;
0 ignored issues
show
Bug introduced by
The type Dom\Node was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
use Dom\NodeList;
0 ignored issues
show
Bug introduced by
The type Dom\NodeList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
39
use DOMAttr;
40
use DOMCharacterData;
41
use DOMDocumentType;
42
use DOMElement;
43
use DOMNamedNodeMap;
44
use DOMNode;
45
use DOMNodeList;
46
use Kint\Value\AbstractValue;
47
use Kint\Value\Context\BaseContext;
48
use Kint\Value\Context\ClassDeclaredContext;
49
use Kint\Value\Context\ContextInterface;
50
use Kint\Value\Context\PropertyContext;
51
use Kint\Value\DomNodeListValue;
52
use Kint\Value\DomNodeValue;
53
use Kint\Value\FixedWidthValue;
54
use Kint\Value\InstanceValue;
55
use Kint\Value\Representation\ContainerRepresentation;
56
use Kint\Value\StringValue;
57
use LogicException;
58
59
class DomPlugin extends AbstractPlugin implements PluginBeginInterface
60
{
61
    /**
62
     * Reflection doesn't work below 8.1, also it won't show readonly status.
63
     *
64
     * In order to ensure this is stable enough we're only going to provide
65
     * properties for element and node. If subclasses like attr or document
66
     * have their own fields then tough shit we're not showing them.
67
     *
68
     * @psalm-var non-empty-array<string, bool> Property names to readable status
69
     */
70
    public const NODE_PROPS = [
71
        'nodeType' => true,
72
        'nodeName' => true,
73
        'baseURI' => true,
74
        'isConnected' => true,
75
        'ownerDocument' => true,
76
        'parentNode' => true,
77
        'parentElement' => true,
78
        'childNodes' => true,
79
        'firstChild' => true,
80
        'lastChild' => true,
81
        'previousSibling' => true,
82
        'nextSibling' => true,
83
        'nodeValue' => true,
84
        'textContent' => false,
85
    ];
86
87
    /**
88
     * @psalm-var non-empty-array<string, bool> Property names to readable status
89
     */
90
    public const ELEMENT_PROPS = [
91
        'namespaceURI' => true,
92
        'prefix' => true,
93
        'localName' => true,
94
        'tagName' => true,
95
        'id' => false,
96
        'className' => false,
97
        'classList' => true,
98
        'attributes' => true,
99
        'firstElementChild' => true,
100
        'lastElementChild' => true,
101
        'childElementCount' => true,
102
        'previousElementSibling' => true,
103
        'nextElementSibling' => true,
104
        'innerHTML' => false,
105
        'substitutedNodeValue' => false,
106
    ];
107
108
    /**
109
     * @psalm-var non-empty-array<string, bool> Property names to readable status
110
     */
111
    public const DOMNODE_PROPS = [
112
        'nodeName' => true,
113
        'nodeValue' => false,
114
        'nodeType' => true,
115
        'parentNode' => true,
116
        'parentElement' => true,
117
        'childNodes' => true,
118
        'firstChild' => true,
119
        'lastChild' => true,
120
        'previousSibling' => true,
121
        'nextSibling' => true,
122
        'attributes' => true,
123
        'isConnected' => true,
124
        'ownerDocument' => true,
125
        'namespaceURI' => true,
126
        'prefix' => false,
127
        'localName' => true,
128
        'baseURI' => true,
129
        'textContent' => false,
130
    ];
131
132
    /**
133
     * @psalm-var non-empty-array<string, bool> Property names to readable status
134
     */
135
    public const DOMELEMENT_PROPS = [
136
        'tagName' => true,
137
        'className' => false,
138
        'id' => false,
139
        'schemaTypeInfo' => true,
140
        'firstElementChild' => true,
141
        'lastElementChild' => true,
142
        'childElementCount' => true,
143
        'previousElementSibling' => true,
144
        'nextElementSibling' => true,
145
    ];
146
147
    public const DOM_VERSIONS = [
148
        'parentElement' => KINT_PHP83,
149
        'isConnected' => KINT_PHP83,
150
        'className' => KINT_PHP83,
151
        'id' => KINT_PHP83,
152
        'firstElementChild' => KINT_PHP80,
153
        'lastElementChild' => KINT_PHP80,
154
        'childElementCount' => KINT_PHP80,
155
        'previousElementSibling' => KINT_PHP80,
156
        'nextElementSibling' => KINT_PHP80,
157
    ];
158
159
    /**
160
     * List of properties to skip parsing.
161
     *
162
     * The properties of a Dom\Node can do a *lot* of damage to debuggers. The
163
     * Dom\Node contains not one, not two, but 13 different ways to recurse into itself:
164
     * * parentNode
165
     * * firstChild
166
     * * lastChild
167
     * * previousSibling
168
     * * nextSibling
169
     * * parentElement
170
     * * firstElementChild
171
     * * lastElementChild
172
     * * previousElementSibling
173
     * * nextElementSibling
174
     * * childNodes
175
     * * attributes
176
     * * ownerDocument
177
     *
178
     * All of this combined: the tiny SVGs used as the caret in Kint were already
179
     * enough to make parsing and rendering take over a second, and send memory
180
     * usage over 128 megs, back in the old DOM API. So we blacklist every field
181
     * we don't strictly need and hope that that's good enough.
182
     *
183
     * In retrospect -- this is probably why print_r does the same
184
     *
185
     * @psalm-var array<string, true>
186
     */
187
    public static array $blacklist = [
188
        'parentNode' => true,
189
        'firstChild' => true,
190
        'lastChild' => true,
191
        'previousSibling' => true,
192
        'nextSibling' => true,
193
        'firstElementChild' => true,
194
        'lastElementChild' => true,
195
        'parentElement' => true,
196
        'previousElementSibling' => true,
197
        'nextElementSibling' => true,
198
        'ownerDocument' => true,
199
    ];
200
201
    /**
202
     * Show all properties and methods.
203
     */
204
    public static bool $verbose = false;
205
206
    protected ClassMethodsPlugin $methods_plugin;
207
    protected ClassStaticsPlugin $statics_plugin;
208
209
    public function __construct(Parser $parser)
210
    {
211
        parent::__construct($parser);
212
213
        $this->methods_plugin = new ClassMethodsPlugin($parser);
214
        $this->statics_plugin = new ClassStaticsPlugin($parser);
215
    }
216
217
    public function setParser(Parser $p): void
218
    {
219
        parent::setParser($p);
220
221
        $this->methods_plugin->setParser($p);
222
        $this->statics_plugin->setParser($p);
223
    }
224
225
    public function getTypes(): array
226
    {
227
        return ['object'];
228
    }
229
230
    public function getTriggers(): int
231
    {
232
        return Parser::TRIGGER_BEGIN;
233
    }
234
235
    public function parseBegin(&$var, ContextInterface $c): ?AbstractValue
236
    {
237
        // Attributes and chardata (Which is parent of comments and text
238
        // nodes) don't need children or attributes of their own
239
        if ($var instanceof Attr || $var instanceof CharacterData || $var instanceof DOMAttr || $var instanceof DOMCharacterData) {
240
            return $this->parseText($var, $c);
241
        }
242
243
        if ($var instanceof NamedNodeMap || $var instanceof NodeList || $var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) {
244
            return $this->parseList($var, $c);
245
        }
246
247
        if ($var instanceof Node || $var instanceof DOMNode) {
248
            return $this->parseNode($var, $c);
249
        }
250
251
        return null;
252
    }
253
254
    /** @psalm-param Node|DOMNode $var */
255
    private function parseProperty(object $var, string $prop, ContextInterface $c): AbstractValue
256
    {
257
        if (!isset($var->{$prop})) {
258
            return new FixedWidthValue($c, null);
259
        }
260
261
        $parser = $this->getParser();
262
        $value = $var->{$prop};
263
264
        if (\is_scalar($value)) {
265
            return $parser->parse($value, $c);
266
        }
267
268
        if (isset(self::$blacklist[$prop])) {
269
            $b = new InstanceValue($c, \get_class($value), \spl_object_hash($value), \spl_object_id($value));
270
            $b->flags |= AbstractValue::FLAG_GENERATED | AbstractValue::FLAG_BLACKLIST;
271
272
            return $b;
273
        }
274
275
        // Everything we can handle in parseBegin
276
        if ($value instanceof Attr || $value instanceof CharacterData || $value instanceof DOMAttr || $value instanceof DOMCharacterData || $value instanceof NamedNodeMap || $value instanceof NodeList || $value instanceof DOMNamedNodeMap || $value instanceof DOMNodeList || $value instanceof Node || $value instanceof DOMNode) {
277
            $out = $this->parseBegin($value, $c);
278
        }
279
280
        if (!isset($out)) {
281
            // Shouldn't ever happen
282
            $out = $parser->parse($value, $c); // @codeCoverageIgnore
283
        }
284
285
        $out->flags |= AbstractValue::FLAG_GENERATED;
286
287
        return $out;
288
    }
289
290
    /** @psalm-param Attr|CharacterData|DOMAttr|DOMCharacterData $var */
291
    private function parseText(object $var, ContextInterface $c): AbstractValue
292
    {
293
        if ($c instanceof BaseContext && null !== $c->access_path) {
294
            $c->access_path .= '->nodeValue';
295
        }
296
297
        return $this->parseProperty($var, 'nodeValue', $c);
298
    }
299
300
    /** @psalm-param NamedNodeMap|NodeList|DOMNamedNodeMap|DOMNodeList $var */
301
    private function parseList(object $var, ContextInterface $c): InstanceValue
302
    {
303
        if ($var instanceof NodeList || $var instanceof DOMNodeList) {
304
            $v = new DomNodeListValue($c, $var);
305
        } else {
306
            $v = new InstanceValue($c, \get_class($var), \spl_object_hash($var), \spl_object_id($var));
307
        }
308
309
        $parser = $this->getParser();
310
        $pdepth = $parser->getDepthLimit();
311
312
        // Depth limit
313
        // Use empty iterator representation since we need it to point out depth limits
314
        if (($var instanceof NodeList || $var instanceof DOMNodeList) && $pdepth && $c->getDepth() >= $pdepth) {
315
            $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT;
316
317
            return $v;
318
        }
319
320
        if (self::$verbose) {
321
            $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS);
322
            $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS);
323
        }
324
325
        if (0 === $var->length) {
326
            $v->setChildren([]);
327
328
            return $v;
329
        }
330
331
        $cdepth = $c->getDepth();
332
        $ap = $c->getAccessPath();
333
        $contents = [];
334
335
        foreach ($var as $key => $item) {
336
            $base_obj = new BaseContext($item->nodeName);
337
            $base_obj->depth = $cdepth + 1;
338
339
            if ($var instanceof NamedNodeMap || $var instanceof DOMNamedNodeMap) {
340
                if (null !== $ap) {
341
                    $base_obj->access_path = $ap.'['.\var_export($item->nodeName, true).']';
342
                }
343
            } else { // NodeList
344
                if (null !== $ap) {
345
                    $base_obj->access_path = $ap.'['.\var_export($key, true).']';
346
                }
347
            }
348
349
            if ($item instanceof HTMLElement) {
350
                $base_obj->name = $item->localName;
351
            }
352
353
            $item = $parser->parse($item, $base_obj);
354
            $item->flags |= AbstractValue::FLAG_GENERATED;
355
356
            $contents[] = $item;
357
        }
358
359
        $v->setChildren($contents);
360
361
        if ($contents) {
0 ignored issues
show
introduced by
$contents is an empty array, thus is always false.
Loading history...
Bug Best Practice introduced by
The expression $contents of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
362
            $v->addRepresentation(new ContainerRepresentation('Iterator', $contents), 0);
363
        }
364
365
        return $v;
366
    }
367
368
    /** @psalm-param Node|DOMNode $var */
369
    private function parseNode(object $var, ContextInterface $c): DomNodeValue
370
    {
371
        $class = \get_class($var);
372
        $pdepth = $this->getParser()->getDepthLimit();
373
374
        if ($pdepth && $c->getDepth() >= $pdepth) {
375
            $v = new DomNodeValue($c, $var);
376
            $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT;
377
378
            return $v;
379
        }
380
381
        if (($var instanceof DocumentType || $var instanceof DOMDocumentType) && $c instanceof BaseContext && $c->name === $var->nodeName) {
382
            $c->name = '!DOCTYPE '.$c->name;
383
        }
384
385
        $cdepth = $c->getDepth();
386
        $ap = $c->getAccessPath();
387
388
        $properties = [];
389
        $children = [];
390
        $attributes = [];
391
392
        foreach (self::getKnownProperties($var) as $prop => $readonly) {
393
            $prop_c = new PropertyContext($prop, $class, ClassDeclaredContext::ACCESS_PUBLIC);
394
            $prop_c->depth = $cdepth + 1;
395
            $prop_c->readonly = KINT_PHP81 && $readonly;
396
397
            if (null !== $ap) {
398
                $prop_c->access_path = $ap.'->'.$prop;
399
            }
400
401
            $properties[] = $prop_obj = $this->parseProperty($var, $prop, $prop_c);
402
403
            if ('childNodes' === $prop) {
404
                if (!$prop_obj instanceof DomNodeListValue) {
405
                    throw new LogicException('childNodes property parsed incorrectly'); // @codeCoverageIgnore
406
                }
407
                $children = self::getChildren($prop_obj);
408
            } elseif ('attributes' === $prop) {
409
                $attributes = $prop_obj->getRepresentation('iterator');
410
                $attributes = $attributes instanceof ContainerRepresentation ? $attributes->getContents() : [];
411
            } elseif ('classList' === $prop) {
412
                if ($iter = $prop_obj->getRepresentation('iterator')) {
413
                    $prop_obj->removeRepresentation($iter);
414
                    $prop_obj->addRepresentation($iter, 0);
415
                }
416
            }
417
        }
418
419
        $v = new DomNodeValue($c, $var);
420
        // If we're in text mode, we can see children through the childNodes property
421
        $v->setChildren($properties);
422
423
        if ($children) {
424
            $v->addRepresentation(new ContainerRepresentation('Children', $children, null, true));
425
        }
426
427
        if ($attributes) {
428
            $v->addRepresentation(new ContainerRepresentation('Attributes', $attributes));
429
        }
430
431
        if (self::$verbose) {
432
            $v->addRepresentation(new ContainerRepresentation('Properties', $properties));
433
434
            $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS);
435
            $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS);
436
        }
437
438
        return $v;
439
    }
440
441
    /**
442
     * @psalm-param Node|DOMNode $var
443
     *
444
     * @psalm-return non-empty-array<string, bool>
445
     */
446
    public static function getKnownProperties(object $var): array
447
    {
448
        if ($var instanceof Node) {
449
            $known_properties = self::NODE_PROPS;
450
            if ($var instanceof Element) {
451
                $known_properties += self::ELEMENT_PROPS;
452
            }
453
454
            if ($var instanceof Document) {
455
                $known_properties['textContent'] = true;
456
            }
457
458
            if ($var instanceof Attr || $var instanceof CharacterData) {
459
                $known_properties['nodeValue'] = false;
460
            }
461
        } else {
462
            $known_properties = self::DOMNODE_PROPS;
463
            if ($var instanceof DOMElement) {
464
                $known_properties += self::DOMELEMENT_PROPS;
465
            }
466
467
            foreach (self::DOM_VERSIONS as $key => $val) {
468
                /**
469
                 * @psalm-var bool $val
470
                 * Psalm bug #4509
471
                 */
472
                if (false === $val) {
473
                    unset($known_properties[$key]); // @codeCoverageIgnore
474
                }
475
            }
476
        }
477
478
        /** @psalm-var non-empty-array $known_properties */
479
        if (!self::$verbose) {
480
            $known_properties = \array_intersect_key($known_properties, [
481
                'nodeValue' => null,
482
                'childNodes' => null,
483
                'attributes' => null,
484
            ]);
485
        }
486
487
        return $known_properties;
488
    }
489
490
    /** @psalm-return list<AbstractValue> */
491
    private static function getChildren(DomNodeListValue $property): array
492
    {
493
        if (0 === $property->getLength()) {
494
            return [];
495
        }
496
497
        if ($property->flags & AbstractValue::FLAG_DEPTH_LIMIT) {
498
            return [$property];
499
        }
500
501
        $list_items = $property->getChildren();
502
503
        if (null === $list_items) {
504
            // This is here for psalm but all DomNodeListValue should
505
            // either be depth_limit or have array children
506
            return []; // @codeCoverageIgnore
507
        }
508
509
        $children = [];
510
511
        foreach ($list_items as $node) {
512
            // Remove text nodes if theyre empty
513
            if ($node instanceof StringValue && '#text' === $node->getContext()->getName()) {
514
                /**
515
                 * @psalm-suppress InvalidArgument
516
                 * Psalm bug #11055
517
                 */
518
                if (\ctype_space($node->getValue()) || '' === $node->getValue()) {
519
                    continue;
520
                }
521
            }
522
523
            $children[] = $node;
524
        }
525
526
        return $children;
527
    }
528
}
529