Builder.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of phpunit/php-code-coverage.
  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. namespace SebastianBergmann\CodeCoverage\Node;
  11. use const DIRECTORY_SEPARATOR;
  12. use function array_shift;
  13. use function basename;
  14. use function count;
  15. use function dirname;
  16. use function explode;
  17. use function implode;
  18. use function is_file;
  19. use function str_ends_with;
  20. use function str_replace;
  21. use function str_starts_with;
  22. use function substr;
  23. use SebastianBergmann\CodeCoverage\CodeCoverage;
  24. use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
  25. use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
  26. /**
  27. * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
  28. *
  29. * @psalm-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
  30. */
  31. final class Builder
  32. {
  33. private readonly FileAnalyser $analyser;
  34. public function __construct(FileAnalyser $analyser)
  35. {
  36. $this->analyser = $analyser;
  37. }
  38. public function build(CodeCoverage $coverage): Directory
  39. {
  40. $data = clone $coverage->getData(); // clone because path munging is destructive to the original data
  41. $commonPath = $this->reducePaths($data);
  42. $root = new Directory(
  43. $commonPath,
  44. null,
  45. );
  46. $this->addItems(
  47. $root,
  48. $this->buildDirectoryStructure($data),
  49. $coverage->getTests(),
  50. );
  51. return $root;
  52. }
  53. /**
  54. * @psalm-param array<string, TestType> $tests
  55. */
  56. private function addItems(Directory $root, array $items, array $tests): void
  57. {
  58. foreach ($items as $key => $value) {
  59. $key = (string) $key;
  60. if (str_ends_with($key, '/f')) {
  61. $key = substr($key, 0, -2);
  62. $filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
  63. if (is_file($filename)) {
  64. $root->addFile(
  65. new File(
  66. $key,
  67. $root,
  68. $value['lineCoverage'],
  69. $value['functionCoverage'],
  70. $tests,
  71. $this->analyser->classesIn($filename),
  72. $this->analyser->traitsIn($filename),
  73. $this->analyser->functionsIn($filename),
  74. $this->analyser->linesOfCodeFor($filename),
  75. ),
  76. );
  77. }
  78. } else {
  79. $child = $root->addDirectory($key);
  80. $this->addItems($child, $value, $tests);
  81. }
  82. }
  83. }
  84. /**
  85. * Builds an array representation of the directory structure.
  86. *
  87. * For instance,
  88. *
  89. * <code>
  90. * Array
  91. * (
  92. * [Money.php] => Array
  93. * (
  94. * ...
  95. * )
  96. *
  97. * [MoneyBag.php] => Array
  98. * (
  99. * ...
  100. * )
  101. * )
  102. * </code>
  103. *
  104. * is transformed into
  105. *
  106. * <code>
  107. * Array
  108. * (
  109. * [.] => Array
  110. * (
  111. * [Money.php] => Array
  112. * (
  113. * ...
  114. * )
  115. *
  116. * [MoneyBag.php] => Array
  117. * (
  118. * ...
  119. * )
  120. * )
  121. * )
  122. * </code>
  123. *
  124. * @psalm-return array<string, array<string, array{lineCoverage: array<int, int>, functionCoverage: array<string, array<int, int>>}>>
  125. */
  126. private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array
  127. {
  128. $result = [];
  129. foreach ($data->coveredFiles() as $originalPath) {
  130. $path = explode(DIRECTORY_SEPARATOR, $originalPath);
  131. $pointer = &$result;
  132. $max = count($path);
  133. for ($i = 0; $i < $max; $i++) {
  134. $type = '';
  135. if ($i === ($max - 1)) {
  136. $type = '/f';
  137. }
  138. $pointer = &$pointer[$path[$i] . $type];
  139. }
  140. $pointer = [
  141. 'lineCoverage' => $data->lineCoverage()[$originalPath] ?? [],
  142. 'functionCoverage' => $data->functionCoverage()[$originalPath] ?? [],
  143. ];
  144. }
  145. return $result;
  146. }
  147. /**
  148. * Reduces the paths by cutting the longest common start path.
  149. *
  150. * For instance,
  151. *
  152. * <code>
  153. * Array
  154. * (
  155. * [/home/sb/Money/Money.php] => Array
  156. * (
  157. * ...
  158. * )
  159. *
  160. * [/home/sb/Money/MoneyBag.php] => Array
  161. * (
  162. * ...
  163. * )
  164. * )
  165. * </code>
  166. *
  167. * is reduced to
  168. *
  169. * <code>
  170. * Array
  171. * (
  172. * [Money.php] => Array
  173. * (
  174. * ...
  175. * )
  176. *
  177. * [MoneyBag.php] => Array
  178. * (
  179. * ...
  180. * )
  181. * )
  182. * </code>
  183. */
  184. private function reducePaths(ProcessedCodeCoverageData $coverage): string
  185. {
  186. if (empty($coverage->coveredFiles())) {
  187. return '.';
  188. }
  189. $commonPath = '';
  190. $paths = $coverage->coveredFiles();
  191. if (count($paths) === 1) {
  192. $commonPath = dirname($paths[0]) . DIRECTORY_SEPARATOR;
  193. $coverage->renameFile($paths[0], basename($paths[0]));
  194. return $commonPath;
  195. }
  196. $max = count($paths);
  197. for ($i = 0; $i < $max; $i++) {
  198. // strip phar:// prefixes
  199. if (str_starts_with($paths[$i], 'phar://')) {
  200. $paths[$i] = substr($paths[$i], 7);
  201. $paths[$i] = str_replace('/', DIRECTORY_SEPARATOR, $paths[$i]);
  202. }
  203. $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
  204. if (empty($paths[$i][0])) {
  205. $paths[$i][0] = DIRECTORY_SEPARATOR;
  206. }
  207. }
  208. $done = false;
  209. $max = count($paths);
  210. while (!$done) {
  211. for ($i = 0; $i < $max - 1; $i++) {
  212. if (!isset($paths[$i][0]) ||
  213. !isset($paths[$i + 1][0]) ||
  214. $paths[$i][0] !== $paths[$i + 1][0]) {
  215. $done = true;
  216. break;
  217. }
  218. }
  219. if (!$done) {
  220. $commonPath .= $paths[0][0];
  221. if ($paths[0][0] !== DIRECTORY_SEPARATOR) {
  222. $commonPath .= DIRECTORY_SEPARATOR;
  223. }
  224. for ($i = 0; $i < $max; $i++) {
  225. array_shift($paths[$i]);
  226. }
  227. }
  228. }
  229. $original = $coverage->coveredFiles();
  230. $max = count($original);
  231. for ($i = 0; $i < $max; $i++) {
  232. $coverage->renameFile($original[$i], implode(DIRECTORY_SEPARATOR, $paths[$i]));
  233. }
  234. return substr($commonPath, 0, -1);
  235. }
  236. }