Passed
Push — feature/issue-718-handle-anony... ( f963d6...3e5eee )
by Kyle
01:51
created

HTMLRenderer::sumUpViolations()   B

Complexity

Conditions 8
Paths 50

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 50
nop 1
dl 0
loc 40
rs 8.0355
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of PHP Mess Detector.
4
 *
5
 * Copyright (c) Manuel Pichler <[email protected]>.
6
 * All rights reserved.
7
 *
8
 * Licensed under BSD License
9
 * For full copyright and license information, please see the LICENSE file.
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @author Manuel Pichler <[email protected]>
13
 * @copyright Manuel Pichler. All rights reserved.
14
 * @license https://opensource.org/licenses/bsd-license.php BSD License
15
 * @link http://phpmd.org/
16
 */
17
18
namespace PHPMD\Renderer;
19
20
use PHPMD\AbstractRenderer;
21
use PHPMD\Report;
22
23
/**
24
 * This renderer output a html file with all found violations.
25
 *
26
 * @author Premysl Karbula <[email protected]>
27
 * @copyright 2017 Premysl Karbula. All rights reserved.
28
 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
29
 */
30
class HTMLRenderer extends AbstractRenderer
31
{
32
    const CATEGORY_PRIORITY = 'category_priority';
33
34
    const CATEGORY_NAMESPACE = 'category_namespace';
35
36
    const CATEGORY_RULESET = 'category_ruleset';
37
38
    const CATEGORY_RULE = 'category_rule';
39
40
    protected static $priorityTitles = array(
41
        1 => 'Top (1)',
42
        2 => 'High (2)',
43
        3 => 'Moderate (3)',
44
        4 => 'Low (4)',
45
        5 => 'Lowest (5)',
46
    );
47
48
    // Used in self::colorize() method.
49
    protected static $descHighlightRules = array(
50
        'method' => array( // Method names.
51
            'regex' => 'method\s+(((["\']).*["\'])|(\S+))',
52
            'css-class' => 'hlt-method',
53
        ),
54
        'quoted' => array( // Quoted strings.
55
            'regex' => '(["\'][^\'"]+["\'])',
56
            'css-class' => 'hlt-quoted',
57
        ),
58
        'variable' => array( // Variables.
59
            'regex' => '(\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)',
60
            'css-class' => 'hlt-variable',
61
        ),
62
    );
63
64
    protected static $compiledHighlightRegex = null;
65
66
    /**
67
     * This method will be called on all renderers before the engine starts the
68
     * real report processing.
69
     *
70
     * @return void
71
     */
72
    public function start()
73
    {
74
        $writer = $this->getWriter();
75
76
        $mainColor = "#2f838a";
77
78
        // Avoid inlining styles.
79
        $style = "
80
            <script>
81
                function toggle(id) {
82
                    var item = document.getElementById(id);
83
                    item.classList.toggle('hidden');
84
                }
85
            </script>
86
            <style>
87
88
                @media (min-width: 1366px) {
89
                    body { max-width: 80%; margin: auto; }
90
                }
91
92
                body {
93
                    font-family: sans-serif;
94
                }
95
96
                a {
97
                    color: $mainColor;
98
                }
99
100
                a:hover {
101
                    color: #333;
102
                }
103
104
                em {
105
                    font-weight: bold;
106
                    font-style: italic;
107
                }
108
109
                h1 {
110
                    padding: 0.5ex 0.2ex;
111
                    border-bottom: 2px solid #333;
112
                }
113
114
                table {
115
                    width: 100%;
116
                    border-spacing: 0;
117
                }
118
119
                table tr > th {
120
                    text-align: left;
121
                }
122
123
                table caption {
124
                    font-weight: bold;
125
                    padding: 1ex 0.5ex;
126
                    text-align: left;
127
                    font-size: 120%;
128
                    border-bottom: 2px solid #333;
129
                }
130
131
                tbody tr:nth-child(odd) {
132
                    background: rgba(0, 0, 0, 0.08);
133
                }
134
135
                tbody tr:hover {
136
                    background: #ffee99;
137
                }
138
139
                thead th {
140
                    border-bottom: 1px solid #aaa;
141
                }
142
143
                table td, table th {
144
                    padding: 0.5ex;
145
                }
146
147
                /* Table 'count' and 'percentage' column */
148
                .t-cnt, .t-pct {
149
                    width: 5em;
150
                }
151
152
                .t-pct {
153
                    opacity: 0.8;
154
                    font-style: italic;
155
                    font-size: 80%;
156
                }
157
158
                /* Table bar chart */
159
                .t-bar {
160
                    height: 0.5ex;
161
                    margin-top: 0.5ex;
162
                    background-color: $mainColor; /* rgba(47, 131, 138, 0.2); */
163
                }
164
165
                section, table {
166
                    margin-bottom: 2em;
167
                }
168
169
                #details-link.hidden {
170
                    display: none;
171
                }
172
173
                #details-wrapper.hidden {
174
                    display: none;
175
                }
