Element   B
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 493
Duplicated Lines 4.67 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 99.34%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 5
dl 23
loc 493
ccs 150
cts 151
cp 0.9934
rs 7.0642
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getChildren() 0 4 1
A hasChild() 0 4 1
A __construct() 0 21 3
A getAttributes() 0 9 2
A getAttribute() 0 10 2
A hasAttribute() 0 4 1
B addAttribute() 8 27 3
A removeAttribute() 0 7 2
A appendChild() 0 5 1
A prependChild() 0 5 1
A removeChild() 7 7 2
A getName() 0 4 1
C clean() 8 58 9
A fixSelf() 0 3 1
A removeInvalidChildren() 0 3 1
A removeInvalidSelf() 0 4 1
B getAllowedAttributes() 0 151 1
A getAttributeParameters() 0 15 3
C remove() 0 30 8
A toHtml() 0 11 2
A buildStartTag() 0 13 4
A buildChildrenHtml() 0 14 2
A getType() 0 4 1
A __toString() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
namespace Groundskeeper\Tokens;
4
5
use Groundskeeper\Configuration;
6
use Psr\Log\LoggerInterface;
7
8
class Element extends AbstractToken implements Cleanable, ContainsChildren, Removable
9
{
10
    /** @var array */
11
    protected $attributes;
12
13
    /** @var Token[] */
14
    protected $children;
15
16
    /** @var string */
17
    private $name;
18
19
    /**
20
     * Constructor
21
     */
22 185
    public function __construct(Configuration $configuration,
23
                                int $line,
24
                                int $position,
25
                                string $name,
26
                                array $attributes = array())
27
    {
28 185
        parent::__construct($configuration, $line, $position);
29
30 185
        $this->attributes = array();
31 185
        foreach ($attributes as $key => $value) {
32 99
            $this->addAttribute($key, $value);
33
        }
34
35 185
        $this->children = array();
36
37 185
        if (!is_string($name)) {
38
            throw new \InvalidArgumentException('Element name must be string type.');
39
        }
40
41 185
        $this->name = trim(strtolower($name));
42 185
    }
43
44
    /**
45
     * Getter for 'attributes'.
46
     */
47 2
    public function getAttributes() : array
48
    {
49 2
        $attributeArray = array();
50 2
        foreach ($this->attributes as $attribute) {
51 1
            $attributeArray[$attribute->getName()] = $attribute->getValue();
52
        }
53
54 2
        return $attributeArray;
55
    }
56
57 4
    public function getAttribute(string $key)
58
    {
59 4
        if (!$this->hasAttribute($key)) {
60 1
            throw new \InvalidArgumentException('Invalid attribute key: ' . $key);
61
        }
62
63 3
        $attributeObject = $this->attributes[$key];
64
65 3
        return $attributeObject->getValue();
66
    }
67
68
    /**
69
     * Hasser for 'attributes'.
70
     *
71
     * @param string $key
72
     *
73
     * @return bool True if the attribute is present.
74
     */
75 45
    public function hasAttribute(string $key) : bool
76
    {
77 45
        return array_key_exists($key, $this->attributes);
78
    }
79
80 102
    public function addAttribute(string $key, $value)
81
    {
82 102
        $key = trim(strtolower($key));
83 102
        if ($key === '') {
84 1
            throw new \InvalidArgumentException('Invalid empty attribute key.');
85
        }
86
87 101
        $attributeParameters = $this->getAttributeParameters($key);
88 101
        $isStandard = true;
89 101 View Code Duplication
        if (empty($attributeParameters)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
90
            $attributeParameters = array(
91 11
                'name' => $key,
92 11
                'regex' => '/\S*/i',
93
                'valueType' => Attribute::CS_STRING
94
            );
95 11
            $isStandard = false;
96
        }
97
98 101
        $this->attributes[$key] = new Attribute(
99 101
            $key,
100 101
            $value,
101 101
            $attributeParameters['valueType'],
0 ignored issues
show
Unused Code introduced by
The call to Attribute::__construct() has too many arguments starting with $attributeParameters['valueType'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
102 101
            $isStandard
103
        );
104
105 101
        return $this;
106
    }
107
108 2
    public function removeAttribute(string $key)
109
    {
110 2
        $key = trim(strtolower($key));
111 2
        if (isset($this->attributes[$key])) {
112 2
            unset($this->attributes[$key]);
113
        }
114 2
    }
115
116
    /**
117
     * Required by ContainsChildren interface.
118
     */
119 3
    public function getChildren() : array
120
    {
121 3
        return $this->children;
122
    }
123
124
    /**
125
     * Required by ContainsChildren interface.
126
     */
127 1
    public function hasChild(Token $token) : bool
128
    {
129 1
        return in_array($token, $this->children, true);
130
    }
131
132
    /**
133
     * Required by ContainsChildren interface.
134
     */
135 140
    public function appendChild(Token $token)
136
    {
137 140
        $token->setParent($this);
138 140
        $this->children[] = $token;
139 140
    }
140
141
    /**
142
     * Required by ContainsChildren interface.
143
     */
144 5
    public function prependChild(Token $token)
145
    {
146 5
        $token->setParent($this);
147 5
        array_unshift($this->children, $token);
148 5
    }
149
150
    /**
151
     * Required by the ContainsChildren interface.
152
     */
153 39 View Code Duplication
    public function removeChild(Token $token)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
154
    {
155 39
        $key = array_search($token, $this->children, true);
156 39
        if ($key !== false) {
157 39
            unset($this->children[$key]);
158
        }
159 39
    }
160
161
    /**
162
     * Getter for 'name'.
163
     */
164 137
    public function getName() : string
165
    {
166 137
        return $this->name;
167
    }
168
169
    /**
170
     * Required by the Cleanable interface.
171
     */
172 140
    public function clean(LoggerInterface $logger) : bool
173
    {
174 140
        if ($this->configuration->get('clean-strategy') === Configuration::CLEAN_STRATEGY_NONE) {
175 1
            return true;
176
        }
177
178
        // Assign attributes to the attributes. (Soooo meta ....)
179 139
        foreach ($this->attributes as $attribute) {
180 92
            $attributeParameters = $this->getAttributeParameters(
181 92
                $attribute->getName()
182
            );
183 92
            $isStandard = true;
184 92 View Code Duplication
            if (empty($attributeParameters)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
                $attributeParameters = array(
186 9
                    'name' => $attribute->getName(),
187 9
                    'regex' => '/\S*/i',
188
                    'valueType' => Attribute::UNKNOWN
189
                );
190 9
                $isStandard = false;
191
            }
192
193 92
            $attribute->setType($attributeParameters['valueType']);
194 92
            $attribute->setIsStandard($isStandard);
195
        }
196
197
        // Clean attributes.
198 139
        foreach ($this->attributes as $attribute) {
199 92
            $attributeCleanResult = $attribute->clean(
200 92
                $this->configuration,
201 92
                $this,
202 92
                $logger
203
            );
204 92
            if (!$attributeCleanResult && $this->configuration->get('clean-strategy') !== Configuration::CLEAN_STRATEGY_LENIENT) {
205 92
                unset($this->attributes[$attribute->getName()]);
206
            }
207
        }
208
209
        // Fix self (if possible)
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
210 139
        $this->fixSelf($logger);
211
212
        // Remove self or children?
213 139
        if ($this->configuration->get('clean-strategy') !== Configuration::CLEAN_STRATEGY_LENIENT) {
214
            // Remove self?
215 139
            if ($this->removeInvalidSelf($logger)) {
216 32
                return false;
217
            }
218
219
            // Remove children?
220 138
            $this->removeInvalidChildren($logger);
221
        }
222
223
        // Clean children.
224 139
        return AbstractToken::cleanChildTokens(
225 139
            $this->configuration,
226 139
            $this->children,
227 139
            $logger
228
        );
229
    }
230
231 132
    protected function fixSelf(LoggerInterface $logger)
232
    {
233 132
    }
234
235 118
    protected function removeInvalidChildren(LoggerInterface $logger)
236
    {
237 118
    }
238
239 129
    protected function removeInvalidSelf(LoggerInterface $logger) : bool
240
    {
241 129
        return false;
242
    }
243
244 101
    protected function getAllowedAttributes()
245
    {
246
        return array(
247
            // Global Attributes
248 101
            '/^accesskey$/i' => Attribute::CS_STRING,
249
            '/^class$/i' => Attribute::CS_STRING,
250
            '/^contenteditable$/i' => Attribute::CS_STRING,
251
            '/^contextmenu$/i' => Attribute::CS_STRING,
252
            '/^data-\S/i' => Attribute::CS_STRING,
253
            '/^dir$/i' => Attribute::CI_ENUM . '("ltr","rtl"|"ltr")',
254
            '/^draggable$/i' => Attribute::CS_STRING,
255
            '/^dropzone$/i' => Attribute::CS_STRING,
256
            '/^hidden$/i' => Attribute::CS_STRING,
257
            '/^id$/i' => Attribute::CS_STRING,
258
            '/^is$/i' => Attribute::CS_STRING,
259
            '/^itemid$/i' => Attribute::CS_STRING,
260
            '/^itemprop$/i' => Attribute::CS_STRING,
261
            '/^itemref$/i' => Attribute::CS_STRING,
262
            '/^itemscope$/i' => Attribute::CS_STRING,
263
            '/^itemtype$/i' => Attribute::CS_STRING,
264
            '/^lang$/i' => Attribute::CI_STRING,
265
            '/^slot$/i' => Attribute::CS_STRING,
266
            '/^spellcheck$/i' => Attribute::CS_STRING,
267
            '/^style$/i' => Attribute::CS_STRING,
268
            '/^tabindex$/i' => Attribute::CS_STRING,
269
            '/^title$/i' => Attribute::CS_STRING,
270
            '/^translate$/i' => Attribute::CI_ENUM . '("yes","no",""|"yes")',
271
272
            // Event Handler Content Attributes
273
            // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes
274
            '/^onabort$/i' => Attribute::JS,
275
            '/^onautocomplete$/i' => Attribute::JS,
276
            '/^onautocompleteerror$/i' => Attribute::JS,
277
            '/^onblur$/i' => Attribute::JS,
278
            '/^oncancel$/i' => Attribute::JS,
279
            '/^oncanplay$/i' => Attribute::JS,
280
            '/^oncanplaythrough$/i' => Attribute::JS,
281
            '/^onchange$/i' => Attribute::JS,
282
            '/^onclick$/i' => Attribute::JS,
283
            '/^onclose$/i' => Attribute::JS,
284
            '/^oncontextmenu$/i' => Attribute::JS,
285
            '/^oncuechange$/i' => Attribute::JS,
286
            '/^ondblclick$/i' => Attribute::JS,
287
            '/^ondrag$/i' => Attribute::JS,
288
            '/^ondragend$/i' => Attribute::JS,
289
            '/^ondragenter$/i' => Attribute::JS,
290
            '/^ondragexit$/i' => Attribute::JS,
291
            '/^ondragleave$/i' => Attribute::JS,
292
            '/^ondragover$/i' => Attribute::JS,
293
            '/^ondragstart$/i' => Attribute::JS,
294
            '/^ondrop$/i' => Attribute::JS,
295
            '/^ondurationchange$/i' => Attribute::JS,
296
            '/^onemptied$/i' => Attribute::JS,
297
            '/^onended$/i' => Attribute::JS,
298
            '/^onerror$/i' => Attribute::JS,
299
            '/^onfocus$/i' => Attribute::JS,
300
            '/^oninput$/i' => Attribute::JS,
301
            '/^oninvalid$/i' => Attribute::JS,
302
            '/^onkeydown$/i' => Attribute::JS,
303
            '/^onkeypress$/i' => Attribute::JS,
304
            '/^onkeyup$/i' => Attribute::JS,
305
            '/^onload$/i' => Attribute::JS,
306
            '/^onloadeddata$/i' => Attribute::JS,
307
            '/^onloadedmetadata$/i' => Attribute::JS,
308
            '/^onloadstart$/i' => Attribute::JS,
309
            '/^onmousedown$/i' => Attribute::JS,
310
            '/^onmouseenter$/i' => Attribute::JS,
311
            '/^onmouseleave$/i' => Attribute::JS,
312
            '/^onmousemove$/i' => Attribute::JS,
313
            '/^onmouseout$/i' => Attribute::JS,
314
            '/^onmouseover$/i' => Attribute::JS,
315
            '/^onmouseup$/i' => Attribute::JS,
316
            '/^onwheel$/i' => Attribute::JS,
317
            '/^onpause$/i' => Attribute::JS,
318
            '/^onplay$/i' => Attribute::JS,
319
            '/^onplaying$/i' => Attribute::JS,
320
            '/^onprogress$/i' => Attribute::JS,
321
            '/^onratechange$/i' => Attribute::JS,
322
            '/^onreset$/i' => Attribute::JS,
323
            '/^onresize$/i' => Attribute::JS,
324
            '/^onscroll$/i' => Attribute::JS,
325
            '/^onseeked$/i' => Attribute::JS,
326
            '/^onseeking$/i' => Attribute::JS,
327
            '/^onselect$/i' => Attribute::JS,
328
            '/^onshow$/i' => Attribute::JS,
329
            '/^onstalled$/i' => Attribute::JS,
330
            '/^onsubmit$/i' => Attribute::JS,
331
            '/^onsuspend$/i' => Attribute::JS,
332
            '/^ontimeupdate$/i' => Attribute::JS,
333
            '/^ontoggle$/i' => Attribute::JS,
334
            '/^onvolumechange$/i' => Attribute::JS,
335
            '/^onwaiting$/i' => Attribute::JS,
336
337
            // WAI-ARIA
338
            // https://w3c.github.io/aria/aria/aria.html
339
            '/^role$/i' => Attribute::CI_STRING,
340
341
            // ARIA global states and properties
342
            '/^aria-atomic$/i' => Attribute::CS_STRING,
343
            '/^aria-busy$/i' => Attribute::CS_STRING,
344
            '/^aria-controls$/i' => Attribute::CS_STRING,
345
            '/^aria-current$/i' => Attribute::CS_STRING,
346
            '/^aria-describedby$/i' => Attribute::CS_STRING,
347
            '/^aria-details$/i' => Attribute::CS_STRING,
348
            '/^aria-disabled$/i' => Attribute::CS_STRING,
349
            '/^aria-dropeffect$/i' => Attribute::CS_STRING,
350
            '/^aria-errormessage$/i' => Attribute::CS_STRING,
351
            '/^aria-flowto$/i' => Attribute::CS_STRING,
352
            '/^aria-grabbed$/i' => Attribute::CS_STRING,
353
            '/^aria-haspopup$/i' => Attribute::CS_STRING,
354
            '/^aria-hidden$/i' => Attribute::CS_STRING,
355
            '/^aria-invalid$/i' => Attribute::CS_STRING,
356
            '/^aria-label$/i' => Attribute::CS_STRING,
357
            '/^aria-labelledby$/i' => Attribute::CS_STRING,
358
            '/^aria-live$/i' => Attribute::CS_STRING,
359
            '/^aria-owns$/i' => Attribute::CS_STRING,
360
            '/^aria-relevant$/i' => Attribute::CS_STRING,
361
            '/^aria-roledescription$/i' => Attribute::CS_STRING,
362
363
            // ARIA widget attributes
364
            '/^aria-autocomplete$/i' => Attribute::CS_STRING,
365
            '/^aria-checked$/i' => Attribute::CS_STRING,
366
            '/^aria-expanded$/i' => Attribute::CS_STRING,
367
            '/^aria-level$/i' => Attribute::CS_STRING,
368
            '/^aria-modal$/i' => Attribute::CS_STRING,
369
            '/^aria-multiline$/i' => Attribute::CS_STRING,
370
            '/^aria-multiselectable$/i' => Attribute::CS_STRING,
371
            '/^aria-orientation$/i' => Attribute::CS_STRING,
372
            '/^aria-placeholder$/i' => Attribute::CS_STRING,
373
            '/^aria-pressed$/i' => Attribute::CS_STRING,
374
            '/^aria-readonly$/i' => Attribute::CS_STRING,
375
            '/^aria-required$/i' => Attribute::CS_STRING,
376
            '/^aria-selected$/i' => Attribute::CS_STRING,
377
            '/^aria-sort$/i' => Attribute::CS_STRING,
378
            '/^aria-valuemax$/i' => Attribute::CS_STRING,
379
            '/^aria-valuemin$/i' => Attribute::CS_STRING,
380
            '/^aria-valuenow$/i' => Attribute::CS_STRING,
381
            '/^aria-valuetext$/i' => Attribute::CS_STRING,
382
383
            // ARIA relationship attributes
384
            '/^aria-activedescendant$/i' => Attribute::CS_STRING,
385
            '/^aria-colcount$/i' => Attribute::CS_STRING,
386
            '/^aria-colindex$/i' => Attribute::CS_STRING,
387
            '/^aria-colspan$/i' => Attribute::CS_STRING,
388
            '/^aria-posinset$/i' => Attribute::CS_STRING,
389
            '/^aria-rowcount$/i' => Attribute::CS_STRING,
390
            '/^aria-rowindex$/i' => Attribute::CS_STRING,
391
            '/^aria-rowspan$/i' => Attribute::CS_STRING,
392
            '/^aria-setsize$/i' => Attribute::CS_STRING
393
        );
394
    }
395
396 101
    private function getAttributeParameters(string $key) : array
397
    {
398 101
        $allowedAttributes = $this->getAllowedAttributes();
399 101
        foreach ($allowedAttributes as $attrRegex => $valueType) {
400 101
            if (preg_match($attrRegex, $key) === 1) {
401
                return array(
402 100
                    'name' => $key,
403 100
                    'regex' => $attrRegex,
404 101
                    'valueType' => $valueType
405
                );
406
            }
407
        }
408
409 11
        return array();
410
    }
411
412
    /**
413
     * Required by the Removable interface.
414
     */
415 137
    public function remove(LoggerInterface $logger)
416
    {
417 137
        $hasRemovableElements = $this->configuration->get('element-blacklist') !== '';
418 137
        $hasRemovableTypes = $this->configuration->get('type-blacklist') !== '';
419 137
        foreach ($this->children as $child) {
420
            // Check types.
421 131
            if ($hasRemovableTypes &&
422 131
                !$this->configuration->isAllowedType($child->getType())) {
423 2
                $logger->debug('Removing ' . $child);
424 2
                $this->removeChild($child);
425
426 2
                continue;
427
            }
428
429
            // Check elements.
430 129
            if ($hasRemovableElements &&
431 129
                $child instanceof self &&
432 129
                !$this->configuration->isAllowedElement($child->getName())) {
433 3
                $logger->debug('Removing ' . $child);
434 3
                $this->removeChild($child);
435
436 3
                continue;
437
            }
438
439
            // Check children.
440 129
            if ($child instanceof Removable) {
441 129
                $child->remove($logger);
442
            }
443
        }
444 137
    }
445
446
    /**
447
     * Required by the Token interface.
448
     */
449 9
    public function toHtml(string $prefix, string $suffix) : string
450
    {
451 9
        $output = $this->buildStartTag($prefix, $suffix);
452 9
        if (empty($this->children)) {
453 6
            return $output;
454
        }
455
456 7
        $output .= $this->buildChildrenHtml($prefix, $suffix);
457
458 7
        return $output . $prefix . '</' . $this->name . '>' . $suffix;
459
    }
460
461 143
    protected function buildStartTag(string $prefix, string $suffix, bool $forceOpen = false) : string
462
    {
463 143
        $output = $prefix . '<' . $this->name;
464 143
        foreach ($this->attributes as $attribute) {
465 94
            $output .= ' ' . (string) $attribute;
466
        }
467
468 143
        if (!$forceOpen && empty($this->children)) {
469 53
            return $output . '/>' . $suffix;
470
        }
471
472 139
        return $output . '>' . $suffix;
473
    }
474
475 139
    protected function buildChildrenHtml(string $prefix, string $suffix) : string
476
    {
477 139
        $output = '';
478 139
        foreach ($this->children as $child) {
479
            $newPrefix = $prefix .
480 135
                str_repeat(
481 135
                    ' ',
482 135
                    $this->configuration->get('indent-spaces')
483
                );
484 135
            $output .= $child->toHtml($newPrefix, $suffix);
485
        }
486
487 139
        return $output;
488
    }
489
490 27
    public function getType() : string
491
    {
492 27
        return Token::ELEMENT;
493
    }
494
495 91
    public function __toString()
496
    {
497 91
        return '"' . $this->name . '" element (line: ' . $this->getLine() .
498 91
            '; position: ' . $this->getPosition() . ')';
499
    }
500
}
501