Passed
Pull Request — master (#1)
by Guillaume
04:03
created

File   F

Complexity

Total Complexity 109

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 321
dl 0
loc 500
rs 2
c 0
b 0
f 0
wmc 109

How to fix   Complexity   

Complex Class

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

1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of phpunit/php-code-coverage.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace SebastianBergmann\CodeCoverage\Report\Html;
11
12
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
13
use SebastianBergmann\CodeCoverage\Percentage;
14
use SebastianBergmann\Template\Template;
15
16
/**
17
 * Renders a file node.
18
 */
19
final class File extends Renderer
20
{
21
    /**
22
     * @var int
23
     */
24
    private $htmlSpecialCharsFlags = \ENT_COMPAT | \ENT_HTML401 | \ENT_SUBSTITUTE;
25
26
    /**
27
     * @throws \RuntimeException
28
     */
29
    public function render(FileNode $node, string $file): void
30
    {
31
        $template = new Template($this->templatePath . 'file.html', '{{', '}}');
32
33
        $template->setVar(
34
            [
35
                'items' => $this->renderItems($node),
36
                'lines' => $this->renderSource($node),
37
            ]
38
        );
39
40
        $this->setCommonTemplateVariables($template, $node);
41
42
        $template->renderTo($file);
43
    }
44
45
    protected function renderItems(FileNode $node): string
46
    {
47
        $template = new Template($this->templatePath . 'file_item.html', '{{', '}}');
48
49
        $methodItemTemplate = new Template(
50
            $this->templatePath . 'method_item.html',
51
            '{{',
52
            '}}'
53
        );
54
55
        $items = $this->renderItemTemplate(
56
            $template,
57
            [
58
                'name'                         => 'Total',
59
                'numClasses'                   => $node->getNumClassesAndTraits(),
60
                'numTestedClasses'             => $node->getNumTestedClassesAndTraits(),
61
                'numMethods'                   => $node->getNumFunctionsAndMethods(),
62
                'numTestedMethods'             => $node->getNumTestedFunctionsAndMethods(),
63
                'linesExecutedPercent'         => $node->getLineExecutedPercent(false),
64
                'linesExecutedPercentAsString' => $node->getLineExecutedPercent(),
65
                'numExecutedLines'             => $node->getNumExecutedLines(),
66
                'numExecutableLines'           => $node->getNumExecutableLines(),
67
                'testedMethodsPercent'         => $node->getTestedFunctionsAndMethodsPercent(false),
68
                'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(),
69
                'testedClassesPercent'         => $node->getTestedClassesAndTraitsPercent(false),
70
                'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(),
71
                'crap'                         => '<abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr>',
72
            ]
73
        );
74
75
        $items .= $this->renderFunctionItems(
76
            $node->getFunctions(),
77
            $methodItemTemplate
78
        );
79
80
        $items .= $this->renderTraitOrClassItems(
81
            $node->getTraits(),
82
            $template,
83
            $methodItemTemplate
84
        );
85
86
        $items .= $this->renderTraitOrClassItems(
87
            $node->getClasses(),
88
            $template,
89
            $methodItemTemplate
90
        );
91
92
        return $items;
93
    }
94
95
    protected function renderTraitOrClassItems(array $items, Template $template, Template $methodItemTemplate): string
96
    {
97
        $buffer = '';
98
99
        if (empty($items)) {
100
            return $buffer;
101
        }
102
103
        foreach ($items as $name => $item) {
104
            $numMethods       = 0;
105
            $numTestedMethods = 0;
106
107
            foreach ($item['methods'] as $method) {
108
                if ($method['executableLines'] > 0) {
109
                    $numMethods++;
110
111
                    if ($method['executedLines'] === $method['executableLines']) {
112
                        $numTestedMethods++;
113
                    }
114
                }
115
            }
116
117
            if ($item['executableLines'] > 0) {
118
                $numClasses                   = 1;
119
                $numTestedClasses             = $numTestedMethods == $numMethods ? 1 : 0;
120
                $linesExecutedPercentAsString = Percentage::fromFractionAndTotal(
121
                    $item['executedLines'],
122
                    $item['executableLines']
123
                )->asString();
124
            } else {
125
                $numClasses                   = 'n/a';
126
                $numTestedClasses             = 'n/a';
127
                $linesExecutedPercentAsString = 'n/a';
128
            }
129
130
            $testedMethodsPercentage = Percentage::fromFractionAndTotal(
131
                $numTestedMethods,
132
                $numMethods
133
            );
134
135
            $testedClassesPercentage = Percentage::fromFractionAndTotal(
136
                $numTestedMethods === $numMethods ? 1 : 0,
137
                1
138
            );
139
140
            $buffer .= $this->renderItemTemplate(
141
                $template,
142
                [
143
                    'name'                         => $this->abbreviateClassName($name),
144
                    'numClasses'                   => $numClasses,
145
                    'numTestedClasses'             => $numTestedClasses,
146
                    'numMethods'                   => $numMethods,
147
                    'numTestedMethods'             => $numTestedMethods,
148
                    'linesExecutedPercent'         => Percentage::fromFractionAndTotal(
149
                        $item['executedLines'],
150
                        $item['executableLines'],
151
                    )->asFloat(),
152
                    'linesExecutedPercentAsString' => $linesExecutedPercentAsString,
153
                    'numExecutedLines'             => $item['executedLines'],
154
                    'numExecutableLines'           => $item['executableLines'],
155
                    'testedMethodsPercent'         => $testedMethodsPercentage->asFloat(),
156
                    'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),
157
                    'testedClassesPercent'         => $testedClassesPercentage->asFloat(),
158
                    'testedClassesPercentAsString' => $testedClassesPercentage->asString(),
159
                    'crap'                         => $item['crap'],
160
                ]
161
            );
162
163
            foreach ($item['methods'] as $method) {
164
                $buffer .= $this->renderFunctionOrMethodItem(
165
                    $methodItemTemplate,
166
                    $method,
167
                    '&nbsp;'
168
                );
169
            }
170
        }
171
172
        return $buffer;
173
    }
174
175
    protected function renderFunctionItems(array $functions, Template $template): string
176
    {
177
        if (empty($functions)) {
178
            return '';
179
        }
180
181
        $buffer = '';
182
183
        foreach ($functions as $function) {
184
            $buffer .= $this->renderFunctionOrMethodItem(
185
                $template,
186
                $function
187
            );
188
        }
189
190
        return $buffer;
191
    }
192
193
    protected function renderFunctionOrMethodItem(Template $template, array $item, string $indent = ''): string
194
    {
195
        $numMethods       = 0;
196
        $numTestedMethods = 0;
197
198
        if ($item['executableLines'] > 0) {
199
            $numMethods = 1;
200
201
            if ($item['executedLines'] === $item['executableLines']) {
202
                $numTestedMethods = 1;
203
            }
204
        }
205
206
        $executedLinesPercentage = Percentage::fromFractionAndTotal(
207
            $item['executedLines'],
208
            $item['executableLines']
209
        );
210
211
        $testedMethodsPercentage = Percentage::fromFractionAndTotal(
212
            $numTestedMethods,
213
            1
214
        );
215
216
        return $this->renderItemTemplate(
217
            $template,
218
            [
219
                'name'                         => \sprintf(
220
                    '%s<a href="#%d"><abbr title="%s">%s</abbr></a>',
221
                    $indent,
222
                    $item['startLine'],
223
                    \htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),
224
                    $item['functionName'] ?? $item['methodName']
225
                ),
226
                'numMethods'                   => $numMethods,
227
                'numTestedMethods'             => $numTestedMethods,
228
                'linesExecutedPercent'         => $executedLinesPercentage->asFloat(),
229
                'linesExecutedPercentAsString' => $executedLinesPercentage->asString(),
230
                'numExecutedLines'             => $item['executedLines'],
231
                'numExecutableLines'           => $item['executableLines'],
232
                'testedMethodsPercent'         => $testedMethodsPercentage->asFloat(),
233
                'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),
234
                'crap'                         => $item['crap'],
235
            ]
236
        );
237
    }
238
239
    protected function renderSource(FileNode $node): string
240
    {
241
        $coverageData = $node->getCoverageData();
242
        $testData     = $node->getTestData();
243
        $codeLines    = $this->loadFile($node->getPath());
244
        $lines        = '';
245
        $i            = 1;
246
247
        foreach ($codeLines as $line) {
248
            $trClass        = '';
249
            $popoverContent = '';
250
            $popoverTitle   = '';
251
252
            if (\array_key_exists($i, $coverageData)) {
253
                $numTests = ($coverageData[$i] ? \count($coverageData[$i]) : 0);
254
255
                if ($coverageData[$i] === null) {
256
                    $trClass = ' class="warning"';
257
                } elseif ($numTests == 0) {
258
                    $trClass = ' class="danger"';
259
                } else {
260
                    $lineCss        = 'covered-by-large-tests';
261
                    $popoverContent = '<ul>';
262
263
                    if ($numTests > 1) {
264
                        $popoverTitle = $numTests . ' tests cover line ' . $i;
265
                    } else {
266
                        $popoverTitle = '1 test covers line ' . $i;
267
                    }
268
269
                    foreach ($coverageData[$i] as $test) {
270
                        if ($lineCss == 'covered-by-large-tests' && $testData[$test]['size'] == 'medium') {
271
                            $lineCss = 'covered-by-medium-tests';
272
                        } elseif ($testData[$test]['size'] == 'small') {
273
                            $lineCss = 'covered-by-small-tests';
274
                        }
275
276
                        switch ($testData[$test]['status']) {
277
                            case 0:
278
                                switch ($testData[$test]['size']) {
279
                                    case 'small':
280
                                        $testCSS = ' class="covered-by-small-tests"';
281
282
                                        break;
283
284
                                    case 'medium':
285
                                        $testCSS = ' class="covered-by-medium-tests"';
286
287
                                        break;
288
289
                                    default:
290
                                        $testCSS = ' class="covered-by-large-tests"';
291
292
                                        break;
293
                                }
294
295
                                break;
296
297
                            case 1:
298
                            case 2:
299
                                $testCSS = ' class="warning"';
300
301
                                break;
302
303
                            case 3:
304
                                $testCSS = ' class="danger"';
305
306
                                break;
307
308
                            case 4:
309
                                $testCSS = ' class="danger"';
310
311
                                break;
312
313
                            default:
314
                                $testCSS = '';
315
                        }
316
317
                        $popoverContent .= \sprintf(
318
                            '<li%s>%s</li>',
319
                            $testCSS,
320
                            \htmlspecialchars($test, $this->htmlSpecialCharsFlags)
321
                        );
322
                    }
323
324
                    $popoverContent .= '</ul>';
325
                    $trClass         = ' class="' . $lineCss . ' popin"';
326
                }
327
            }
328
329
            $popover = '';
330
331
            if (!empty($popoverTitle)) {
332
                $popover = \sprintf(
333
                    ' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
334
                    $popoverTitle,
335
                    \htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
336
                );
337
            }
338
339
            $lines .= \sprintf(
340
                '     <tr%s><td%s><div align="right"><a name="%d"></a><a href="#%d">%d</a></div></td><td class="codeLine">%s</td></tr>' . "\n",
341
                $trClass,
342
                $popover,
343
                $i,
344
                $i,
345
                $i,
346
                $line
347
            );
348
349
            $i++;
350
        }
351
352
        return $lines;
353
    }
354
355
    /**
356
     * @param string $file
357
     */
358
    protected function loadFile($file): array
359
    {
360
        $buffer              = \file_get_contents($file);
361
        $tokens              = \token_get_all($buffer);
362
        $result              = [''];
363
        $i                   = 0;
364
        $stringFlag          = false;
365
        $fileEndsWithNewLine = \substr($buffer, -1) == "\n";
366
367
        unset($buffer);
368
369
        foreach ($tokens as $j => $token) {
370
            if (\is_string($token)) {
371
                if ($token === '"' && $tokens[$j - 1] !== '\\') {
372
                    $result[$i] .= \sprintf(
373
                        '<span class="string">%s</span>',
374
                        \htmlspecialchars($token, $this->htmlSpecialCharsFlags)
375
                    );
376
377
                    $stringFlag = !$stringFlag;
378
                } else {
379
                    $result[$i] .= \sprintf(
380
                        '<span class="keyword">%s</span>',
381
                        \htmlspecialchars($token, $this->htmlSpecialCharsFlags)
382
                    );
383
                }
384
385
                continue;
386
            }
387
388
            [$token, $value] = $token;
389
390
            $value = \str_replace(
391
                ["\t", ' '],
392
                ['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'],
393
                \htmlspecialchars($value, $this->htmlSpecialCharsFlags)
394
            );
395
396
            if ($value === "\n") {
397
                $result[++$i] = '';
398
            } else {
399
                $lines = \explode("\n", $value);
400
401
                foreach ($lines as $jj => $line) {
402
                    $line = \trim($line);
403
404
                    if ($line !== '') {
405
                        if ($stringFlag) {
406
                            $colour = 'string';
407
                        } else {
408
                            switch ($token) {
409
                                case \T_INLINE_HTML:
410
                                    $colour = 'html';
411
412
                                    break;
413
414
                                case \T_COMMENT:
415
                                case \T_DOC_COMMENT:
416
                                    $colour = 'comment';
417
418
                                    break;
419
420
                                case \T_ABSTRACT:
421
                                case \T_ARRAY:
422
                                case \T_AS:
423
                                case \T_BREAK:
424
                                case \T_CALLABLE:
425
                                case \T_CASE:
426
                                case \T_CATCH:
427
                                case \T_CLASS:
428
                                case \T_CLONE:
429
                                case \T_CONTINUE:
430
                                case \T_DEFAULT:
431
                                case \T_ECHO:
432
                                case \T_ELSE:
433
                                case \T_ELSEIF:
434
                                case \T_EMPTY:
435
                                case \T_ENDDECLARE:
436
                                case \T_ENDFOR:
437
                                case \T_ENDFOREACH:
438
                                case \T_ENDIF:
439
                                case \T_ENDSWITCH:
440
                                case \T_ENDWHILE:
441
                                case \T_EXIT:
442
                                case \T_EXTENDS:
443
                                case \T_FINAL:
444
                                case \T_FINALLY:
445
                                case \T_FOREACH:
446
                                case \T_FUNCTION:
447
                                case \T_GLOBAL:
448
                                case \T_IF:
449
                                case \T_IMPLEMENTS:
450
                                case \T_INCLUDE:
451
                                case \T_INCLUDE_ONCE:
452
                                case \T_INSTANCEOF:
453
                                case \T_INSTEADOF:
454
                                case \T_INTERFACE:
455
                                case \T_ISSET:
456
                                case \T_LOGICAL_AND:
457
                                case \T_LOGICAL_OR:
458
                                case \T_LOGICAL_XOR:
459
                                case \T_NAMESPACE:
460
                                case \T_NEW:
461
                                case \T_PRIVATE:
462
                                case \T_PROTECTED:
463
                                case \T_PUBLIC:
464
                                case \T_REQUIRE:
465
                                case \T_REQUIRE_ONCE:
466
                                case \T_RETURN:
467
                                case \T_STATIC:
468
                                case \T_THROW:
469
                                case \T_TRAIT:
470
                                case \T_TRY:
471
                                case \T_UNSET:
472
                                case \T_USE:
473
                                case \T_VAR:
474
                                case \T_WHILE:
475
                                case \T_YIELD:
476
                                    $colour = 'keyword';
477
478
                                    break;
479
480
                                default:
481
                                    $colour = 'default';
482
                            }
483
                        }
484
485
                        $result[$i] .= \sprintf(
486
                            '<span class="%s">%s</span>',
487
                            $colour,
488
                            $line
489
                        );
490
                    }
491
492
                    if (isset($lines[$jj + 1])) {
493
                        $result[++$i] = '';
494
                    }
495
                }
496
            }
497
        }
498
499
        if ($fileEndsWithNewLine) {
500
            unset($result[\count($result) - 1]);
501
        }
502
503
        return $result;
504
    }
505
506
    private function abbreviateClassName(string $className): string
507
    {
508
        $tmp = \explode('\\', $className);
509
510
        if (\count($tmp) > 1) {
511
            $className = \sprintf(
512
                '<abbr title="%s">%s</abbr>',
513
                $className,
514
                \array_pop($tmp)
515
            );
516
        }
517
518
        return $className;
519
    }
520
}
521