176
177
                ul.code {
178
                    margin: 0;
179
                    padding: 0;
180
                }
181
182
                ul.code.hidden {
183
                    display: none;
184
                }
185
186
                ul.code li {
187
                    display: flex;
188
                    line-height: 1.4em;
189
                    font-family: monospace;
190
                    white-space: nowrap;
191
                }
192
193
                ul.code li:nth-child(odd) {
194
                    background-color: rgba(47, 131, 138, 0.1)
195
                }
196
197
                /* Excerpt: Line number */
198
                ul.code .no {
199
                    width: 5%;
200
                    min-width: 5em;
201
                    text-align: right;
202
                    border-right: 1px solid rgba(47, 131, 138, 0.6);
203
                    padding-right: 1ex;
204
                    box-sizing: border-box;
205
                }
206
207
                /* Excerpt: Code */
208
                ul.code .cd {
209
                    padding-left: 1ex;
210
                    white-space: pre-wrap;
211
                    box-sizing: border-box;
212
                    word-wrap: break-word;
213
                    overflow: hidden;
214
                }
215
216
                .hlt {
217
                    background: #ffee99 !important
218
                }
219
220
                .prio {
221
                    color: #333;
222
                    float: right;
223
                }
224
225
                .indx {
226
                    padding: 0.5ex 1ex;
227
                    background-color: #000;
228
                    color: #fff;
229
                    text-decoration: none;
230
                }
231
232
                .indx:hover {
233
                    background-color: $mainColor;
234
                    color: #fff;
235
                }
236
237
                /* Problem container */
238
                .prb h3 {
239
                    padding: 1ex 0.5ex;
240
                    border-bottom: 2px solid #000;
241
                    font-size: 95%;
242
                    margin: 0;
243
                }
244
245
                .info-lnk {
246
                    font-style: italic !important;
247
                    font-weight: normal !important;
248
                    text-decoration: none;
249
                }
250
251
                .info-lnk.blck {
252
                    padding: 0.5ex 1ex;
253
                    background-color: rgba(47, 131, 138, 0.2);
254
                }
255
256
                .path-basename {
257
                    font-weight: bold;
258
                }
259
260
                .hlt-info {
261
                    display: inline-block;
262
                    padding: 2px 4px;
263
                    font-style: italic;
264
                }
265
266
                    .hlt-info.quoted {
267
                        background-color: #92de71;
268
                    }
269
270
                    .hlt-info.variable {
271
                        background-color: #a3d2ff;
272
                    }
273
274
                    .hlt-info.method {
275
                        background-color: #f7c0ff;
276
                    }
277
278
                .sub-info {
279
                    padding: 1ex 0.5ex;
280
                }
281
282
                /* Handle printer friendly styles */
