CodeCoverage.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  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. 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\PHPDBG;
  16. use SebastianBergmann\CodeCoverage\Driver\Xdebug;
  17. use SebastianBergmann\CodeCoverage\Node\Builder;
  18. use SebastianBergmann\CodeCoverage\Node\Directory;
  19. use SebastianBergmann\CodeUnitReverseLookup\Wizard;
  20. use SebastianBergmann\Environment\Runtime;
  21. /**
  22. * Provides collection functionality for PHP code coverage information.
  23. */
  24. final class CodeCoverage
  25. {
  26. /**
  27. * @var Driver
  28. */
  29. private $driver;
  30. /**
  31. * @var Filter
  32. */
  33. private $filter;
  34. /**
  35. * @var Wizard
  36. */
  37. private $wizard;
  38. /**
  39. * @var bool
  40. */
  41. private $cacheTokens = false;
  42. /**
  43. * @var bool
  44. */
  45. private $checkForUnintentionallyCoveredCode = false;
  46. /**
  47. * @var bool
  48. */
  49. private $forceCoversAnnotation = false;
  50. /**
  51. * @var bool
  52. */
  53. private $checkForUnexecutedCoveredCode = false;
  54. /**
  55. * @var bool
  56. */
  57. private $checkForMissingCoversAnnotation = false;
  58. /**
  59. * @var bool
  60. */
  61. private $addUncoveredFilesFromWhitelist = true;
  62. /**
  63. * @var bool
  64. */
  65. private $processUncoveredFilesFromWhitelist = false;
  66. /**
  67. * @var bool
  68. */
  69. private $ignoreDeprecatedCode = false;
  70. /**
  71. * @var PhptTestCase|string|TestCase
  72. */
  73. private $currentId;
  74. /**
  75. * Code coverage data.
  76. *
  77. * @var array
  78. */
  79. private $data = [];
  80. /**
  81. * @var array
  82. */
  83. private $ignoredLines = [];
  84. /**
  85. * @var bool
  86. */
  87. private $disableIgnoredLines = false;
  88. /**
  89. * Test data.
  90. *
  91. * @var array
  92. */
  93. private $tests = [];
  94. /**
  95. * @var string[]
  96. */
  97. private $unintentionallyCoveredSubclassesWhitelist = [];
  98. /**
  99. * Determine if the data has been initialized or not
  100. *
  101. * @var bool
  102. */
  103. private $isInitialized = false;
  104. /**
  105. * Determine whether we need to check for dead and unused code on each test
  106. *
  107. * @var bool
  108. */
  109. private $shouldCheckForDeadAndUnused = true;
  110. /**
  111. * @var Directory
  112. */
  113. private $report;
  114. /**
  115. * @throws RuntimeException
  116. */
  117. public function __construct(Driver $driver = null, Filter $filter = null)
  118. {
  119. if ($filter === null) {
  120. $filter = new Filter;
  121. }
  122. if ($driver === null) {
  123. $driver = $this->selectDriver($filter);
  124. }
  125. $this->driver = $driver;
  126. $this->filter = $filter;
  127. $this->wizard = new Wizard;
  128. }
  129. /**
  130. * Returns the code coverage information as a graph of node objects.
  131. */
  132. public function getReport(): Directory
  133. {
  134. if ($this->report === null) {
  135. $builder = new Builder;
  136. $this->report = $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. $firstMethodEndLine = $firstMethod['endLine'];
  556. $lastMethodEndLine = $firstMethodEndLine;
  557. do {
  558. $lastMethod = \array_pop($classOrTrait['methods']);
  559. } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
  560. if ($lastMethod !== null) {
  561. $lastMethodEndLine = $lastMethod['endLine'];
  562. }
  563. foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
  564. $this->ignoredLines[$fileName][] = $line;
  565. }
  566. foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
  567. $this->ignoredLines[$fileName][] = $line;
  568. }
  569. }
  570. if ($this->disableIgnoredLines) {
  571. $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
  572. \sort($this->ignoredLines[$fileName]);
  573. return $this->ignoredLines[$fileName];
  574. }
  575. $ignore = false;
  576. $stop = false;
  577. foreach ($tokens->tokens() as $token) {
  578. switch (\get_class($token)) {
  579. case \PHP_Token_COMMENT::class:
  580. case \PHP_Token_DOC_COMMENT::class:
  581. $_token = \trim($token);
  582. $_line = \trim($lines[$token->getLine() - 1]);
  583. if ($_token === '// @codeCoverageIgnore' ||
  584. $_token === '//@codeCoverageIgnore') {
  585. $ignore = true;
  586. $stop = true;
  587. } elseif ($_token === '// @codeCoverageIgnoreStart' ||
  588. $_token === '//@codeCoverageIgnoreStart') {
  589. $ignore = true;
  590. } elseif ($_token === '// @codeCoverageIgnoreEnd' ||
  591. $_token === '//@codeCoverageIgnoreEnd') {
  592. $stop = true;
  593. }
  594. if (!$ignore) {
  595. $start = $token->getLine();
  596. $end = $start + \substr_count($token, "\n");
  597. // Do not ignore the first line when there is a token
  598. // before the comment
  599. if (0 !== \strpos($_token, $_line)) {
  600. $start++;
  601. }
  602. for ($i = $start; $i < $end; $i++) {
  603. $this->ignoredLines[$fileName][] = $i;
  604. }
  605. // A DOC_COMMENT token or a COMMENT token starting with "/*"
  606. // does not contain the final \n character in its text
  607. if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
  608. $this->ignoredLines[$fileName][] = $i;
  609. }
  610. }
  611. break;
  612. case \PHP_Token_INTERFACE::class:
  613. case \PHP_Token_TRAIT::class:
  614. case \PHP_Token_CLASS::class:
  615. case \PHP_Token_FUNCTION::class:
  616. /* @var \PHP_Token_Interface $token */
  617. $docblock = $token->getDocblock();
  618. $this->ignoredLines[$fileName][] = $token->getLine();
  619. if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
  620. $endLine = $token->getEndLine();
  621. for ($i = $token->getLine(); $i <= $endLine; $i++) {
  622. $this->ignoredLines[$fileName][] = $i;
  623. }
  624. }
  625. break;
  626. /* @noinspection PhpMissingBreakStatementInspection */
  627. case \PHP_Token_NAMESPACE::class:
  628. $this->ignoredLines[$fileName][] = $token->getEndLine();
  629. // Intentional fallthrough
  630. case \PHP_Token_DECLARE::class:
  631. case \PHP_Token_OPEN_TAG::class:
  632. case \PHP_Token_CLOSE_TAG::class:
  633. case \PHP_Token_USE::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->canCollectCodeCoverage()) {
  740. throw new RuntimeException('No code coverage driver available');
  741. }
  742. if ($runtime->isPHPDBG()) {
  743. return new PHPDBG;
  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. $coverage = $this->driver->stop();
  790. foreach ($coverage as $file => $fileCoverage) {
  791. if ($this->filter->isFiltered($file)) {
  792. continue;
  793. }
  794. foreach (\array_keys($fileCoverage) as $key) {
  795. if ($fileCoverage[$key] === Driver::LINE_EXECUTED) {
  796. $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
  797. }
  798. }
  799. $data[$file] = $fileCoverage;
  800. }
  801. $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  802. }
  803. }
  804. private function coverageToCodeUnits(array $data): array
  805. {
  806. $codeUnits = [];
  807. foreach ($data as $filename => $lines) {
  808. foreach ($lines as $line => $flag) {
  809. if ($flag === 1) {
  810. $codeUnits[] = $this->wizard->lookup($filename, $line);
  811. }
  812. }
  813. }
  814. return \array_unique($codeUnits);
  815. }
  816. private function linesToCodeUnits(array $data): array
  817. {
  818. $codeUnits = [];
  819. foreach ($data as $filename => $lines) {
  820. foreach ($lines as $line) {
  821. $codeUnits[] = $this->wizard->lookup($filename, $line);
  822. }
  823. }
  824. return \array_unique($codeUnits);
  825. }
  826. }