File::abbreviateClassName()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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