283
                @media print {
284
                    body, th { font-size: 10pt; }
285
                    .hlt-info { padding: 0; background: none; }
286
                    section, table { margin-bottom: 1em; }
287
                    h1, h2, h3, table caption { padding: 0.5ex 0.2ex; }
288
                    .prb h3 { border-bottom: 0.5px solid #aaa; }
289
                    .t-bar { display: none; }
290
                    .info-lnk { display: none; }
291
                    #details-wrapper { display: block !important; font-size: 90% !important; }
292
                }
293
294
            </style>";
295
296
        $style = self::reduceWhitespace($style);
297
        $writer->write("<html><head>{$style}<title>PHPMD Report</title></head><body>" . PHP_EOL);
298
299
        $header = sprintf("
300
            <header>
301
                <h1>PHPMD Report</h1>
302
                Generated at <em>%s</em>
303
                with <a href='%s' target='_blank'>PHP Mess Detector</a>
304
                on <em>PHP %s</em>
305
                on <em>%s</em>
306
            </header>
307
        ", date('Y-m-d H:i'), "https://phpmd.org", \PHP_VERSION, gethostname());
308
309
        $writer->write($header);
310
    }
311
312
    /**
313
     * This method will be called when the engine has finished the source analysis
314
     * phase.
315
     *
316
     * @param \PHPMD\Report $report
317
     * @return void
318
     */
319
    public function renderReport(Report $report)
320
    {
321
        $w = $this->getWriter();
322
323
        $index = 0;
324
        $violations = $report->getRuleViolations();
325
326
        $count = count($violations);
327
        $w->write(sprintf('<h3>%d problems found</h3>', $count));
328
329
        // If no problems were found, don't bother with rendering anything else.
330
        if (!$count) {
331
            return;
332
        }
333
334
        // Render summary tables.
335
        $w->write("<h2>Summary</h2>");
336
        $categorized = self::sumUpViolations($violations);
337
        $this->writeTable('By priority', 'Priority', $categorized[self::CATEGORY_PRIORITY]);
338
        $this->writeTable('By namespace', 'PHP Namespace', $categorized[self::CATEGORY_NAMESPACE]);
339
        $this->writeTable('By rule set', 'Rule set', $categorized[self::CATEGORY_RULESET]);
340
        $this->writeTable('By name', 'Rule name', $categorized[self::CATEGORY_RULE]);
341
342
        // Render details of each violation and place the "Details" display toggle.
343
        $w->write("<h2 style='page-break-before: always'>Details</h2>");
344
        $w->write("
345
            <a
346
                id='details-link'
347
                class='info-lnk blck'
348
                href='#'
349
                onclick='toggle(\"details-link\"); toggle(\"details-wrapper\"); return false;'
350
            >
351
            Show details &#x25BC;
352
        </a>");
353
        $w->write("<div id='details-wrapper' class='hidden'>");
354
355
        foreach ($violations as $violation) {
356
            // This is going to be used as ID in HTML (deep anchoring).
357
            $htmlId = "p-" . $index++;
358
359
            // Get excerpt of the code from validated file.
360
            $excerptHtml = null;
361
            $excerpt = self::getLineExcerpt(
362
                $violation->getFileName(),
363
                $violation->getBeginLine(),
364
                2
365
            );
366
367
            foreach ($excerpt as $line => $code) {
368
                $class = $line === $violation->getBeginLine() ? " class='hlt'" : null;
369
                $codeHtml = htmlspecialchars($code);
370
                $excerptHtml .= "<li{$class}><div class='no'>{$line}</div><div class='cd'>{$codeHtml}</div></li>";
371
            }
372
373
            $descHtml = self::colorize(htmlentities($violation->getDescription()));
374
            $filePath = $violation->getFileName();
375
            $fileHtml = "<a href='file://$filePath' target='_blank'>" . self::highlightFile($filePath) . "</a>";
376
377
            // Create an external link to rule's help, if there's any provided.
378
            $linkHtml = null;
379
            if ($url = $violation->getRule()->getExternalInfoUrl()) {
380
                $linkHtml = "<a class='info-lnk' href='{$url}' target='_blank'>(help)</a>";
381
            }
382
383
            // HTML snippet handling the toggle to display the file's code.
384
            $showCodeAnchor = "
385
                <a class='info-lnk blck' href='#' onclick='toggle(\"$htmlId-code\"); return false;'>
386
                    Show code &#x25BC;
387
                </a>";
388
389
            $prio = self::$priorityTitles[$violation->getRule()->getPriority()];
390
            $html = "
391
                <section class='prb' id='$htmlId'>
392
                    <header>
393
                        <h3>
394
                            <a href='#$htmlId' class='indx'>#{$index}</a>
395
                            {$descHtml} {$linkHtml} <span class='prio'>{$prio}</span>
396
                        </h3>
397
                    </header>
398
                    <div class='sub-info'><b>File:</b> {$fileHtml} {$showCodeAnchor}</div>
399
                    <ul class='code hidden' id='$htmlId-code'>%s</ul>
400
                </section>";
401
402
            // Remove unnecessary tab/space characters at the line beginnings.
403
            $html = self::reduceWhitespace($html);
404
            $w->write(sprintf($html, $excerptHtml));
405
        }
406
    }
407
408
    /**
409
     * This method will be called the engine has finished the report processing
410
     * for all registered renderers.
411
     *
412
     * @return void
413
     */
414
    public function end()
415
    {
416
        $writer = $this->getWriter();
417
        $writer->write('</div></body></html>');
418
    }
419
420
    /**
421
     * Return array of lines from a specified file:line, optionally with extra lines around
422
     * for additional cognitive context.
423
     *
424
     * @return array
425
     */
426
    protected static function getLineExcerpt($file, $lineNumber, $extra = 0)
427
    {
428
        if (!is_readable($file)) {
429
            return array();
430
        }
431
432
        $file = new \SplFileObject($file);
433
434
        // We have to subtract 1 to extract correct lines via SplFileObject.
435
        $line = max($lineNumber - 1 - $extra, 0);
436
437
        $result = array();
438
439
        if (!$file->eof()) {
440
            $file->seek($line);
441
            for ($i = 0; $i <= ($extra * 2); $i++) {
442
                $result[++$line] = trim((string)$file->current(), "\n");
443
                $file->next();
444
            }
445
        }
446
447
        return $result;
448
    }
449
450
    /**
451
     * Take a rule description text and try to decorate/stylize parts of it with HTML.
452
     * Based on self::$descHighlightRules config.
453
     *
454
     * @return string
455
     */
456
    protected static function colorize($message)
457
    {
458
        // Compile final regex, if not done already.
459
        if (!self::$compiledHighlightRegex) {
460
            $prepared = self::$descHighlightRules;
461
            array_walk($prepared, function (&$v, $k) {
462
                $v = "(?<{$k}>{$v['regex']})";
463
            });
464
465
            self::$compiledHighlightRegex = "#(" . implode('|', $prepared) . ")#";
466
467
        }
468
469
        $rules = self::$descHighlightRules;
470
471
        return preg_replace_callback(self::$compiledHighlightRegex, function ($x) use ($rules) {
472
            // Extract currently matched specification of highlighting (Match groups
473
            // are named and we can find out which is not empty.).
474
            $definition = array_keys(array_intersect_key($rules, array_filter($x)));
475
            $definition = reset($definition);
476
477
            return "<span class='hlt-info {$definition}'>{$x[0]}</span>";
478
        }, $message);
479
    }
480
481
    /**
482
     * Take a file path and return a bit of HTML where the basename is wrapped in styled <span>.
483
     *
484
     * @return string
485
     */
486
    protected static function highlightFile($path)
487
    {
488
        $file = substr(strrchr($path, "/"), 1);
489
        $dir = str_replace($file, null, $path);
490
491
        return $dir . "<span class='path-basename'>" . $file . '</span>';
492
    }
493
494
    /**
495
     * Render a pretty informational table and send the HTML to the writer.
496
     *
497
     * @return void
498
     */
499
    protected function writeTable($title, $itemsTitle, $items)
500
    {
501
        if (!$items) {
502
            return;
503
        }
504
505
        $writer = $this->getWriter();
506
        $rows = null;
507
508
        // We will need to calculate percentages and whatnot.
509
        $max = max($items);
510
        $sum = array_sum($items);
511
512
        foreach ($items as $name => $count) {
513
            // Calculate chart/bar's percentage width relative to the highest occuring item.
514
            $width = $max !== 0 ? $count / $max * 100 : 0; // Avoid division by zero.
515
516
            $bar = sprintf(
517
                '<div class="t-bar" style="width: %d%%; opacity: %.2f"></div>',
518
                $width,
519
                min(0.2 + $width / 100, 1) // Minimum opacity for the bar is 0.2.
520
            );
521
522
            $pct = $sum !== 0 ? sprintf('%.1f', $count / $sum * 100) : '-'; // Avoid division by zero.
523
            $rows .= "<tr>
524
                <td class='t-cnt'>$count</td>
525
                <td class='t-pct'>$pct %</td>
526
                <th class='t-n'>{$name}{$bar}</th>
527
            </tr>";
528
529
        }
530
531
        $header = "<thead><tr><th>Count</th><th>%</th><th>$itemsTitle</th></tr></thead>";
532
        $html = "<section><table><caption>$title</caption>{$header}{$rows}</table></section>";
533
        $writer->write(self::reduceWhitespace($html));
534
    }
535
536
    /**
537
     * Go through passed violations and count occurrences based on pre-specified conditions.
538
     *
539
     * @return array
540
     */
541
    protected static function sumUpViolations($violations)
542
    {
543
        $result = array(
544
            self::CATEGORY_PRIORITY => array(),
545
            self::CATEGORY_NAMESPACE => array(),
546
            self::CATEGORY_RULESET => array(),
547
            self::CATEGORY_RULE => array(),
548
        );
549
550
        foreach ($violations as $v) {
551
            // We use "ref" reference to make things somewhat easier to read.
552
            // Also, using a reference to non-existing array index doesn't throw a notice.
553
554
            if ($ns = $v->getNamespaceName()) {
555
                $ref = &$result[self::CATEGORY_NAMESPACE][$ns];
556
                $ref = isset($ref) ? $ref + 1 : 1;
557
            }
558
559
            $rule = $v->getRule();
560
561
            // Friendly priority -> Add a describing word to "just number".
562
            $friendlyPriority = self::$priorityTitles[$rule->getPriority()];
563
            $ref = &$result[self::CATEGORY_PRIORITY][$friendlyPriority];
564
            $ref = isset($ref) ? $ref + 1 : 1;
565
566
            $ref = &$result[self::CATEGORY_RULESET][$rule->getRuleSetName()];
567
            $ref = isset($ref) ? $ref + 1 : 1;
568
569
            $ref = &$result[self::CATEGORY_RULE][$rule->getName()];
570
            $ref = isset($ref) ? $ref + 1 : 1;
571
572
        }
573
574
        // Sort numbers in each category from high to low.
575
        foreach ($result as &$inner) {
576
            arsort($inner);
577
        }
578
579
        return $result;
580
    }
581
582
    /**
583
     * Reduces two and more whitespaces in a row to a single whitespace to conserve space.
584
     *
585
     * @return string
586
     */
587
    protected static function reduceWhitespace($input, $eol = true)
588
    {
589
        return preg_replace("#\s+#", " ", $input) . ($eol ? PHP_EOL : null);
590
    }
591
}
592