Passed
Push — master ( 3a6c9c...7c86a5 )
by Siad
06:49
created

CoverageReportTask   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 542
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 238
dl 0
loc 542
ccs 0
cts 250
cp 0
rs 5.04
c 1
b 0
f 0
wmc 57

16 Methods

Rating   Name   Duplication   Size   Complexity  
A setGeshiPath() 0 3 1
B highlightSourceFile() 0 50 6
A setOutfile() 0 3 1
A stripDiv() 0 12 1
A __construct() 0 7 1
A createReport() 0 6 1
A setGeshiLanguagesPath() 0 3 1
A main() 0 28 4
A addSubpackageToPackage() 0 17 3
A getSubpackageElement() 0 11 3
B calculateStatistics() 0 80 5
A addClassToSubpackage() 0 13 2
A addClassToPackage() 0 11 2
A getPackageElement() 0 11 3
F transformCoverageInformation() 0 124 20
A transformSourceFile() 0 34 3

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
/**
21
 * Transforms information in a code coverage database to XML
22
 *
23
 * @author  Michiel Rook <[email protected]>
24
 * @package phing.tasks.ext.coverage
25
 * @since   2.1.0
26
 */
27
class CoverageReportTask extends Task
28
{
29
    use ClasspathAware;
30
31
    private $outfile = "coverage.xml";
32
33
    private $transformers = [];
34
35
    /**
36
     * the path to the GeSHi library (optional)
37
     */
38
    private $geshipath = "";
39
40
    /**
41
     * the path to the GeSHi language files (optional)
42
     */
43
    private $geshilanguagespath = "";
44
45
    /**
46
     * @var DOMDocument
47
     */
48
    private $doc;
49
50
    /**
51
     * @param $path
52
     */
53
    public function setGeshiPath($path)
54
    {
55
        $this->geshipath = $path;
56
    }
57
58
    /**
59
     * @param $path
60
     */
61
    public function setGeshiLanguagesPath($path)
62
    {
63
        $this->geshilanguagespath = $path;
64
    }
65
66
    /**
67
     *
68
     */
69
    public function __construct()
70
    {
71
        parent::__construct();
72
        $this->doc = new DOMDocument();
73
        $this->doc->encoding = 'UTF-8';
74
        $this->doc->formatOutput = true;
75
        $this->doc->appendChild($this->doc->createElement('snapshot'));
76
    }
77
78
    /**
79
     * @param $outfile
80
     */
81
    public function setOutfile($outfile)
82
    {
83
        $this->outfile = $outfile;
84
    }
85
86
    /**
87
     * Generate a report based on the XML created by this task
88
     */
89
    public function createReport()
90
    {
91
        $transformer = new CoverageReportTransformer($this);
92
        $this->transformers[] = $transformer;
93
94
        return $transformer;
95
    }
96
97
    /**
98
     * @param $packageName
99
     * @return null
100
     */
101
    protected function getPackageElement($packageName)
102
    {
103
        $packages = $this->doc->documentElement->getElementsByTagName('package');
104
105
        foreach ($packages as $package) {
106
            if ($package->getAttribute('name') == $packageName) {
107
                return $package;
108
            }
109
        }
110
111
        return null;
112
    }
113
114
    /**
115
     * @param $packageName
116
     * @param $element
117
     */
118
    protected function addClassToPackage($packageName, $element)
119
    {
120
        $package = $this->getPackageElement($packageName);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $package is correct as $this->getPackageElement($packageName) targeting CoverageReportTask::getPackageElement() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
121
122
        if ($package === null) {
0 ignored issues
show
introduced by
The condition $package === null is always true.
Loading history...
123
            $package = $this->doc->createElement('package');
124
            $package->setAttribute('name', $packageName);
125
            $this->doc->documentElement->appendChild($package);
126
        }
127
128
        $package->appendChild($element);
129
    }
130
131
    /**
132
     * Adds a subpackage to their package
133
     *
134
     * @param string $packageName The name of the package
135
     * @param string $subpackageName The name of the subpackage
136
     *
137
     * @author Benjamin Schultz <[email protected]>
138
     * @return void
139
     */
140
    protected function addSubpackageToPackage($packageName, $subpackageName)
141
    {
142
        $package = $this->getPackageElement($packageName);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $package is correct as $this->getPackageElement($packageName) targeting CoverageReportTask::getPackageElement() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
143
        $subpackage = $this->getSubpackageElement($subpackageName);
144
145
        if ($package === null) {
0 ignored issues
show
introduced by
The condition $package === null is always true.
Loading history...
146
            $package = $this->doc->createElement('package');
147
            $package->setAttribute('name', $packageName);
148
            $this->doc->documentElement->appendChild($package);
149
        }
150
151
        if ($subpackage === null) {
152
            $subpackage = $this->doc->createElement('subpackage');
153
            $subpackage->setAttribute('name', $subpackageName);
154
        }
155
156
        $package->appendChild($subpackage);
157
    }
158
159
    /**
160
     * Returns the subpackage element
161
     *
162
     * @param string $subpackageName The name of the subpackage
163
     *
164
     * @author Benjamin Schultz <[email protected]>
165
     * @return DOMNode|null null when no DOMNode with the given name exists
166
     */
167
    protected function getSubpackageElement($subpackageName)
168
    {
169
        $subpackages = $this->doc->documentElement->getElementsByTagName('subpackage');
170
171
        foreach ($subpackages as $subpackage) {
172
            if ($subpackage->getAttribute('name') == $subpackageName) {
173
                return $subpackage;
174
            }
175
        }
176
177
        return null;
178
    }
179
180
    /**
181
     * Adds a class to their subpackage
182
     *
183
     * @param string $classname The name of the class
184
     * @param DOMNode $element The dom node to append to the subpackage element
185
     *
186
     * @author Benjamin Schultz <[email protected]>
187
     * @return void
188
     */
189
    protected function addClassToSubpackage($classname, $element)
190
    {
191
        $subpackageName = PHPUnitUtil::getSubpackageName($classname);
192
193
        $subpackage = $this->getSubpackageElement($subpackageName);
194
195
        if ($subpackage === null) {
196
            $subpackage = $this->doc->createElement('subpackage');
197
            $subpackage->setAttribute('name', $subpackageName);
198
            $this->doc->documentElement->appendChild($subpackage);
199
        }
200
201
        $subpackage->appendChild($element);
202
    }
203
204
    /**
205
     * @param $source
206
     * @return string
207
     */
208
    protected function stripDiv($source)
209
    {
210
        $openpos = strpos($source, "<div");
211
        $closepos = strpos($source, ">", $openpos);
212
213
        $line = substr($source, $closepos + 1);
214
215
        $tagclosepos = strpos($line, "</div>");
216
217
        $line = substr($line, 0, $tagclosepos);
218
219
        return $line;
220
    }
221
222
    /**
223
     * @param $filename
224
     * @return array
225
     */
226
    protected function highlightSourceFile($filename)
227
    {
228
        if ($this->geshipath) {
229
            include_once $this->geshipath . '/geshi.php';
230
231
            $source = file_get_contents($filename);
232
233
            $geshi = new GeSHi($source, 'php', $this->geshilanguagespath);
0 ignored issues
show
Bug introduced by
The type GeSHi 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...
234
235
            $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
0 ignored issues
show
Bug introduced by
The constant GESHI_NORMAL_LINE_NUMBERS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
236
237
            $geshi->enable_strict_mode(true);
238
239
            $geshi->enable_classes(true);
240
241
            $geshi->set_url_for_keyword_group(3, '');
242
243
            $html = $geshi->parse_code();
244
245
            $lines = preg_split("#</?li>#", $html);
246
247
            // skip first and last line
248
            array_pop($lines);
0 ignored issues
show
Bug introduced by
It seems like $lines can also be of type false; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

248
            array_pop(/** @scrutinizer ignore-type */ $lines);
Loading history...
249
            array_shift($lines);
250
251
            $lines = array_filter($lines);
252
253
            $lines = array_map([$this, 'stripDiv'], $lines);
254
255
            return $lines;
256
        } else {
257
            $lines = file($filename);
258
259
            for ($i = 0; $i < count($lines); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Bug introduced by
It seems like $lines can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
            for ($i = 0; $i < count(/** @scrutinizer ignore-type */ $lines); $i++) {
Loading history...
260
                $line = $lines[$i];
261
262
                $line = rtrim($line);
263
264
                if (function_exists('mb_check_encoding') && mb_check_encoding($line, 'UTF-8')) {
265
                    $lines[$i] = $line;
266
                } else {
267
                    if (function_exists('mb_convert_encoding')) {
268
                        $lines[$i] = mb_convert_encoding($line, 'UTF-8');
269
                    } else {
270
                        $lines[$i] = utf8_encode($line);
271
                    }
272
                }
273
            }
274
275
            return $lines;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $lines could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
276
        }
277
    }
278
279
    /**
280
     * @param $filename
281
     * @param $coverageInformation
282
     * @param int $classStartLine
283
     * @return DOMElement
284
     */
285
    protected function transformSourceFile($filename, $coverageInformation, $classStartLine = 1)
286
    {
287
        $sourceElement = $this->doc->createElement('sourcefile');
288
        $sourceElement->setAttribute('name', basename($filename));
289
290
        /**
291
         * Add original/full filename to document
292
         */
293
        $sourceElement->setAttribute('sourcefile', $filename);
294
295
        $filelines = $this->highlightSourceFile($filename);
296
297
        $linenr = 1;
298
299
        foreach ($filelines as $line) {
300
            $lineElement = $this->doc->createElement('sourceline');
301
            $lineElement->setAttribute(
302
                'coveredcount',
303
                ($coverageInformation[$linenr] ?? '0')
304
            );
305
306
            if ($linenr == $classStartLine) {
307
                $lineElement->setAttribute('startclass', 1);
308
            }
309
310
            $textnode = $this->doc->createTextNode($line);
311
            $lineElement->appendChild($textnode);
312
313
            $sourceElement->appendChild($lineElement);
314
315
            $linenr++;
316
        }
317
318
        return $sourceElement;
319
    }
320
321
    /**
322
     * Transforms the coverage information
323
     *
324
     * @param string $filename The filename
325
     * @param array $coverageInformation Array with covergae information
326
     *
327
     * @author Michiel Rook <[email protected]>
328
     * @author Benjamin Schultz <[email protected]>
329
     * @return void
330
     */
331
    protected function transformCoverageInformation($filename, $coverageInformation)
332
    {
333
        $classes = PHPUnitUtil::getDefinedClasses($filename, $this->classpath);
334
335
        if (is_array($classes)) {
0 ignored issues
show
introduced by
The condition is_array($classes) is always true.
Loading history...
336
            foreach ($classes as $classname) {
337
                $reflection = new ReflectionClass($classname);
338
                $methods = $reflection->getMethods();
339
340
                if (method_exists($reflection, 'getShortName')) {
341
                    $className = $reflection->getShortName();
342
                } else {
343
                    $className = $reflection->getName();
344
                }
345
346
                $classElement = $this->doc->createElement('class');
347
                $classElement->setAttribute('name', $className);
348
349
                $packageName = PHPUnitUtil::getPackageName($reflection->getName());
350
                $subpackageName = PHPUnitUtil::getSubpackageName($reflection->getName());
351
352
                if ($subpackageName !== null) {
353
                    $this->addSubpackageToPackage($packageName, $subpackageName);
354
                    $this->addClassToSubpackage($reflection->getName(), $classElement);
355
                } else {
356
                    $this->addClassToPackage($packageName, $classElement);
357
                }
358
359
                $classStartLine = $reflection->getStartLine();
360
361
                $methodscovered = 0;
362
                $methodcount = 0;
363
364
                // Strange PHP5 reflection bug, classes without parent class or implemented interfaces seem to start one line off
365
                if ($reflection->getParentClass() == null && count($reflection->getInterfaces()) == 0) {
366
                    unset($coverageInformation[$classStartLine + 1]);
367
                } else {
368
                    unset($coverageInformation[$classStartLine]);
369
                }
370
371
                // Remove out-of-bounds info
372
                unset($coverageInformation[0]);
373
374
                reset($coverageInformation);
375
376
                foreach ($methods as $method) {
377
                    // PHP5 reflection considers methods of a parent class to be part of a subclass, we don't
378
                    if ($method->getDeclaringClass()->getName() != $reflection->getName()) {
379
                        continue;
380
                    }
381
382
                    // small fix for XDEBUG_CC_UNUSED
383
                    if (isset($coverageInformation[$method->getStartLine()])) {
384
                        unset($coverageInformation[$method->getStartLine()]);
385
                    }
386
387
                    if (isset($coverageInformation[$method->getEndLine()])) {
388
                        unset($coverageInformation[$method->getEndLine()]);
389
                    }
390
391
                    if ($method->isAbstract()) {
392
                        continue;
393
                    }
394
395
                    $linenr = key($coverageInformation);
396
397
                    while ($linenr !== null && $linenr < $method->getStartLine()) {
398
                        next($coverageInformation);
399
                        $linenr = key($coverageInformation);
400
                    }
401
402
                    $methodCoveredCount = 0;
403
                    $methodTotalCount = 0;
404
405
                    $methodHasCoveredLine = false;
406
407
                    while ($linenr !== null && $linenr <= $method->getEndLine()) {
408
                        $methodTotalCount++;
409
                        $methodHasCoveredLine = true;
410
411
                        // set covered when CODE is other than -1 (not executed)
412
                        if ($coverageInformation[$linenr] > 0 || $coverageInformation[$linenr] == -2) {
413
                            $methodCoveredCount++;
414
                        }
415
416
                        next($coverageInformation);
417
                        $linenr = key($coverageInformation);
418
                    }
419
420
                    if (($methodTotalCount == $methodCoveredCount) && $methodHasCoveredLine) {
421
                        $methodscovered++;
422
                    }
423
424
                    $methodcount++;
425
                }
426
427
                $statementcount = count(
428
                    array_filter(
429
                        $coverageInformation,
430
                        function ($var) {
431
                            return ($var != -2);
432
                        }
433
                    )
434
                );
435
436
                $statementscovered = count(
437
                    array_filter(
438
                        $coverageInformation,
439
                        function ($var) {
440
                            return ($var >= 0);
441
                        }
442
                    )
443
                );
444
445
                $classElement->appendChild(
446
                    $this->transformSourceFile($filename, $coverageInformation, $classStartLine)
447
                );
448
449
                $classElement->setAttribute('methodcount', $methodcount);
450
                $classElement->setAttribute('methodscovered', $methodscovered);
451
                $classElement->setAttribute('statementcount', $statementcount);
452
                $classElement->setAttribute('statementscovered', $statementscovered);
453
                $classElement->setAttribute('totalcount', $methodcount + $statementcount);
454
                $classElement->setAttribute('totalcovered', $methodscovered + $statementscovered);
455
            }
456
        }
457
    }
458
459
    protected function calculateStatistics()
460
    {
461
        $packages = $this->doc->documentElement->getElementsByTagName('package');
462
463
        $totalmethodcount = 0;
464
        $totalmethodscovered = 0;
465
466
        $totalstatementcount = 0;
467
        $totalstatementscovered = 0;
468
469
        foreach ($packages as $package) {
470
            $methodcount = 0;
471
            $methodscovered = 0;
472
473
            $statementcount = 0;
474
            $statementscovered = 0;
475
476
            $subpackages = $package->getElementsByTagName('subpackage');
477
478
            foreach ($subpackages as $subpackage) {
479
                $subpackageMethodCount = 0;
480
                $subpackageMethodsCovered = 0;
481
482
                $subpackageStatementCount = 0;
483
                $subpackageStatementsCovered = 0;
484
485
                $subpackageClasses = $subpackage->getElementsByTagName('class');
486
487
                foreach ($subpackageClasses as $subpackageClass) {
488
                    $subpackageMethodCount += $subpackageClass->getAttribute('methodcount');
489
                    $subpackageMethodsCovered += $subpackageClass->getAttribute('methodscovered');
490
491
                    $subpackageStatementCount += $subpackageClass->getAttribute('statementcount');
492
                    $subpackageStatementsCovered += $subpackageClass->getAttribute('statementscovered');
493
                }
494
495
                $subpackage->setAttribute('methodcount', $subpackageMethodCount);
496
                $subpackage->setAttribute('methodscovered', $subpackageMethodsCovered);
497
498
                $subpackage->setAttribute('statementcount', $subpackageStatementCount);
499
                $subpackage->setAttribute('statementscovered', $subpackageStatementsCovered);
500
501
                $subpackage->setAttribute('totalcount', $subpackageMethodCount + $subpackageStatementCount);
502
                $subpackage->setAttribute('totalcovered', $subpackageMethodsCovered + $subpackageStatementsCovered);
503
            }
504
505
            $classes = $package->getElementsByTagName('class');
506
507
            foreach ($classes as $class) {
508
                $methodcount += $class->getAttribute('methodcount');
509
                $methodscovered += $class->getAttribute('methodscovered');
510
511
                $statementcount += $class->getAttribute('statementcount');
512
                $statementscovered += $class->getAttribute('statementscovered');
513
            }
514
515
            $package->setAttribute('methodcount', $methodcount);
516
            $package->setAttribute('methodscovered', $methodscovered);
517
518
            $package->setAttribute('statementcount', $statementcount);
519
            $package->setAttribute('statementscovered', $statementscovered);
520
521
            $package->setAttribute('totalcount', $methodcount + $statementcount);
522
            $package->setAttribute('totalcovered', $methodscovered + $statementscovered);
523
524
            $totalmethodcount += $methodcount;
525
            $totalmethodscovered += $methodscovered;
526
527
            $totalstatementcount += $statementcount;
528
            $totalstatementscovered += $statementscovered;
529
        }
530
531
        $this->doc->documentElement->setAttribute('methodcount', $totalmethodcount);
532
        $this->doc->documentElement->setAttribute('methodscovered', $totalmethodscovered);
533
534
        $this->doc->documentElement->setAttribute('statementcount', $totalstatementcount);
535
        $this->doc->documentElement->setAttribute('statementscovered', $totalstatementscovered);
536
537
        $this->doc->documentElement->setAttribute('totalcount', $totalmethodcount + $totalstatementcount);
538
        $this->doc->documentElement->setAttribute('totalcovered', $totalmethodscovered + $totalstatementscovered);
539
    }
540
541
    public function main()
542
    {
543
        $coverageDatabase = $this->project->getProperty('coverage.database');
544
545
        if (!$coverageDatabase) {
546
            throw new BuildException("Property coverage.database is not set - please include coverage-setup in your build file");
547
        }
548
549
        $database = new PhingFile($coverageDatabase);
550
551
        $this->log("Transforming coverage report");
552
553
        $props = new Properties();
554
        $props->load($database);
555
556
        foreach ($props->keys() as $filename) {
557
            $file = unserialize($props->getProperty($filename));
558
559
            $this->transformCoverageInformation($file['fullname'], $file['coverage']);
560
        }
561
562
        $this->calculateStatistics();
563
564
        $this->doc->save($this->outfile);
565
566
        foreach ($this->transformers as $transformer) {
567
            $transformer->setXmlDocument($this->doc);
568
            $transformer->transform();
569
        }
570
    }
571
}
572