CoverageReportTask::transformCoverageInformation()   F
last analyzed

Complexity

Conditions 20
Paths 434

Size

Total Lines 124
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 420

Importance

Changes 0
Metric Value
eloc 71
dl 0
loc 124
ccs 0
cts 76
cp 0
rs 0.7861
c 0
b 0
f 0
cc 20
nc 434
nop 2
crap 420

How to fix   Long Method    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
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\Ext\Coverage;
22
23
use Phing\Task;
24
use Phing\Type\Element\ClasspathAware;
25
use Phing\Exception\BuildException;
26
use Phing\Io\File;
27
use Phing\Util\Properties;
28
use Phing\Task\Ext\PhpUnit\PHPUnitUtil;
29
30
/**
31
 * Transforms information in a code coverage database to XML
32
 *
33
 * @author  Michiel Rook <[email protected]>
34
 * @package phing.tasks.ext.coverage
35
 * @since   2.1.0
36
 */
37
class CoverageReportTask extends Task
38
{
39
    use ClasspathAware;
40
41
    private $outfile = "coverage.xml";
42
43
    private $transformers = [];
44
45
    /**
46
     * the path to the GeSHi library (optional)
47
     */
48
    private $geshipath = "";
49
50
    /**
51
     * the path to the GeSHi language files (optional)
52
     */
53
    private $geshilanguagespath = "";
54
55
    /**
56
     * @var \DOMDocument
57
     */
58
    private $doc;
59
60
    /**
61
     * @param $path
62
     */
63
    public function setGeshiPath($path)
64
    {
65
        $this->geshipath = $path;
66
    }
67
68
    /**
69
     * @param $path
70
     */
71
    public function setGeshiLanguagesPath($path)
72
    {
73
        $this->geshilanguagespath = $path;
74
    }
75
76
    /**
77
     *
78
     */
79
    public function __construct()
80
    {
81
        parent::__construct();
82
        $this->doc = new \DOMDocument();
83
        $this->doc->encoding = 'UTF-8';
84
        $this->doc->formatOutput = true;
85
        $this->doc->appendChild($this->doc->createElement('snapshot'));
86
    }
87
88
    /**
89
     * @param $outfile
90
     */
91
    public function setOutfile($outfile)
92
    {
93
        $this->outfile = $outfile;
94
    }
95
96
    /**
97
     * Generate a report based on the XML created by this task
98
     */
99
    public function createReport()
100
    {
101
        $transformer = new CoverageReportTransformer();
102
        $this->transformers[] = $transformer;
103
104
        return $transformer;
105
    }
106
107
    /**
108
     * @param string $packageName
109
     * @return \DOMElement|null
110
     */
111
    protected function getPackageElement($packageName): ?\DOMNode
112
    {
113
        $packages = $this->doc->documentElement->getElementsByTagName('package');
114
115
        /** @var \DOMElement $package */
116
        foreach ($packages as $package) {
117
            if ($package->getAttribute('name') === $packageName) {
118
                return $package;
119
            }
120
        }
121
122
        return null;
123
    }
124
125
    /**
126
     * @param $packageName
127
     * @param $element
128
     */
129
    protected function addClassToPackage($packageName, $element)
130
    {
131
        $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 Phing\Task\Ext\Coverage\...sk::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...
132
133
        if ($package === null) {
0 ignored issues
show
introduced by
The condition $package === null is always true.
Loading history...
134
            $package = $this->doc->createElement('package');
135
            $package->setAttribute('name', $packageName);
136
            $this->doc->documentElement->appendChild($package);
137
        }
138
139
        $package->appendChild($element);
140
    }
141
142
    /**
143
     * Adds a subpackage to their package
144
     *
145
     * @param string $packageName The name of the package
146
     * @param string $subpackageName The name of the subpackage
147
     *
148
     * @author Benjamin Schultz <[email protected]>
149
     * @return void
150
     */
151
    protected function addSubpackageToPackage($packageName, $subpackageName)
152
    {
153
        $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 Phing\Task\Ext\Coverage\...sk::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...
154
        $subpackage = $this->getSubpackageElement($subpackageName);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $subpackage is correct as $this->getSubpackageElement($subpackageName) targeting Phing\Task\Ext\Coverage\...:getSubpackageElement() 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...
155
156
        if ($package === null) {
0 ignored issues
show
introduced by
The condition $package === null is always true.
Loading history...
157
            $package = $this->doc->createElement('package');
158
            $package->setAttribute('name', $packageName);
159
            $this->doc->documentElement->appendChild($package);
160
        }
161
162
        if ($subpackage === null) {
0 ignored issues
show
introduced by
The condition $subpackage === null is always true.
Loading history...
163
            $subpackage = $this->doc->createElement('subpackage');
164
            $subpackage->setAttribute('name', $subpackageName);
165
        }
166
167
        $package->appendChild($subpackage);
168
    }
169
170
    /**
171
     * Returns the subpackage element
172
     *
173
     * @param string $subpackageName The name of the subpackage
174
     *
175
     * @author Benjamin Schultz <[email protected]>
176
     * @return \DOMNode|null null when no DOMNode with the given name exists
177
     */
178
    protected function getSubpackageElement($subpackageName)
179
    {
180
        $subpackages = $this->doc->documentElement->getElementsByTagName('subpackage');
181
182
        foreach ($subpackages as $subpackage) {
183
            if ($subpackage->getAttribute('name') == $subpackageName) {
184
                return $subpackage;
185
            }
186
        }
187
188
        return null;
189
    }
190
191
    /**
192
     * Adds a class to their subpackage
193
     *
194
     * @param string $classname The name of the class
195
     * @param \DOMNode $element The dom node to append to the subpackage element
196
     *
197
     * @author Benjamin Schultz <[email protected]>
198
     * @return void
199
     */
200
    protected function addClassToSubpackage($classname, $element)
201
    {
202
        $subpackageName = PHPUnitUtil::getSubpackageName($classname);
203
204
        $subpackage = $this->getSubpackageElement($subpackageName);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $subpackage is correct as $this->getSubpackageElement($subpackageName) targeting Phing\Task\Ext\Coverage\...:getSubpackageElement() 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...
205
206
        if ($subpackage === null) {
0 ignored issues
show
introduced by
The condition $subpackage === null is always true.
Loading history...
207
            $subpackage = $this->doc->createElement('subpackage');
208
            $subpackage->setAttribute('name', $subpackageName);
209
            $this->doc->documentElement->appendChild($subpackage);
210
        }
211
212
        $subpackage->appendChild($element);
213
    }
214
215
    /**
216
     * @param $source
217
     * @return string
218
     */
219
    protected function stripDiv($source)
220
    {
221
        $openpos = strpos($source, "<div");
222
        $closepos = strpos($source, ">", $openpos);
223
224
        $line = substr($source, $closepos + 1);
225
226
        $tagclosepos = strpos($line, "</div>");
227
228
        $line = substr($line, 0, $tagclosepos);
229
230
        return $line;
231
    }
232
233
    /**
234
     * @param $filename
235
     * @return array
236
     */
237
    protected function highlightSourceFile($filename)
238
    {
239
        if ($this->geshipath) {
240
            include_once $this->geshipath . '/geshi.php';
241
242
            $source = file_get_contents($filename);
243
244
            /** @phpstan-ignore-next-line */
245
            $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...
246
            /** @phpstan-ignore-next-line */
247
            $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
0 ignored issues
show
Bug introduced by
The constant Phing\Task\Ext\Coverage\GESHI_NORMAL_LINE_NUMBERS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
248
            $geshi->enable_strict_mode(true);
249
            $geshi->enable_classes(true);
250
            $geshi->set_url_for_keyword_group(3, '');
251
252
            $html = $geshi->parse_code();
253
254
            $lines = preg_split("#</?li>#", $html);
255
256
            // skip first and last line
257
            array_pop($lines);
258
            array_shift($lines);
259
260
            $lines = array_filter($lines);
261
262
            $lines = array_map([$this, 'stripDiv'], $lines);
263
264
            return $lines;
265
        }
266
267
        $lines = file($filename);
268
        $numLines = count($lines);
269
270
        for ($i = 0; $i < $numLines; $i++) {
271
            $line = $lines[$i];
272
273
            $line = rtrim($line);
274
275
            if (function_exists('mb_check_encoding') && mb_check_encoding($line, 'UTF-8')) {
276
                $lines[$i] = $line;
277
            } else {
278
                if (function_exists('mb_convert_encoding')) {
279
                    $lines[$i] = mb_convert_encoding($line, 'UTF-8');
280
                } else {
281
                    $lines[$i] = utf8_encode($line);
282
                }
283
            }
284
        }
285
286
        return $lines;
287
    }
288
289
    /**
290
     * @param $filename
291
     * @param $coverageInformation
292
     * @param int $classStartLine
293
     * @return \DOMElement
294
     */
295
    protected function transformSourceFile($filename, $coverageInformation, $classStartLine = 1)
296
    {
297
        $sourceElement = $this->doc->createElement('sourcefile');
298
        $sourceElement->setAttribute('name', basename($filename));
299
300
        /**
301
         * Add original/full filename to document
302
         */
303
        $sourceElement->setAttribute('sourcefile', $filename);
304
305
        $filelines = $this->highlightSourceFile($filename);
306
307
        $linenr = 1;
308
309
        foreach ($filelines as $line) {
310
            $lineElement = $this->doc->createElement('sourceline');
311
            $lineElement->setAttribute(
312
                'coveredcount',
313
                ($coverageInformation[$linenr] ?? '0')
314
            );
315
316
            if ($linenr == $classStartLine) {
317
                $lineElement->setAttribute('startclass', 1);
318
            }
319
320
            $textnode = $this->doc->createTextNode($line);
321
            $lineElement->appendChild($textnode);
322
323
            $sourceElement->appendChild($lineElement);
324
325
            $linenr++;
326
        }
327
328
        return $sourceElement;
329
    }
330
331
    /**
332
     * Transforms the coverage information
333
     *
334
     * @param string $filename The filename
335
     * @param array $coverageInformation Array with covergae information
336
     *
337
     * @author Michiel Rook <[email protected]>
338
     * @author Benjamin Schultz <[email protected]>
339
     * @return void
340
     */
341
    protected function transformCoverageInformation($filename, $coverageInformation)
342
    {
343
        $classes = PHPUnitUtil::getDefinedClasses($filename, $this->classpath);
344
345
        if (is_array($classes)) {
0 ignored issues
show
introduced by
The condition is_array($classes) is always true.
Loading history...
346
            foreach ($classes as $classname) {
347
                $reflection = new \ReflectionClass($classname);
348
                $methods = $reflection->getMethods();
349
350
                if (method_exists($reflection, 'getShortName')) {
351
                    $className = $reflection->getShortName();
352
                } else {
353
                    $className = $reflection->getName();
354
                }
355
356
                $classElement = $this->doc->createElement('class');
357
                $classElement->setAttribute('name', $className);
358
359
                $packageName = PHPUnitUtil::getPackageName($reflection->getName());
360
                $subpackageName = PHPUnitUtil::getSubpackageName($reflection->getName());
361
362
                if ($subpackageName !== null) {
363
                    $this->addSubpackageToPackage($packageName, $subpackageName);
364
                    $this->addClassToSubpackage($reflection->getName(), $classElement);
365
                } else {
366
                    $this->addClassToPackage($packageName, $classElement);
367
                }
368
369
                $classStartLine = $reflection->getStartLine();
370
371
                $methodscovered = 0;
372
                $methodcount = 0;
373
374
                // Strange PHP5 reflection bug, classes without parent class or implemented interfaces seem to start one line off
375
                if ($reflection->getParentClass() == null && count($reflection->getInterfaces()) == 0) {
376
                    unset($coverageInformation[$classStartLine + 1]);
377
                } else {
378
                    unset($coverageInformation[$classStartLine]);
379
                }
380
381
                // Remove out-of-bounds info
382
                unset($coverageInformation[0]);
383
384
                reset($coverageInformation);
385
386
                foreach ($methods as $method) {
387
                    // PHP5 reflection considers methods of a parent class to be part of a subclass, we don't
388
                    if ($method->getDeclaringClass()->getName() != $reflection->getName()) {
389
                        continue;
390
                    }
391
392
                    // small fix for XDEBUG_CC_UNUSED
393
                    if (isset($coverageInformation[$method->getStartLine()])) {
394
                        unset($coverageInformation[$method->getStartLine()]);
395
                    }
396
397
                    if (isset($coverageInformation[$method->getEndLine()])) {
398
                        unset($coverageInformation[$method->getEndLine()]);
399
                    }
400
401
                    if ($method->isAbstract()) {
402
                        continue;
403
                    }
404
405
                    $linenr = key($coverageInformation);
406
407
                    while ($linenr !== null && $linenr < $method->getStartLine()) {
408
                        next($coverageInformation);
409
                        $linenr = key($coverageInformation);
410
                    }
411
412
                    $methodCoveredCount = 0;
413
                    $methodTotalCount = 0;
414
415
                    $methodHasCoveredLine = false;
416
417
                    while ($linenr !== null && $linenr <= $method->getEndLine()) {
418
                        $methodTotalCount++;
419
                        $methodHasCoveredLine = true;
420
421
                        // set covered when CODE is other than -1 (not executed)
422
                        if ($coverageInformation[$linenr] > 0 || $coverageInformation[$linenr] == -2) {
423
                            $methodCoveredCount++;
424
                        }
425
426
                        next($coverageInformation);
427
                        $linenr = key($coverageInformation);
428
                    }
429
430
                    if (($methodTotalCount == $methodCoveredCount) && $methodHasCoveredLine) {
431
                        $methodscovered++;
432
                    }
433
434
                    $methodcount++;
435
                }
436
437
                $statementcount = count(
438
                    array_filter(
439
                        $coverageInformation,
440
                        function ($var) {
441
                            return ($var != -2);
442
                        }
443
                    )
444
                );
445
446
                $statementscovered = count(
447
                    array_filter(
448
                        $coverageInformation,
449
                        function ($var) {
450
                            return ($var >= 0);
451
                        }
452
                    )
453
                );
454
455
                $classElement->appendChild(
456
                    $this->transformSourceFile($filename, $coverageInformation, $classStartLine)
457
                );
458
459
                $classElement->setAttribute('methodcount', $methodcount);
460
                $classElement->setAttribute('methodscovered', $methodscovered);
461
                $classElement->setAttribute('statementcount', $statementcount);
462
                $classElement->setAttribute('statementscovered', $statementscovered);
463
                $classElement->setAttribute('totalcount', $methodcount + $statementcount);
464
                $classElement->setAttribute('totalcovered', $methodscovered + $statementscovered);
465
            }
466
        }
467
    }
468
469
    protected function calculateStatistics()
470
    {
471
        $packages = $this->doc->documentElement->getElementsByTagName('package');
472
473
        $totalmethodcount = 0;
474
        $totalmethodscovered = 0;
475
476
        $totalstatementcount = 0;
477
        $totalstatementscovered = 0;
478
479
        /** @var \DOMNode $package */
480
        foreach ($packages as $package) {
481
            $methodcount = 0;
482
            $methodscovered = 0;
483
484
            $statementcount = 0;
485
            $statementscovered = 0;
486
487
            $subpackages = $package->getElementsByTagName('subpackage');
488
489
            /** @var \DOMNode $subpackage */
490
            foreach ($subpackages as $subpackage) {
491
                $subpackageMethodCount = 0;
492
                $subpackageMethodsCovered = 0;
493
494
                $subpackageStatementCount = 0;
495
                $subpackageStatementsCovered = 0;
496
497
                $subpackageClasses = $subpackage->getElementsByTagName('class');
498
499
                /** @var \DOMNode $subpackageClass */
500
                foreach ($subpackageClasses as $subpackageClass) {
501
                    $subpackageMethodCount += $subpackageClass->getAttribute('methodcount');
502
                    $subpackageMethodsCovered += $subpackageClass->getAttribute('methodscovered');
503
504
                    $subpackageStatementCount += $subpackageClass->getAttribute('statementcount');
505
                    $subpackageStatementsCovered += $subpackageClass->getAttribute('statementscovered');
506
                }
507
508
                $subpackage->setAttribute('methodcount', $subpackageMethodCount);
509
                $subpackage->setAttribute('methodscovered', $subpackageMethodsCovered);
510
511
                $subpackage->setAttribute('statementcount', $subpackageStatementCount);
512
                $subpackage->setAttribute('statementscovered', $subpackageStatementsCovered);
513
514
                $subpackage->setAttribute('totalcount', $subpackageMethodCount + $subpackageStatementCount);
515
                $subpackage->setAttribute('totalcovered', $subpackageMethodsCovered + $subpackageStatementsCovered);
516
            }
517
518
            $classes = $package->getElementsByTagName('class');
519
520
            /** @var \DOMNode $class */
521
            foreach ($classes as $class) {
522
                $methodcount += $class->getAttribute('methodcount');
523
                $methodscovered += $class->getAttribute('methodscovered');
524
525
                $statementcount += $class->getAttribute('statementcount');
526
                $statementscovered += $class->getAttribute('statementscovered');
527
            }
528
529
            $package->setAttribute('methodcount', $methodcount);
530
            $package->setAttribute('methodscovered', $methodscovered);
531
532
            $package->setAttribute('statementcount', $statementcount);
533
            $package->setAttribute('statementscovered', $statementscovered);
534
535
            $package->setAttribute('totalcount', $methodcount + $statementcount);
536
            $package->setAttribute('totalcovered', $methodscovered + $statementscovered);
537
538
            $totalmethodcount += $methodcount;
539
            $totalmethodscovered += $methodscovered;
540
541
            $totalstatementcount += $statementcount;
542
            $totalstatementscovered += $statementscovered;
543
        }
544
545
        $this->doc->documentElement->setAttribute('methodcount', $totalmethodcount);
546
        $this->doc->documentElement->setAttribute('methodscovered', $totalmethodscovered);
547
548
        $this->doc->documentElement->setAttribute('statementcount', $totalstatementcount);
549
        $this->doc->documentElement->setAttribute('statementscovered', $totalstatementscovered);
550
551
        $this->doc->documentElement->setAttribute('totalcount', $totalmethodcount + $totalstatementcount);
552
        $this->doc->documentElement->setAttribute('totalcovered', $totalmethodscovered + $totalstatementscovered);
553
    }
554
555
    public function main()
556
    {
557
        $coverageDatabase = $this->project->getProperty('coverage.database');
558
559
        if (!$coverageDatabase) {
560
            throw new BuildException("Property coverage.database is not set - please include coverage-setup in your build file");
561
        }
562
563
        $database = new File($coverageDatabase);
564
565
        $this->log("Transforming coverage report");
566
567
        $props = new Properties();
568
        $props->load($database);
569
570
        foreach ($props->keys() as $filename) {
571
            $file = unserialize($props->getProperty($filename));
0 ignored issues
show
Bug introduced by
It seems like $props->getProperty($filename) can also be of type null; however, parameter $data of unserialize() does only seem to accept string, 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

571
            $file = unserialize(/** @scrutinizer ignore-type */ $props->getProperty($filename));
Loading history...
572
573
            $this->transformCoverageInformation($file['fullname'], $file['coverage']);
574
        }
575
576
        $this->calculateStatistics();
577
578
        $this->doc->save($this->outfile);
579
580
        foreach ($this->transformers as $transformer) {
581
            $transformer->setXmlDocument($this->doc);
582
            $transformer->transform();
583
        }
584
    }
585
}
586