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