| Total Complexity | 89 |
| Total Lines | 483 |
| Duplicated Lines | 0 % |
| Changes | 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