PageRenderTime 43ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/src/CodeCoverage.php

http://github.com/sebastianbergmann/php-code-coverage
PHP | 1006 lines | 622 code | 181 blank | 203 comment | 83 complexity | d3fbcd5a9869196736f52c3da2f5c5f8 MD5 | raw file
Possible License(s): BSD-3-Clause
  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;
  11. use PHPUnit\Framework\TestCase;
  12. use PHPUnit\Runner\PhptTestCase;
  13. use PHPUnit\Util\Test;
  14. use SebastianBergmann\CodeCoverage\Driver\Driver;
  15. use SebastianBergmann\CodeCoverage\Driver\PCOV;
  16. use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
  17. use SebastianBergmann\CodeCoverage\Driver\Xdebug;
  18. use SebastianBergmann\CodeCoverage\Node\Builder;
  19. use SebastianBergmann\CodeCoverage\Node\Directory;
  20. use SebastianBergmann\CodeUnitReverseLookup\Wizard;
  21. use SebastianBergmann\Environment\Runtime;
  22. /**
  23. * Provides collection functionality for PHP code coverage information.
  24. */
  25. final class CodeCoverage
  26. {
  27. /**
  28. * @var Driver
  29. */
  30. private $driver;
  31. /**
  32. * @var Filter
  33. */
  34. private $filter;
  35. /**
  36. * @var Wizard
  37. */
  38. private $wizard;
  39. /**
  40. * @var bool
  41. */
  42. private $cacheTokens = false;
  43. /**
  44. * @var bool
  45. */
  46. private $checkForUnintentionallyCoveredCode = false;
  47. /**
  48. * @var bool
  49. */
  50. private $forceCoversAnnotation = false;
  51. /**
  52. * @var bool
  53. */
  54. private $checkForUnexecutedCoveredCode = false;
  55. /**
  56. * @var bool
  57. */
  58. private $checkForMissingCoversAnnotation = false;
  59. /**
  60. * @var bool
  61. */
  62. private $addUncoveredFilesFromWhitelist = true;
  63. /**
  64. * @var bool
  65. */
  66. private $processUncoveredFilesFromWhitelist = false;
  67. /**
  68. * @var bool
  69. */
  70. private $ignoreDeprecatedCode = false;
  71. /**
  72. * @var PhptTestCase|string|TestCase
  73. */
  74. private $currentId;
  75. /**
  76. * Code coverage data.
  77. *
  78. * @var array
  79. */
  80. private $data = [];
  81. /**
  82. * @var array
  83. */
  84. private $ignoredLines = [];
  85. /**
  86. * @var bool
  87. */
  88. private $disableIgnoredLines = false;
  89. /**
  90. * Test data.
  91. *
  92. * @var array
  93. */
  94. private $tests = [];
  95. /**
  96. * @var string[]
  97. */
  98. private $unintentionallyCoveredSubclassesWhitelist = [];
  99. /**
  100. * Determine if the data has been initialized or not
  101. *
  102. * @var bool
  103. */
  104. private $isInitialized = false;
  105. /**
  106. * Determine whether we need to check for dead and unused code on each test
  107. *
  108. * @var bool
  109. */
  110. private $shouldCheckForDeadAndUnused = true;
  111. /**
  112. * @var Directory
  113. */
  114. private $report;
  115. /**
  116. * @throws RuntimeException
  117. */
  118. public function __construct(Driver $driver = null, Filter $filter = null)
  119. {
  120. if ($filter === null) {
  121. $filter = new Filter;
  122. }
  123. if ($driver === null) {
  124. $driver = $this->selectDriver($filter);
  125. }
  126. $this->driver = $driver;
  127. $this->filter = $filter;
  128. $this->wizard = new Wizard;
  129. }
  130. /**
  131. * Returns the code coverage information as a graph of node objects.
  132. */
  133. public function getReport(): Directory
  134. {
  135. if ($this->report === null) {
  136. $this->report = (new Builder)->build($this);
  137. }
  138. return $this->report;
  139. }
  140. /**
  141. * Clears collected code coverage data.
  142. */
  143. public function clear(): void
  144. {
  145. $this->isInitialized = false;
  146. $this->currentId = null;
  147. $this->data = [];
  148. $this->tests = [];
  149. $this->report = null;
  150. }
  151. /**
  152. * Returns the filter object used.
  153. */
  154. public function filter(): Filter
  155. {
  156. return $this->filter;
  157. }
  158. /**
  159. * Returns the collected code coverage data.
  160. */
  161. public function getData(bool $raw = false): array
  162. {
  163. if (!$raw && $this->addUncoveredFilesFromWhitelist) {
  164. $this->addUncoveredFilesFromWhitelist();
  165. }
  166. return $this->data;
  167. }
  168. /**
  169. * Sets the coverage data.
  170. */
  171. public function setData(array $data): void
  172. {
  173. $this->data = $data;
  174. $this->report = null;
  175. }
  176. /**
  177. * Returns the test data.
  178. */
  179. public function getTests(): array
  180. {
  181. return $this->tests;
  182. }
  183. /**
  184. * Sets the test data.
  185. */
  186. public function setTests(array $tests): void
  187. {
  188. $this->tests = $tests;
  189. }
  190. /**
  191. * Start collection of code coverage information.
  192. *
  193. * @param PhptTestCase|string|TestCase $id
  194. *
  195. * @throws RuntimeException
  196. */
  197. public function start($id, bool $clear = false): void
  198. {
  199. if ($clear) {
  200. $this->clear();
  201. }
  202. if ($this->isInitialized === false) {
  203. $this->initializeData();
  204. }
  205. $this->currentId = $id;
  206. $this->driver->start($this->shouldCheckForDeadAndUnused);
  207. }
  208. /**
  209. * Stop collection of code coverage information.
  210. *
  211. * @param array|false $linesToBeCovered
  212. *
  213. * @throws MissingCoversAnnotationException
  214. * @throws CoveredCodeNotExecutedException
  215. * @throws RuntimeException
  216. * @throws InvalidArgumentException
  217. * @throws \ReflectionException
  218. */
  219. public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): array
  220. {
  221. if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) {
  222. throw InvalidArgumentException::create(
  223. 2,
  224. 'array or false'
  225. );
  226. }
  227. $data = $this->driver->stop();
  228. $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation);
  229. $this->currentId = null;
  230. return $data;
  231. }
  232. /**
  233. * Appends code coverage data.
  234. *
  235. * @param PhptTestCase|string|TestCase $id
  236. * @param array|false $linesToBeCovered
  237. *
  238. * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
  239. * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException
  240. * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
  241. * @throws \ReflectionException
  242. * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
  243. * @throws RuntimeException
  244. */
  245. public function append(array $data, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): void
  246. {
  247. if ($id === null) {
  248. $id = $this->currentId;
  249. }
  250. if ($id === null) {
  251. throw new RuntimeException;
  252. }
  253. $this->applyWhitelistFilter($data);
  254. $this->applyIgnoredLinesFilter($data);
  255. $this->initializeFilesThatAreSeenTheFirstTime($data);
  256. if (!$append) {
  257. return;
  258. }
  259. if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') {
  260. $this->applyCoversAnnotationFilter(
  261. $data,
  262. $linesToBeCovered,
  263. $linesToBeUsed,
  264. $ignoreForceCoversAnnotation
  265. );
  266. }
  267. if (empty($data)) {
  268. return;
  269. }
  270. $size = 'unknown';
  271. $status = -1;
  272. if ($id instanceof TestCase) {
  273. $_size = $id->getSize();
  274. if ($_size === Test::SMALL) {
  275. $size = 'small';
  276. } elseif ($_size === Test::MEDIUM) {
  277. $size = 'medium';
  278. } elseif ($_size === Test::LARGE) {
  279. $size = 'large';
  280. }
  281. $status = $id->getStatus();
  282. $id = \get_class($id) . '::' . $id->getName();
  283. } elseif ($id instanceof PhptTestCase) {
  284. $size = 'large';
  285. $id = $id->getName();
  286. }
  287. $this->tests[$id] = ['size' => $size, 'status' => $status];
  288. foreach ($data as $file => $lines) {
  289. if (!$this->filter->isFile($file)) {
  290. continue;
  291. }
  292. foreach ($lines as $k => $v) {
  293. if ($v === Driver::LINE_EXECUTED) {
  294. if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) {
  295. $this->data[$file][$k][] = $id;
  296. }
  297. }
  298. }
  299. }
  300. $this->report = null;
  301. }
  302. /**
  303. * Merges the data from another instance.
  304. *
  305. * @param CodeCoverage $that
  306. */
  307. public function merge(self $that): void
  308. {
  309. $this->filter->setWhitelistedFiles(
  310. \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
  311. );
  312. foreach ($that->data as $file => $lines) {
  313. if (!isset($this->data[$file])) {
  314. if (!$this->filter->isFiltered($file)) {
  315. $this->data[$file] = $lines;
  316. }
  317. continue;
  318. }
  319. // we should compare the lines if any of two contains data
  320. $compareLineNumbers = \array_unique(
  321. \array_merge(
  322. \array_keys($this->data[$file]),
  323. \array_keys($that->data[$file])
  324. )
  325. );
  326. foreach ($compareLineNumbers as $line) {
  327. $thatPriority = $this->getLinePriority($that->data[$file], $line);
  328. $thisPriority = $this->getLinePriority($this->data[$file], $line);
  329. if ($thatPriority > $thisPriority) {
  330. $this->data[$file][$line] = $that->data[$file][$line];
  331. } elseif ($thatPriority === $thisPriority && \is_array($this->data[$file][$line])) {
  332. $this->data[$file][$line] = \array_unique(
  333. \array_merge($this->data[$file][$line], $that->data[$file][$line])
  334. );
  335. }
  336. }
  337. }
  338. $this->tests = \array_merge($this->tests, $that->getTests());
  339. $this->report = null;
  340. }
  341. public function setCacheTokens(bool $flag): void
  342. {
  343. $this->cacheTokens = $flag;
  344. }
  345. public function getCacheTokens(): bool
  346. {
  347. return $this->cacheTokens;
  348. }
  349. public function setCheckForUnintentionallyCoveredCode(bool $flag): void
  350. {
  351. $this->checkForUnintentionallyCoveredCode = $flag;
  352. }
  353. public function setForceCoversAnnotation(bool $flag): void
  354. {
  355. $this->forceCoversAnnotation = $flag;
  356. }
  357. public function setCheckForMissingCoversAnnotation(bool $flag): void
  358. {
  359. $this->checkForMissingCoversAnnotation = $flag;
  360. }
  361. public function setCheckForUnexecutedCoveredCode(bool $flag): void
  362. {
  363. $this->checkForUnexecutedCoveredCode = $flag;
  364. }
  365. public function setAddUncoveredFilesFromWhitelist(bool $flag): void
  366. {
  367. $this->addUncoveredFilesFromWhitelist = $flag;
  368. }
  369. public function setProcessUncoveredFilesFromWhitelist(bool $flag): void
  370. {
  371. $this->processUncoveredFilesFromWhitelist = $flag;
  372. }
  373. public function setDisableIgnoredLines(bool $flag): void
  374. {
  375. $this->disableIgnoredLines = $flag;
  376. }
  377. public function setIgnoreDeprecatedCode(bool $flag): void
  378. {
  379. $this->ignoreDeprecatedCode = $flag;
  380. }
  381. public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): void
  382. {
  383. $this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
  384. }
  385. /**
  386. * Determine the priority for a line
  387. *
  388. * 1 = the line is not set
  389. * 2 = the line has not been tested
  390. * 3 = the line is dead code
  391. * 4 = the line has been tested
  392. *
  393. * During a merge, a higher number is better.
  394. *
  395. * @param array $data
  396. * @param int $line
  397. *
  398. * @return int
  399. */
  400. private function getLinePriority($data, $line)
  401. {
  402. if (!\array_key_exists($line, $data)) {
  403. return 1;
  404. }
  405. if (\is_array($data[$line]) && \count($data[$line]) === 0) {
  406. return 2;
  407. }
  408. if ($data[$line] === null) {
  409. return 3;
  410. }
  411. return 4;
  412. }
  413. /**
  414. * Applies the @covers annotation filtering.
  415. *
  416. * @param array|false $linesToBeCovered
  417. *
  418. * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
  419. * @throws \ReflectionException
  420. * @throws MissingCoversAnnotationException
  421. * @throws UnintentionallyCoveredCodeException
  422. */
  423. private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, bool $ignoreForceCoversAnnotation): void
  424. {
  425. if ($linesToBeCovered === false ||
  426. ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) {
  427. if ($this->checkForMissingCoversAnnotation) {
  428. throw new MissingCoversAnnotationException;
  429. }
  430. $data = [];
  431. return;
  432. }
  433. if (empty($linesToBeCovered)) {
  434. return;
  435. }
  436. if ($this->checkForUnintentionallyCoveredCode &&
  437. (!$this->currentId instanceof TestCase ||
  438. (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
  439. $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
  440. }
  441. if ($this->checkForUnexecutedCoveredCode) {
  442. $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
  443. }
  444. $data = \array_intersect_key($data, $linesToBeCovered);
  445. foreach (\array_keys($data) as $filename) {
  446. $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]);
  447. $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered);
  448. }
  449. }
  450. private function applyWhitelistFilter(array &$data): void
  451. {
  452. foreach (\array_keys($data) as $filename) {
  453. if ($this->filter->isFiltered($filename)) {
  454. unset($data[$filename]);
  455. }
  456. }
  457. }
  458. /**
  459. * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
  460. */
  461. private function applyIgnoredLinesFilter(array &$data): void
  462. {
  463. foreach (\array_keys($data) as $filename) {
  464. if (!$this->filter->isFile($filename)) {
  465. continue;
  466. }
  467. foreach ($this->getLinesToBeIgnored($filename) as $line) {
  468. unset($data[$filename][$line]);
  469. }
  470. }
  471. }
  472. private function initializeFilesThatAreSeenTheFirstTime(array $data): void
  473. {
  474. foreach ($data as $file => $lines) {
  475. if (!isset($this->data[$file]) && $this->filter->isFile($file)) {
  476. $this->data[$file] = [];
  477. foreach ($lines as $k => $v) {
  478. $this->data[$file][$k] = $v === -2 ? null : [];
  479. }
  480. }
  481. }
  482. }
  483. /**
  484. * @throws CoveredCodeNotExecutedException
  485. * @throws InvalidArgumentException
  486. * @throws MissingCoversAnnotationException
  487. * @throws RuntimeException
  488. * @throws UnintentionallyCoveredCodeException
  489. * @throws \ReflectionException
  490. */
  491. private function addUncoveredFilesFromWhitelist(): void
  492. {
  493. $data = [];
  494. $uncoveredFiles = \array_diff(
  495. $this->filter->getWhitelist(),
  496. \array_keys($this->data)
  497. );
  498. foreach ($uncoveredFiles as $uncoveredFile) {
  499. if (!\file_exists($uncoveredFile)) {
  500. continue;
  501. }
  502. $data[$uncoveredFile] = [];
  503. $lines = \count(\file($uncoveredFile));
  504. for ($i = 1; $i <= $lines; $i++) {
  505. $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
  506. }
  507. }
  508. $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  509. }
  510. private function getLinesToBeIgnored(string $fileName): array
  511. {
  512. if (isset($this->ignoredLines[$fileName])) {
  513. return $this->ignoredLines[$fileName];
  514. }
  515. try {
  516. return $this->getLinesToBeIgnoredInner($fileName);
  517. } catch (\OutOfBoundsException $e) {
  518. // This can happen with PHP_Token_Stream if the file is syntactically invalid,
  519. // and probably affects a file that wasn't executed.
  520. return [];
  521. }
  522. }
  523. private function getLinesToBeIgnoredInner(string $fileName): array
  524. {
  525. $this->ignoredLines[$fileName] = [];
  526. $lines = \file($fileName);
  527. foreach ($lines as $index => $line) {
  528. if (!\trim($line)) {
  529. $this->ignoredLines[$fileName][] = $index + 1;
  530. }
  531. }
  532. if ($this->cacheTokens) {
  533. $tokens = \PHP_Token_Stream_CachingFactory::get($fileName);
  534. } else {
  535. $tokens = new \PHP_Token_Stream($fileName);
  536. }
  537. foreach ($tokens->getInterfaces() as $interface) {
  538. $interfaceStartLine = $interface['startLine'];
  539. $interfaceEndLine = $interface['endLine'];
  540. foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
  541. $this->ignoredLines[$fileName][] = $line;
  542. }
  543. }
  544. foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
  545. $classOrTraitStartLine = $classOrTrait['startLine'];
  546. $classOrTraitEndLine = $classOrTrait['endLine'];
  547. if (empty($classOrTrait['methods'])) {
  548. foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
  549. $this->ignoredLines[$fileName][] = $line;
  550. }
  551. continue;
  552. }
  553. $firstMethod = \array_shift($classOrTrait['methods']);
  554. $firstMethodStartLine = $firstMethod['startLine'];
  555. $lastMethodEndLine = $firstMethod['endLine'];
  556. do {
  557. $lastMethod = \array_pop($classOrTrait['methods']);
  558. } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
  559. if ($lastMethod !== null) {
  560. $lastMethodEndLine = $lastMethod['endLine'];
  561. }
  562. foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
  563. $this->ignoredLines[$fileName][] = $line;
  564. }
  565. foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
  566. $this->ignoredLines[$fileName][] = $line;
  567. }
  568. }
  569. if ($this->disableIgnoredLines) {
  570. $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
  571. \sort($this->ignoredLines[$fileName]);
  572. return $this->ignoredLines[$fileName];
  573. }
  574. $ignore = false;
  575. $stop = false;
  576. foreach ($tokens->tokens() as $token) {
  577. switch (\get_class($token)) {
  578. case \PHP_Token_COMMENT::class:
  579. case \PHP_Token_DOC_COMMENT::class:
  580. $_token = \trim((string) $token);
  581. $_line = \trim($lines[$token->getLine() - 1]);
  582. if ($_token === '// @codeCoverageIgnore' ||
  583. $_token === '//@codeCoverageIgnore') {
  584. $ignore = true;
  585. $stop = true;
  586. } elseif ($_token === '// @codeCoverageIgnoreStart' ||
  587. $_token === '//@codeCoverageIgnoreStart') {
  588. $ignore = true;
  589. } elseif ($_token === '// @codeCoverageIgnoreEnd' ||
  590. $_token === '//@codeCoverageIgnoreEnd') {
  591. $stop = true;
  592. }
  593. if (!$ignore) {
  594. $start = $token->getLine();
  595. $end = $start + \substr_count((string) $token, "\n");
  596. // Do not ignore the first line when there is a token
  597. // before the comment
  598. if (0 !== \strpos($_token, $_line)) {
  599. $start++;
  600. }
  601. for ($i = $start; $i < $end; $i++) {
  602. $this->ignoredLines[$fileName][] = $i;
  603. }
  604. // A DOC_COMMENT token or a COMMENT token starting with "/*"
  605. // does not contain the final \n character in its text
  606. if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
  607. $this->ignoredLines[$fileName][] = $i;
  608. }
  609. }
  610. break;
  611. case \PHP_Token_INTERFACE::class:
  612. case \PHP_Token_TRAIT::class:
  613. case \PHP_Token_CLASS::class:
  614. case \PHP_Token_FUNCTION::class:
  615. /* @var \PHP_Token_Interface $token */
  616. $docblock = (string) $token->getDocblock();
  617. $this->ignoredLines[$fileName][] = $token->getLine();
  618. if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
  619. $endLine = $token->getEndLine();
  620. for ($i = $token->getLine(); $i <= $endLine; $i++) {
  621. $this->ignoredLines[$fileName][] = $i;
  622. }
  623. }
  624. break;
  625. /* @noinspection PhpMissingBreakStatementInspection */
  626. case \PHP_Token_NAMESPACE::class:
  627. $this->ignoredLines[$fileName][] = $token->getEndLine();
  628. // Intentional fallthrough
  629. case \PHP_Token_DECLARE::class:
  630. case \PHP_Token_OPEN_TAG::class:
  631. case \PHP_Token_CLOSE_TAG::class:
  632. case \PHP_Token_USE::class:
  633. case \PHP_Token_USE_FUNCTION::class:
  634. $this->ignoredLines[$fileName][] = $token->getLine();
  635. break;
  636. }
  637. if ($ignore) {
  638. $this->ignoredLines[$fileName][] = $token->getLine();
  639. if ($stop) {
  640. $ignore = false;
  641. $stop = false;
  642. }
  643. }
  644. }
  645. $this->ignoredLines[$fileName][] = \count($lines) + 1;
  646. $this->ignoredLines[$fileName] = \array_unique(
  647. $this->ignoredLines[$fileName]
  648. );
  649. $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
  650. \sort($this->ignoredLines[$fileName]);
  651. return $this->ignoredLines[$fileName];
  652. }
  653. /**
  654. * @throws \ReflectionException
  655. * @throws UnintentionallyCoveredCodeException
  656. */
  657. private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void
  658. {
  659. $allowedLines = $this->getAllowedLines(
  660. $linesToBeCovered,
  661. $linesToBeUsed
  662. );
  663. $unintentionallyCoveredUnits = [];
  664. foreach ($data as $file => $_data) {
  665. foreach ($_data as $line => $flag) {
  666. if ($flag === 1 && !isset($allowedLines[$file][$line])) {
  667. $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
  668. }
  669. }
  670. }
  671. $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
  672. if (!empty($unintentionallyCoveredUnits)) {
  673. throw new UnintentionallyCoveredCodeException(
  674. $unintentionallyCoveredUnits
  675. );
  676. }
  677. }
  678. /**
  679. * @throws CoveredCodeNotExecutedException
  680. */
  681. private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void
  682. {
  683. $executedCodeUnits = $this->coverageToCodeUnits($data);
  684. $message = '';
  685. foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
  686. if (!\in_array($codeUnit, $executedCodeUnits)) {
  687. $message .= \sprintf(
  688. '- %s is expected to be executed (@covers) but was not executed' . "\n",
  689. $codeUnit
  690. );
  691. }
  692. }
  693. foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
  694. if (!\in_array($codeUnit, $executedCodeUnits)) {
  695. $message .= \sprintf(
  696. '- %s is expected to be executed (@uses) but was not executed' . "\n",
  697. $codeUnit
  698. );
  699. }
  700. }
  701. if (!empty($message)) {
  702. throw new CoveredCodeNotExecutedException($message);
  703. }
  704. }
  705. private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
  706. {
  707. $allowedLines = [];
  708. foreach (\array_keys($linesToBeCovered) as $file) {
  709. if (!isset($allowedLines[$file])) {
  710. $allowedLines[$file] = [];
  711. }
  712. $allowedLines[$file] = \array_merge(
  713. $allowedLines[$file],
  714. $linesToBeCovered[$file]
  715. );
  716. }
  717. foreach (\array_keys($linesToBeUsed) as $file) {
  718. if (!isset($allowedLines[$file])) {
  719. $allowedLines[$file] = [];
  720. }
  721. $allowedLines[$file] = \array_merge(
  722. $allowedLines[$file],
  723. $linesToBeUsed[$file]
  724. );
  725. }
  726. foreach (\array_keys($allowedLines) as $file) {
  727. $allowedLines[$file] = \array_flip(
  728. \array_unique($allowedLines[$file])
  729. );
  730. }
  731. return $allowedLines;
  732. }
  733. /**
  734. * @throws RuntimeException
  735. */
  736. private function selectDriver(Filter $filter): Driver
  737. {
  738. $runtime = new Runtime;
  739. if ($runtime->hasPHPDBGCodeCoverage()) {
  740. return new PHPDBG;
  741. }
  742. if ($runtime->hasPCOV()) {
  743. return new PCOV;
  744. }
  745. if ($runtime->hasXdebug()) {
  746. return new Xdebug($filter);
  747. }
  748. throw new RuntimeException('No code coverage driver available');
  749. }
  750. private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
  751. {
  752. $unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits);
  753. \sort($unintentionallyCoveredUnits);
  754. foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) {
  755. $unit = \explode('::', $unintentionallyCoveredUnits[$k]);
  756. if (\count($unit) !== 2) {
  757. continue;
  758. }
  759. $class = new \ReflectionClass($unit[0]);
  760. foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
  761. if ($class->isSubclassOf($whitelisted)) {
  762. unset($unintentionallyCoveredUnits[$k]);
  763. break;
  764. }
  765. }
  766. }
  767. return \array_values($unintentionallyCoveredUnits);
  768. }
  769. /**
  770. * @throws CoveredCodeNotExecutedException
  771. * @throws InvalidArgumentException
  772. * @throws MissingCoversAnnotationException
  773. * @throws RuntimeException
  774. * @throws UnintentionallyCoveredCodeException
  775. * @throws \ReflectionException
  776. */
  777. private function initializeData(): void
  778. {
  779. $this->isInitialized = true;
  780. if ($this->processUncoveredFilesFromWhitelist) {
  781. $this->shouldCheckForDeadAndUnused = false;
  782. $this->driver->start();
  783. foreach ($this->filter->getWhitelist() as $file) {
  784. if ($this->filter->isFile($file)) {
  785. include_once $file;
  786. }
  787. }
  788. $data = [];
  789. foreach ($this->driver->stop() as $file => $fileCoverage) {
  790. if ($this->filter->isFiltered($file)) {
  791. continue;
  792. }
  793. foreach (\array_keys($fileCoverage) as $key) {
  794. if ($fileCoverage[$key] === Driver::LINE_EXECUTED) {
  795. $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
  796. }
  797. }
  798. $data[$file] = $fileCoverage;
  799. }
  800. $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  801. }
  802. }
  803. private function coverageToCodeUnits(array $data): array
  804. {
  805. $codeUnits = [];
  806. foreach ($data as $filename => $lines) {
  807. foreach ($lines as $line => $flag) {
  808. if ($flag === 1) {
  809. $codeUnits[] = $this->wizard->lookup($filename, $line);
  810. }
  811. }
  812. }
  813. return \array_unique($codeUnits);
  814. }
  815. private function linesToCodeUnits(array $data): array
  816. {
  817. $codeUnits = [];
  818. foreach ($data as $filename => $lines) {
  819. foreach ($lines as $line) {
  820. $codeUnits[] = $this->wizard->lookup($filename, $line);
  821. }
  822. }
  823. return \array_unique($codeUnits);
  824. }
  825. }