Total Complexity | 89 |
Total Lines | 483 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like DomPlugin 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.
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 DomPlugin, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 | 'outerHTML' => false, |
||
106 | 'substitutedNodeValue' => false, |
||
107 | ]; |
||
108 | |||
109 | public const DOM_NS_VERSIONS = [ |
||
110 | 'outerHTML' => KINT_PHP85, |
||
111 | ]; |
||
112 | |||
113 | /** |
||
114 | * @psalm-var non-empty-array<string, bool> Property names to readable status |
||
115 | */ |
||
116 | public const DOMNODE_PROPS = [ |
||
117 | 'nodeName' => true, |
||
118 | 'nodeValue' => false, |
||
119 | 'nodeType' => true, |
||
120 | 'parentNode' => true, |
||
121 | 'parentElement' => true, |
||
122 | 'childNodes' => true, |
||
123 | 'firstChild' => true, |
||
124 | 'lastChild' => true, |
||
125 | 'previousSibling' => true, |
||
126 | 'nextSibling' => true, |
||
127 | 'attributes' => true, |
||
128 | 'isConnected' => true, |
||
129 | 'ownerDocument' => true, |
||
130 | 'namespaceURI' => true, |
||
131 | 'prefix' => false, |
||
132 | 'localName' => true, |
||
133 | 'baseURI' => true, |
||
134 | 'textContent' => false, |
||
135 | ]; |
||
136 | |||
137 | /** |
||
138 | * @psalm-var non-empty-array<string, bool> Property names to readable status |
||
139 | */ |
||
140 | public const DOMELEMENT_PROPS = [ |
||
141 | 'tagName' => true, |
||
142 | 'className' => false, |
||
143 | 'id' => false, |
||
144 | 'schemaTypeInfo' => true, |
||
145 | 'firstElementChild' => true, |
||
146 | 'lastElementChild' => true, |
||
147 | 'childElementCount' => true, |
||
148 | 'previousElementSibling' => true, |
||
149 | 'nextElementSibling' => true, |
||
150 | ]; |
||
151 | |||
152 | public const DOM_VERSIONS = [ |
||
153 | 'parentElement' => KINT_PHP83, |
||
154 | 'isConnected' => KINT_PHP83, |
||
155 | 'className' => KINT_PHP83, |
||
156 | 'id' => KINT_PHP83, |
||
157 | 'firstElementChild' => KINT_PHP80, |
||
158 | 'lastElementChild' => KINT_PHP80, |
||
159 | 'childElementCount' => KINT_PHP80, |
||
160 | 'previousElementSibling' => KINT_PHP80, |
||
161 | 'nextElementSibling' => KINT_PHP80, |
||
162 | ]; |
||
163 | |||
164 | /** |
||
165 | * List of properties to skip parsing. |
||
166 | * |
||
167 | * The properties of a Dom\Node can do a *lot* of damage to debuggers. The |
||
168 | * Dom\Node contains not one, not two, but 13 different ways to recurse into itself: |
||
169 | * * parentNode |
||
170 | * * firstChild |
||
171 | * * lastChild |
||
172 | * * previousSibling |
||
173 | * * nextSibling |
||
174 | * * parentElement |
||
175 | * * firstElementChild |
||
176 | * * lastElementChild |
||
177 | * * previousElementSibling |
||
178 | * * nextElementSibling |
||
179 | * * childNodes |
||
180 | * * attributes |
||
181 | * * ownerDocument |
||
182 | * |
||
183 | * All of this combined: the tiny SVGs used as the caret in Kint were already |
||
184 | * enough to make parsing and rendering take over a second, and send memory |
||
185 | * usage over 128 megs, back in the old DOM API. So we blacklist every field |
||
186 | * we don't strictly need and hope that that's good enough. |
||
187 | * |
||
188 | * In retrospect -- this is probably why print_r does the same |
||
189 | * |
||
190 | * @psalm-var array<string, true> |
||
191 | */ |
||
192 | public static array $blacklist = [ |
||
193 | 'parentNode' => true, |
||
194 | 'firstChild' => true, |
||
195 | 'lastChild' => true, |
||
196 | 'previousSibling' => true, |
||
197 | 'nextSibling' => true, |
||
198 | 'firstElementChild' => true, |
||
199 | 'lastElementChild' => true, |
||
200 | 'parentElement' => true, |
||
201 | 'previousElementSibling' => true, |
||
202 | 'nextElementSibling' => true, |
||
203 | 'ownerDocument' => true, |
||
204 | ]; |
||
205 | |||
206 | /** |
||
207 | * Show all properties and methods. |
||
208 | */ |
||
209 | public static bool $verbose = false; |
||
210 | |||
211 | protected ClassMethodsPlugin $methods_plugin; |
||
212 | protected ClassStaticsPlugin $statics_plugin; |
||
213 | |||
214 | public function __construct(Parser $parser) |
||
215 | { |
||
216 | parent::__construct($parser); |
||
217 | |||
218 | $this->methods_plugin = new ClassMethodsPlugin($parser); |
||
219 | $this->statics_plugin = new ClassStaticsPlugin($parser); |
||
220 | } |
||
221 | |||
222 | public function setParser(Parser $p): void |
||
223 | { |
||
224 | parent::setParser($p); |
||
225 | |||
226 | $this->methods_plugin->setParser($p); |
||
227 | $this->statics_plugin->setParser($p); |
||
228 | } |
||
229 | |||
230 | public function getTypes(): array |
||
231 | { |
||
232 | return ['object']; |
||
233 | } |
||
234 | |||
235 | public function getTriggers(): int |
||
236 | { |
||
237 | return Parser::TRIGGER_BEGIN; |
||
238 | } |
||
239 | |||
240 | public function parseBegin(&$var, ContextInterface $c): ?AbstractValue |
||
241 | { |
||
242 | // Attributes and chardata (Which is parent of comments and text |
||
243 | // nodes) don't need children or attributes of their own |
||
244 | if ($var instanceof Attr || $var instanceof CharacterData || $var instanceof DOMAttr || $var instanceof DOMCharacterData) { |
||
245 | return $this->parseText($var, $c); |
||
246 | } |
||
247 | |||
248 | if ($var instanceof NamedNodeMap || $var instanceof NodeList || $var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) { |
||
249 | return $this->parseList($var, $c); |
||
250 | } |
||
251 | |||
252 | if ($var instanceof Node || $var instanceof DOMNode) { |
||
253 | return $this->parseNode($var, $c); |
||
254 | } |
||
255 | |||
256 | return null; |
||
257 | } |
||
258 | |||
259 | /** @psalm-param Node|DOMNode $var */ |
||
260 | private function parseProperty(object $var, string $prop, ContextInterface $c): AbstractValue |
||
261 | { |
||
262 | if (!isset($var->{$prop})) { |
||
263 | return new FixedWidthValue($c, null); |
||
264 | } |
||
265 | |||
266 | $parser = $this->getParser(); |
||
267 | $value = $var->{$prop}; |
||
268 | |||
269 | if (\is_scalar($value)) { |
||
270 | return $parser->parse($value, $c); |
||
271 | } |
||
272 | |||
273 | if (isset(self::$blacklist[$prop])) { |
||
274 | $b = new InstanceValue($c, \get_class($value), \spl_object_hash($value), \spl_object_id($value)); |
||
275 | $b->flags |= AbstractValue::FLAG_GENERATED | AbstractValue::FLAG_BLACKLIST; |
||
276 | |||
277 | return $b; |
||
278 | } |
||
279 | |||
280 | // Everything we can handle in parseBegin |
||
281 | 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) { |
||
282 | $out = $this->parseBegin($value, $c); |
||
283 | } |
||
284 | |||
285 | if (!isset($out)) { |
||
286 | // Shouldn't ever happen |
||
287 | $out = $parser->parse($value, $c); // @codeCoverageIgnore |
||
288 | } |
||
289 | |||
290 | $out->flags |= AbstractValue::FLAG_GENERATED; |
||
291 | |||
292 | return $out; |
||
293 | } |
||
294 | |||
295 | /** @psalm-param Attr|CharacterData|DOMAttr|DOMCharacterData $var */ |
||
296 | private function parseText(object $var, ContextInterface $c): AbstractValue |
||
297 | { |
||
298 | if ($c instanceof BaseContext && null !== $c->access_path) { |
||
299 | $c->access_path .= '->nodeValue'; |
||
300 | } |
||
301 | |||
302 | return $this->parseProperty($var, 'nodeValue', $c); |
||
303 | } |
||
304 | |||
305 | /** @psalm-param NamedNodeMap|NodeList|DOMNamedNodeMap|DOMNodeList $var */ |
||
306 | private function parseList(object $var, ContextInterface $c): InstanceValue |
||
307 | { |
||
308 | if ($var instanceof NodeList || $var instanceof DOMNodeList) { |
||
309 | $v = new DomNodeListValue($c, $var); |
||
310 | } else { |
||
311 | $v = new InstanceValue($c, \get_class($var), \spl_object_hash($var), \spl_object_id($var)); |
||
312 | } |
||
313 | |||
314 | $parser = $this->getParser(); |
||
315 | $pdepth = $parser->getDepthLimit(); |
||
316 | |||
317 | // Depth limit |
||
318 | // Use empty iterator representation since we need it to point out depth limits |
||
319 | if (($var instanceof NodeList || $var instanceof DOMNodeList) && $pdepth && $c->getDepth() >= $pdepth) { |
||
320 | $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT; |
||
321 | |||
322 | return $v; |
||
323 | } |
||
324 | |||
325 | if (self::$verbose) { |
||
326 | $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); |
||
327 | $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); |
||
328 | } |
||
329 | |||
330 | if (0 === $var->length) { |
||
331 | $v->setChildren([]); |
||
332 | |||
333 | return $v; |
||
334 | } |
||
335 | |||
336 | $cdepth = $c->getDepth(); |
||
337 | $ap = $c->getAccessPath(); |
||
338 | $contents = []; |
||
339 | |||
340 | foreach ($var as $key => $item) { |
||
341 | $base_obj = new BaseContext($item->nodeName); |
||
342 | $base_obj->depth = $cdepth + 1; |
||
343 | |||
344 | if ($var instanceof NamedNodeMap || $var instanceof DOMNamedNodeMap) { |
||
345 | if (null !== $ap) { |
||
346 | $base_obj->access_path = $ap.'['.\var_export($item->nodeName, true).']'; |
||
347 | } |
||
348 | } else { // NodeList |
||
349 | if (null !== $ap) { |
||
350 | $base_obj->access_path = $ap.'['.\var_export($key, true).']'; |
||
351 | } |
||
352 | } |
||
353 | |||
354 | if ($item instanceof HTMLElement) { |
||
355 | $base_obj->name = $item->localName; |
||
356 | } |
||
357 | |||
358 | $item = $parser->parse($item, $base_obj); |
||
359 | $item->flags |= AbstractValue::FLAG_GENERATED; |
||
360 | |||
361 | $contents[] = $item; |
||
362 | } |
||
363 | |||
364 | $v->setChildren($contents); |
||
365 | |||
366 | if ($contents) { |
||
367 | $v->addRepresentation(new ContainerRepresentation('Iterator', $contents), 0); |
||
368 | } |
||
369 | |||
370 | return $v; |
||
371 | } |
||
372 | |||
373 | /** @psalm-param Node|DOMNode $var */ |
||
374 | private function parseNode(object $var, ContextInterface $c): DomNodeValue |
||
375 | { |
||
376 | $class = \get_class($var); |
||
377 | $pdepth = $this->getParser()->getDepthLimit(); |
||
378 | |||
379 | if ($pdepth && $c->getDepth() >= $pdepth) { |
||
380 | $v = new DomNodeValue($c, $var); |
||
381 | $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT; |
||
382 | |||
383 | return $v; |
||
384 | } |
||
385 | |||
386 | if (($var instanceof DocumentType || $var instanceof DOMDocumentType) && $c instanceof BaseContext && $c->name === $var->nodeName) { |
||
387 | $c->name = '!DOCTYPE '.$c->name; |
||
388 | } |
||
389 | |||
390 | $cdepth = $c->getDepth(); |
||
391 | $ap = $c->getAccessPath(); |
||
392 | |||
393 | $properties = []; |
||
394 | $children = []; |
||
395 | $attributes = []; |
||
396 | |||
397 | foreach (self::getKnownProperties($var) as $prop => $readonly) { |
||
398 | $prop_c = new PropertyContext($prop, $class, ClassDeclaredContext::ACCESS_PUBLIC); |
||
399 | $prop_c->depth = $cdepth + 1; |
||
400 | $prop_c->readonly = KINT_PHP81 && $readonly; |
||
401 | |||
402 | if (null !== $ap) { |
||
403 | $prop_c->access_path = $ap.'->'.$prop; |
||
404 | } |
||
405 | |||
406 | $properties[] = $prop_obj = $this->parseProperty($var, $prop, $prop_c); |
||
407 | |||
408 | if ('childNodes' === $prop) { |
||
409 | if (!$prop_obj instanceof DomNodeListValue) { |
||
410 | throw new LogicException('childNodes property parsed incorrectly'); // @codeCoverageIgnore |
||
411 | } |
||
412 | $children = self::getChildren($prop_obj); |
||
413 | } elseif ('attributes' === $prop) { |
||
414 | $attributes = $prop_obj->getRepresentation('iterator'); |
||
415 | $attributes = $attributes instanceof ContainerRepresentation ? $attributes->getContents() : []; |
||
416 | } elseif ('classList' === $prop) { |
||
417 | if ($iter = $prop_obj->getRepresentation('iterator')) { |
||
418 | $prop_obj->removeRepresentation($iter); |
||
419 | $prop_obj->addRepresentation($iter, 0); |
||
420 | } |
||
421 | } |
||
422 | } |
||
423 | |||
424 | $v = new DomNodeValue($c, $var); |
||
425 | // If we're in text mode, we can see children through the childNodes property |
||
426 | $v->setChildren($properties); |
||
427 | |||
428 | if ($children) { |
||
429 | $v->addRepresentation(new ContainerRepresentation('Children', $children, null, true)); |
||
430 | } |
||
431 | |||
432 | if ($attributes) { |
||
433 | $v->addRepresentation(new ContainerRepresentation('Attributes', $attributes)); |
||
434 | } |
||
435 | |||
436 | if (self::$verbose) { |
||
437 | $v->addRepresentation(new ContainerRepresentation('Properties', $properties)); |
||
438 | |||
439 | $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); |
||
440 | $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); |
||
441 | } |
||
442 | |||
443 | return $v; |
||
444 | } |
||
445 | |||
446 | /** |
||
447 | * @psalm-param Node|DOMNode $var |
||
448 | * |
||
449 | * @psalm-return non-empty-array<string, bool> |
||
450 | */ |
||
451 | public static function getKnownProperties(object $var): array |
||
452 | { |
||
453 | if ($var instanceof Node) { |
||
454 | $known_properties = self::NODE_PROPS; |
||
455 | if ($var instanceof Element) { |
||
456 | $known_properties += self::ELEMENT_PROPS; |
||
457 | } |
||
458 | |||
459 | if ($var instanceof Document) { |
||
460 | $known_properties['textContent'] = true; |
||
461 | } |
||
462 | |||
463 | if ($var instanceof Attr || $var instanceof CharacterData) { |
||
464 | $known_properties['nodeValue'] = false; |
||
465 | } |
||
466 | |||
467 | foreach (self::DOM_NS_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 | } else { |
||
477 | $known_properties = self::DOMNODE_PROPS; |
||
478 | if ($var instanceof DOMElement) { |
||
479 | $known_properties += self::DOMELEMENT_PROPS; |
||
480 | } |
||
481 | |||
482 | foreach (self::DOM_VERSIONS as $key => $val) { |
||
483 | /** |
||
484 | * @psalm-var bool $val |
||
485 | * Psalm bug #4509 |
||
486 | */ |
||
487 | if (false === $val) { |
||
488 | unset($known_properties[$key]); // @codeCoverageIgnore |
||
489 | } |
||
490 | } |
||
491 | } |
||
492 | |||
493 | /** @psalm-var non-empty-array $known_properties */ |
||
494 | if (!self::$verbose) { |
||
495 | $known_properties = \array_intersect_key($known_properties, [ |
||
496 | 'nodeValue' => null, |
||
497 | 'childNodes' => null, |
||
498 | 'attributes' => null, |
||
499 | ]); |
||
500 | } |
||
501 | |||
502 | return $known_properties; |
||
503 | } |
||
504 | |||
505 | /** @psalm-return list<AbstractValue> */ |
||
506 | private static function getChildren(DomNodeListValue $property): array |
||
542 | } |
||
543 | } |
||
544 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths