Revision 688 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?php declare(strict_types=1);/** This file is part of phpunit/php-code-coverage.** (c) Sebastian Bergmann <sebastian@phpunit.de>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace SebastianBergmann\CodeCoverage\Report\Html;use const ENT_COMPAT;use const ENT_HTML401;use const ENT_SUBSTITUTE;use const T_ABSTRACT;use const T_ARRAY;use const T_AS;use const T_BREAK;use const T_CALLABLE;use const T_CASE;use const T_CATCH;use const T_CLASS;use const T_CLONE;use const T_COMMENT;use const T_CONST;use const T_CONTINUE;use const T_DECLARE;use const T_DEFAULT;use const T_DO;use const T_DOC_COMMENT;use const T_ECHO;use const T_ELSE;use const T_ELSEIF;use const T_EMPTY;use const T_ENDDECLARE;use const T_ENDFOR;use const T_ENDFOREACH;use const T_ENDIF;use const T_ENDSWITCH;use const T_ENDWHILE;use const T_EVAL;use const T_EXIT;use const T_EXTENDS;use const T_FINAL;use const T_FINALLY;use const T_FOR;use const T_FOREACH;use const T_FUNCTION;use const T_GLOBAL;use const T_GOTO;use const T_HALT_COMPILER;use const T_IF;use const T_IMPLEMENTS;use const T_INCLUDE;use const T_INCLUDE_ONCE;use const T_INLINE_HTML;use const T_INSTANCEOF;use const T_INSTEADOF;use const T_INTERFACE;use const T_ISSET;use const T_LIST;use const T_NAMESPACE;use const T_NEW;use const T_PRINT;use const T_PRIVATE;use const T_PROTECTED;use const T_PUBLIC;use const T_REQUIRE;use const T_REQUIRE_ONCE;use const T_RETURN;use const T_STATIC;use const T_SWITCH;use const T_THROW;use const T_TRAIT;use const T_TRY;use const T_UNSET;use const T_USE;use const T_VAR;use const T_WHILE;use const T_YIELD;use const T_YIELD_FROM;use function array_key_exists;use function array_keys;use function array_merge;use function array_pop;use function array_unique;use function constant;use function count;use function defined;use function explode;use function file_get_contents;use function htmlspecialchars;use function is_string;use function ksort;use function range;use function sort;use function sprintf;use function str_replace;use function substr;use function token_get_all;use function trim;use PHPUnit\Runner\BaseTestRunner;use SebastianBergmann\CodeCoverage\Node\File as FileNode;use SebastianBergmann\CodeCoverage\Util\Percentage;use SebastianBergmann\Template\Template;/*** @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage*/final class File extends Renderer{/*** @psalm-var array<int,true>*/private static $keywordTokens = [];/*** @var array*/private static $formattedSourceCache = [];/*** @var int*/private $htmlSpecialCharsFlags = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;public function render(FileNode $node, string $file): void{$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'file_branch.html' : 'file.html');$template = new Template($templateName, '{{', '}}');$this->setCommonTemplateVariables($template, $node);$template->setVar(['items' => $this->renderItems($node),'lines' => $this->renderSourceWithLineCoverage($node),'legend' => '<p><span class="legend covered-by-small-tests">Covered by small (and larger) tests</span><span class="legend covered-by-medium-tests">Covered by medium (and large) tests</span><span class="legend covered-by-large-tests">Covered by large tests (and tests of unknown size)</span><span class="legend not-covered">Not covered</span><span class="legend not-coverable">Not coverable</span></p>','structure' => '',]);$template->renderTo($file . '.html');if ($this->hasBranchCoverage) {$template->setVar(['items' => $this->renderItems($node),'lines' => $this->renderSourceWithBranchCoverage($node),'legend' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p>','structure' => $this->renderBranchStructure($node),]);$template->renderTo($file . '_branch.html');$template->setVar(['items' => $this->renderItems($node),'lines' => $this->renderSourceWithPathCoverage($node),'legend' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p>','structure' => $this->renderPathStructure($node),]);$template->renderTo($file . '_path.html');}}private function renderItems(FileNode $node): string{$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'file_item_branch.html' : 'file_item.html');$template = new Template($templateName, '{{', '}}');$methodTemplateName = $this->templatePath . ($this->hasBranchCoverage ? 'method_item_branch.html' : 'method_item.html');$methodItemTemplate = new Template($methodTemplateName,'{{','}}');$items = $this->renderItemTemplate($template,['name' => 'Total','numClasses' => $node->numberOfClassesAndTraits(),'numTestedClasses' => $node->numberOfTestedClassesAndTraits(),'numMethods' => $node->numberOfFunctionsAndMethods(),'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(),'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(),'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(),'numExecutedLines' => $node->numberOfExecutedLines(),'numExecutableLines' => $node->numberOfExecutableLines(),'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(),'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(),'numExecutedBranches' => $node->numberOfExecutedBranches(),'numExecutableBranches' => $node->numberOfExecutableBranches(),'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(),'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(),'numExecutedPaths' => $node->numberOfExecutedPaths(),'numExecutablePaths' => $node->numberOfExecutablePaths(),'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(),'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(),'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(),'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(),'crap' => '<abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr>',]);$items .= $this->renderFunctionItems($node->functions(),$methodItemTemplate);$items .= $this->renderTraitOrClassItems($node->traits(),$template,$methodItemTemplate);$items .= $this->renderTraitOrClassItems($node->classes(),$template,$methodItemTemplate);return $items;}private function renderTraitOrClassItems(array $items, Template $template, Template $methodItemTemplate): string{$buffer = '';if (empty($items)) {return $buffer;}foreach ($items as $name => $item) {$numMethods = 0;$numTestedMethods = 0;foreach ($item['methods'] as $method) {if ($method['executableLines'] > 0) {$numMethods++;if ($method['executedLines'] === $method['executableLines']) {$numTestedMethods++;}}}if ($item['executableLines'] > 0) {$numClasses = 1;$numTestedClasses = $numTestedMethods === $numMethods ? 1 : 0;$linesExecutedPercentAsString = Percentage::fromFractionAndTotal($item['executedLines'],$item['executableLines'])->asString();$branchesExecutedPercentAsString = Percentage::fromFractionAndTotal($item['executedBranches'],$item['executableBranches'])->asString();$pathsExecutedPercentAsString = Percentage::fromFractionAndTotal($item['executedPaths'],$item['executablePaths'])->asString();} else {$numClasses = 0;$numTestedClasses = 0;$linesExecutedPercentAsString = 'n/a';$branchesExecutedPercentAsString = 'n/a';$pathsExecutedPercentAsString = 'n/a';}$testedMethodsPercentage = Percentage::fromFractionAndTotal($numTestedMethods,$numMethods);$testedClassesPercentage = Percentage::fromFractionAndTotal($numTestedMethods === $numMethods ? 1 : 0,1);$buffer .= $this->renderItemTemplate($template,['name' => $this->abbreviateClassName($name),'numClasses' => $numClasses,'numTestedClasses' => $numTestedClasses,'numMethods' => $numMethods,'numTestedMethods' => $numTestedMethods,'linesExecutedPercent' => Percentage::fromFractionAndTotal($item['executedLines'],$item['executableLines'],)->asFloat(),'linesExecutedPercentAsString' => $linesExecutedPercentAsString,'numExecutedLines' => $item['executedLines'],'numExecutableLines' => $item['executableLines'],'branchesExecutedPercent' => Percentage::fromFractionAndTotal($item['executedBranches'],$item['executableBranches'],)->asFloat(),'branchesExecutedPercentAsString' => $branchesExecutedPercentAsString,'numExecutedBranches' => $item['executedBranches'],'numExecutableBranches' => $item['executableBranches'],'pathsExecutedPercent' => Percentage::fromFractionAndTotal($item['executedPaths'],$item['executablePaths'])->asFloat(),'pathsExecutedPercentAsString' => $pathsExecutedPercentAsString,'numExecutedPaths' => $item['executedPaths'],'numExecutablePaths' => $item['executablePaths'],'testedMethodsPercent' => $testedMethodsPercentage->asFloat(),'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),'testedClassesPercent' => $testedClassesPercentage->asFloat(),'testedClassesPercentAsString' => $testedClassesPercentage->asString(),'crap' => $item['crap'],]);foreach ($item['methods'] as $method) {$buffer .= $this->renderFunctionOrMethodItem($methodItemTemplate,$method,' ');}}return $buffer;}private function renderFunctionItems(array $functions, Template $template): string{if (empty($functions)) {return '';}$buffer = '';foreach ($functions as $function) {$buffer .= $this->renderFunctionOrMethodItem($template,$function);}return $buffer;}private function renderFunctionOrMethodItem(Template $template, array $item, string $indent = ''): string{$numMethods = 0;$numTestedMethods = 0;if ($item['executableLines'] > 0) {$numMethods = 1;if ($item['executedLines'] === $item['executableLines']) {$numTestedMethods = 1;}}$executedLinesPercentage = Percentage::fromFractionAndTotal($item['executedLines'],$item['executableLines']);$executedBranchesPercentage = Percentage::fromFractionAndTotal($item['executedBranches'],$item['executableBranches']);$executedPathsPercentage = Percentage::fromFractionAndTotal($item['executedPaths'],$item['executablePaths']);$testedMethodsPercentage = Percentage::fromFractionAndTotal($numTestedMethods,1);return $this->renderItemTemplate($template,['name' => sprintf('%s<a href="#%d"><abbr title="%s">%s</abbr></a>',$indent,$item['startLine'],htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),$item['functionName'] ?? $item['methodName']),'numMethods' => $numMethods,'numTestedMethods' => $numTestedMethods,'linesExecutedPercent' => $executedLinesPercentage->asFloat(),'linesExecutedPercentAsString' => $executedLinesPercentage->asString(),'numExecutedLines' => $item['executedLines'],'numExecutableLines' => $item['executableLines'],'branchesExecutedPercent' => $executedBranchesPercentage->asFloat(),'branchesExecutedPercentAsString' => $executedBranchesPercentage->asString(),'numExecutedBranches' => $item['executedBranches'],'numExecutableBranches' => $item['executableBranches'],'pathsExecutedPercent' => $executedPathsPercentage->asFloat(),'pathsExecutedPercentAsString' => $executedPathsPercentage->asString(),'numExecutedPaths' => $item['executedPaths'],'numExecutablePaths' => $item['executablePaths'],'testedMethodsPercent' => $testedMethodsPercentage->asFloat(),'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),'crap' => $item['crap'],]);}private function renderSourceWithLineCoverage(FileNode $node): string{$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');$coverageData = $node->lineCoverageData();$testData = $node->testData();$codeLines = $this->loadFile($node->pathAsString());$lines = '';$i = 1;foreach ($codeLines as $line) {$trClass = '';$popoverContent = '';$popoverTitle = '';if (array_key_exists($i, $coverageData)) {$numTests = ($coverageData[$i] ? count($coverageData[$i]) : 0);if ($coverageData[$i] === null) {$trClass = 'warning';} elseif ($numTests === 0) {$trClass = 'danger';} else {if ($numTests > 1) {$popoverTitle = $numTests . ' tests cover line ' . $i;} else {$popoverTitle = '1 test covers line ' . $i;}$lineCss = 'covered-by-large-tests';$popoverContent = '<ul>';foreach ($coverageData[$i] as $test) {if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {$lineCss = 'covered-by-medium-tests';} elseif ($testData[$test]['size'] === 'small') {$lineCss = 'covered-by-small-tests';}$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);}$popoverContent .= '</ul>';$trClass = $lineCss . ' popin';}}$popover = '';if (!empty($popoverTitle)) {$popover = sprintf(' data-title="%s" data-content="%s" data-placement="top" data-html="true"',$popoverTitle,htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags));}$lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover);$i++;}$linesTemplate->setVar(['lines' => $lines]);return $linesTemplate->render();}private function renderSourceWithBranchCoverage(FileNode $node): string{$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');$functionCoverageData = $node->functionCoverageData();$testData = $node->testData();$codeLines = $this->loadFile($node->pathAsString());$lineData = [];/** @var int $line */foreach (array_keys($codeLines) as $line) {$lineData[$line + 1] = ['includedInBranches' => 0,'includedInHitBranches' => 0,'tests' => [],];}foreach ($functionCoverageData as $method) {foreach ($method['branches'] as $branch) {foreach (range($branch['line_start'], $branch['line_end']) as $line) {if (!isset($lineData[$line])) { // blank line at end of file is sometimes included herecontinue;}$lineData[$line]['includedInBranches']++;if ($branch['hit']) {$lineData[$line]['includedInHitBranches']++;$lineData[$line]['tests'] = array_unique(array_merge($lineData[$line]['tests'], $branch['hit']));}}}}$lines = '';$i = 1;/** @var string $line */foreach ($codeLines as $line) {$trClass = '';$popover = '';if ($lineData[$i]['includedInBranches'] > 0) {$lineCss = 'success';if ($lineData[$i]['includedInHitBranches'] === 0) {$lineCss = 'danger';} elseif ($lineData[$i]['includedInHitBranches'] !== $lineData[$i]['includedInBranches']) {$lineCss = 'warning';}$popoverContent = '<ul>';if (count($lineData[$i]['tests']) === 1) {$popoverTitle = '1 test covers line ' . $i;} else {$popoverTitle = count($lineData[$i]['tests']) . ' tests cover line ' . $i;}$popoverTitle .= '. These are covering ' . $lineData[$i]['includedInHitBranches'] . ' out of the ' . $lineData[$i]['includedInBranches'] . ' code branches.';foreach ($lineData[$i]['tests'] as $test) {$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);}$popoverContent .= '</ul>';$trClass = $lineCss . ' popin';$popover = sprintf(' data-title="%s" data-content="%s" data-placement="top" data-html="true"',$popoverTitle,htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags));}$lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover);$i++;}$linesTemplate->setVar(['lines' => $lines]);return $linesTemplate->render();}private function renderSourceWithPathCoverage(FileNode $node): string{$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');$functionCoverageData = $node->functionCoverageData();$testData = $node->testData();$codeLines = $this->loadFile($node->pathAsString());$lineData = [];/** @var int $line */foreach (array_keys($codeLines) as $line) {$lineData[$line + 1] = ['includedInPaths' => [],'includedInHitPaths' => [],'tests' => [],];}foreach ($functionCoverageData as $method) {foreach ($method['paths'] as $pathId => $path) {foreach ($path['path'] as $branchTaken) {foreach (range($method['branches'][$branchTaken]['line_start'], $method['branches'][$branchTaken]['line_end']) as $line) {if (!isset($lineData[$line])) {continue;}$lineData[$line]['includedInPaths'][] = $pathId;if ($path['hit']) {$lineData[$line]['includedInHitPaths'][] = $pathId;$lineData[$line]['tests'] = array_unique(array_merge($lineData[$line]['tests'], $path['hit']));}}}}}$lines = '';$i = 1;/** @var string $line */foreach ($codeLines as $line) {$trClass = '';$popover = '';$includedInPathsCount = count(array_unique($lineData[$i]['includedInPaths']));$includedInHitPathsCount = count(array_unique($lineData[$i]['includedInHitPaths']));if ($includedInPathsCount > 0) {$lineCss = 'success';if ($includedInHitPathsCount === 0) {$lineCss = 'danger';} elseif ($includedInHitPathsCount !== $includedInPathsCount) {$lineCss = 'warning';}$popoverContent = '<ul>';if (count($lineData[$i]['tests']) === 1) {$popoverTitle = '1 test covers line ' . $i;} else {$popoverTitle = count($lineData[$i]['tests']) . ' tests cover line ' . $i;}$popoverTitle .= '. These are covering ' . $includedInHitPathsCount . ' out of the ' . $includedInPathsCount . ' code paths.';foreach ($lineData[$i]['tests'] as $test) {$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);}$popoverContent .= '</ul>';$trClass = $lineCss . ' popin';$popover = sprintf(' data-title="%s" data-content="%s" data-placement="top" data-html="true"',$popoverTitle,htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags));}$lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover);$i++;}$linesTemplate->setVar(['lines' => $lines]);return $linesTemplate->render();}private function renderBranchStructure(FileNode $node): string{$branchesTemplate = new Template($this->templatePath . 'branches.html.dist', '{{', '}}');$coverageData = $node->functionCoverageData();$testData = $node->testData();$codeLines = $this->loadFile($node->pathAsString());$branches = '';ksort($coverageData);foreach ($coverageData as $methodName => $methodData) {if (!$methodData['branches']) {continue;}$branchStructure = '';foreach ($methodData['branches'] as $branch) {$branchStructure .= $this->renderBranchLines($branch, $codeLines, $testData);}if ($branchStructure !== '') { // don't show empty branches$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";$branches .= $branchStructure;}}$branchesTemplate->setVar(['branches' => $branches]);return $branchesTemplate->render();}private function renderBranchLines(array $branch, array $codeLines, array $testData): string{$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');$lines = '';$branchLines = range($branch['line_start'], $branch['line_end']);sort($branchLines); // sometimes end_line < start_line/** @var int $line */foreach ($branchLines as $line) {if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included herecontinue;}$popoverContent = '';$popoverTitle = '';$numTests = count($branch['hit']);if ($numTests === 0) {$trClass = 'danger';} else {$lineCss = 'covered-by-large-tests';$popoverContent = '<ul>';if ($numTests > 1) {$popoverTitle = $numTests . ' tests cover this branch';} else {$popoverTitle = '1 test covers this branch';}foreach ($branch['hit'] as $test) {if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {$lineCss = 'covered-by-medium-tests';} elseif ($testData[$test]['size'] === 'small') {$lineCss = 'covered-by-small-tests';}$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);}$trClass = $lineCss . ' popin';}$popover = '';if (!empty($popoverTitle)) {$popover = sprintf(' data-title="%s" data-content="%s" data-placement="top" data-html="true"',$popoverTitle,htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags));}$lines .= $this->renderLine($singleLineTemplate, $line, $codeLines[$line - 1], $trClass, $popover);}if ($lines === '') {return '';}$linesTemplate->setVar(['lines' => $lines]);return $linesTemplate->render();}private function renderPathStructure(FileNode $node): string{$pathsTemplate = new Template($this->templatePath . 'paths.html.dist', '{{', '}}');$coverageData = $node->functionCoverageData();$testData = $node->testData();$codeLines = $this->loadFile($node->pathAsString());$paths = '';ksort($coverageData);foreach ($coverageData as $methodName => $methodData) {if (!$methodData['paths']) {continue;}$pathStructure = '';if (count($methodData['paths']) > 100) {$pathStructure .= '<p>' . count($methodData['paths']) . ' is too many paths to sensibly render, consider refactoring your code to bring this number down.</p>';continue;}foreach ($methodData['paths'] as $path) {$pathStructure .= $this->renderPathLines($path, $methodData['branches'], $codeLines, $testData);}if ($pathStructure !== '') {$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";$paths .= $pathStructure;}}$pathsTemplate->setVar(['paths' => $paths]);return $pathsTemplate->render();}private function renderPathLines(array $path, array $branches, array $codeLines, array $testData): string{$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');$lines = '';$first = true;foreach ($path['path'] as $branchId) {if ($first) {$first = false;} else {$lines .= ' <tr><td colspan="2"> </td></tr>' . "\n";}$branchLines = range($branches[$branchId]['line_start'], $branches[$branchId]['line_end']);sort($branchLines); // sometimes end_line < start_line/** @var int $line */foreach ($branchLines as $line) {if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included herecontinue;}$popoverContent = '';$popoverTitle = '';$numTests = count($path['hit']);if ($numTests === 0) {$trClass = 'danger';} else {$lineCss = 'covered-by-large-tests';$popoverContent = '<ul>';if ($numTests > 1) {$popoverTitle = $numTests . ' tests cover this path';} else {$popoverTitle = '1 test covers this path';}foreach ($path['hit'] as $test) {if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {$lineCss = 'covered-by-medium-tests';} elseif ($testData[$test]['size'] === 'small') {$lineCss = 'covered-by-small-tests';}$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);}$trClass = $lineCss . ' popin';}$popover = '';if (!empty($popoverTitle)) {$popover = sprintf(' data-title="%s" data-content="%s" data-placement="top" data-html="true"',$popoverTitle,htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags));}$lines .= $this->renderLine($singleLineTemplate, $line, $codeLines[$line - 1], $trClass, $popover);}}if ($lines === '') {return '';}$linesTemplate->setVar(['lines' => $lines]);return $linesTemplate->render();}private function renderLine(Template $template, int $lineNumber, string $lineContent, string $class, string $popover): string{$template->setVar(['lineNumber' => $lineNumber,'lineContent' => $lineContent,'class' => $class,'popover' => $popover,]);return $template->render();}private function loadFile(string $file): array{if (isset(self::$formattedSourceCache[$file])) {return self::$formattedSourceCache[$file];}$buffer = file_get_contents($file);$tokens = token_get_all($buffer);$result = [''];$i = 0;$stringFlag = false;$fileEndsWithNewLine = substr($buffer, -1) === "\n";unset($buffer);foreach ($tokens as $j => $token) {if (is_string($token)) {if ($token === '"' && $tokens[$j - 1] !== '\\') {$result[$i] .= sprintf('<span class="string">%s</span>',htmlspecialchars($token, $this->htmlSpecialCharsFlags));$stringFlag = !$stringFlag;} else {$result[$i] .= sprintf('<span class="keyword">%s</span>',htmlspecialchars($token, $this->htmlSpecialCharsFlags));}continue;}[$token, $value] = $token;$value = str_replace(["\t", ' '],[' ', ' '],htmlspecialchars($value, $this->htmlSpecialCharsFlags));if ($value === "\n") {$result[++$i] = '';} else {$lines = explode("\n", $value);foreach ($lines as $jj => $line) {$line = trim($line);if ($line !== '') {if ($stringFlag) {$colour = 'string';} else {$colour = 'default';if ($this->isInlineHtml($token)) {$colour = 'html';} elseif ($this->isComment($token)) {$colour = 'comment';} elseif ($this->isKeyword($token)) {$colour = 'keyword';}}$result[$i] .= sprintf('<span class="%s">%s</span>',$colour,$line);}if (isset($lines[$jj + 1])) {$result[++$i] = '';}}}}if ($fileEndsWithNewLine) {unset($result[count($result) - 1]);}self::$formattedSourceCache[$file] = $result;return $result;}private function abbreviateClassName(string $className): string{$tmp = explode('\\', $className);if (count($tmp) > 1) {$className = sprintf('<abbr title="%s">%s</abbr>',$className,array_pop($tmp));}return $className;}private function abbreviateMethodName(string $methodName): string{$parts = explode('->', $methodName);if (count($parts) === 2) {return $this->abbreviateClassName($parts[0]) . '->' . $parts[1];}return $methodName;}private function createPopoverContentForTest(string $test, array $testData): string{$testCSS = '';if ($testData['fromTestcase']) {switch ($testData['status']) {case BaseTestRunner::STATUS_PASSED:switch ($testData['size']) {case 'small':$testCSS = ' class="covered-by-small-tests"';break;case 'medium':$testCSS = ' class="covered-by-medium-tests"';break;default:$testCSS = ' class="covered-by-large-tests"';break;}break;case BaseTestRunner::STATUS_SKIPPED:case BaseTestRunner::STATUS_INCOMPLETE:case BaseTestRunner::STATUS_RISKY:case BaseTestRunner::STATUS_WARNING:$testCSS = ' class="warning"';break;case BaseTestRunner::STATUS_FAILURE:case BaseTestRunner::STATUS_ERROR:$testCSS = ' class="danger"';break;}}return sprintf('<li%s>%s</li>',$testCSS,htmlspecialchars($test, $this->htmlSpecialCharsFlags));}private function isComment(int $token): bool{return $token === T_COMMENT || $token === T_DOC_COMMENT;}private function isInlineHtml(int $token): bool{return $token === T_INLINE_HTML;}private function isKeyword(int $token): bool{return isset(self::keywordTokens()[$token]);}/*** @psalm-return array<int,true>*/private static function keywordTokens(): array{if (self::$keywordTokens !== []) {return self::$keywordTokens;}self::$keywordTokens = [T_ABSTRACT => true,T_ARRAY => true,T_AS => true,T_BREAK => true,T_CALLABLE => true,T_CASE => true,T_CATCH => true,T_CLASS => true,T_CLONE => true,T_CONST => true,T_CONTINUE => true,T_DECLARE => true,T_DEFAULT => true,T_DO => true,T_ECHO => true,T_ELSE => true,T_ELSEIF => true,T_EMPTY => true,T_ENDDECLARE => true,T_ENDFOR => true,T_ENDFOREACH => true,T_ENDIF => true,T_ENDSWITCH => true,T_ENDWHILE => true,T_EVAL => true,T_EXIT => true,T_EXTENDS => true,T_FINAL => true,T_FINALLY => true,T_FOR => true,T_FOREACH => true,T_FUNCTION => true,T_GLOBAL => true,T_GOTO => true,T_HALT_COMPILER => true,T_IF => true,T_IMPLEMENTS => true,T_INCLUDE => true,T_INCLUDE_ONCE => true,T_INSTANCEOF => true,T_INSTEADOF => true,T_INTERFACE => true,T_ISSET => true,T_LIST => true,T_NAMESPACE => true,T_NEW => true,T_PRINT => true,T_PRIVATE => true,T_PROTECTED => true,T_PUBLIC => true,T_REQUIRE => true,T_REQUIRE_ONCE => true,T_RETURN => true,T_STATIC => true,T_SWITCH => true,T_THROW => true,T_TRAIT => true,T_TRY => true,T_UNSET => true,T_USE => true,T_VAR => true,T_WHILE => true,T_YIELD => true,T_YIELD_FROM => true,];if (defined('T_FN')) {self::$keywordTokens[constant('T_FN')] = true;}if (defined('T_MATCH')) {self::$keywordTokens[constant('T_MATCH')] = true;}if (defined('T_ENUM')) {self::$keywordTokens[constant('T_ENUM')] = true;}if (defined('T_READONLY')) {self::$keywordTokens[constant('T_READONLY')] = true;}return self::$keywordTokens;}}