CodeCoverage.php 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159
  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 SebastianBergmann\CodeCoverage\Driver\Driver;
  14. use SebastianBergmann\CodeCoverage\Driver\Xdebug;
  15. use SebastianBergmann\CodeCoverage\Driver\HHVM;
  16. use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
  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. 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 mixed
  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. * Constructor.
  116. *
  117. * @param Driver $driver
  118. * @param Filter $filter
  119. *
  120. * @throws RuntimeException
  121. */
  122. public function __construct(Driver $driver = null, Filter $filter = null)
  123. {
  124. if ($driver === null) {
  125. $driver = $this->selectDriver();
  126. }
  127. if ($filter === null) {
  128. $filter = new Filter;
  129. }
  130. $this->driver = $driver;
  131. $this->filter = $filter;
  132. $this->wizard = new Wizard;
  133. }
  134. /**
  135. * Returns the code coverage information as a graph of node objects.
  136. *
  137. * @return Directory
  138. */
  139. public function getReport()
  140. {
  141. if ($this->report === null) {
  142. $builder = new Builder;
  143. $this->report = $builder->build($this);
  144. }
  145. return $this->report;
  146. }
  147. /**
  148. * Clears collected code coverage data.
  149. */
  150. public function clear()
  151. {
  152. $this->isInitialized = false;
  153. $this->currentId = null;
  154. $this->data = [];
  155. $this->tests = [];
  156. $this->report = null;
  157. }
  158. /**
  159. * Returns the filter object used.
  160. *
  161. * @return Filter
  162. */
  163. public function filter()
  164. {
  165. return $this->filter;
  166. }
  167. /**
  168. * Returns the collected code coverage data.
  169. * Set $raw = true to bypass all filters.
  170. *
  171. * @param bool $raw
  172. *
  173. * @return array
  174. */
  175. public function getData($raw = false)
  176. {
  177. if (!$raw && $this->addUncoveredFilesFromWhitelist) {
  178. $this->addUncoveredFilesFromWhitelist();
  179. }
  180. return $this->data;
  181. }
  182. /**
  183. * Sets the coverage data.
  184. *
  185. * @param array $data
  186. */
  187. public function setData(array $data)
  188. {
  189. $this->data = $data;
  190. $this->report = null;
  191. }
  192. /**
  193. * Returns the test data.
  194. *
  195. * @return array
  196. */
  197. public function getTests()
  198. {
  199. return $this->tests;
  200. }
  201. /**
  202. * Sets the test data.
  203. *
  204. * @param array $tests
  205. */
  206. public function setTests(array $tests)
  207. {
  208. $this->tests = $tests;
  209. }
  210. /**
  211. * Start collection of code coverage information.
  212. *
  213. * @param mixed $id
  214. * @param bool $clear
  215. *
  216. * @throws InvalidArgumentException
  217. */
  218. public function start($id, $clear = false)
  219. {
  220. if (!is_bool($clear)) {
  221. throw InvalidArgumentException::create(
  222. 1,
  223. 'boolean'
  224. );
  225. }
  226. if ($clear) {
  227. $this->clear();
  228. }
  229. if ($this->isInitialized === false) {
  230. $this->initializeData();
  231. }
  232. $this->currentId = $id;
  233. $this->driver->start($this->shouldCheckForDeadAndUnused);
  234. }
  235. /**
  236. * Stop collection of code coverage information.
  237. *
  238. * @param bool $append
  239. * @param mixed $linesToBeCovered
  240. * @param array $linesToBeUsed
  241. *
  242. * @return array
  243. *
  244. * @throws InvalidArgumentException
  245. */
  246. public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [])
  247. {
  248. if (!is_bool($append)) {
  249. throw InvalidArgumentException::create(
  250. 1,
  251. 'boolean'
  252. );
  253. }
  254. if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
  255. throw InvalidArgumentException::create(
  256. 2,
  257. 'array or false'
  258. );
  259. }
  260. $data = $this->driver->stop();
  261. $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
  262. $this->currentId = null;
  263. return $data;
  264. }
  265. /**
  266. * Appends code coverage data.
  267. *
  268. * @param array $data
  269. * @param mixed $id
  270. * @param bool $append
  271. * @param mixed $linesToBeCovered
  272. * @param array $linesToBeUsed
  273. *
  274. * @throws RuntimeException
  275. */
  276. public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [])
  277. {
  278. if ($id === null) {
  279. $id = $this->currentId;
  280. }
  281. if ($id === null) {
  282. throw new RuntimeException;
  283. }
  284. $this->applyListsFilter($data);
  285. $this->applyIgnoredLinesFilter($data);
  286. $this->initializeFilesThatAreSeenTheFirstTime($data);
  287. if (!$append) {
  288. return;
  289. }
  290. if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
  291. $this->applyCoversAnnotationFilter(
  292. $data,
  293. $linesToBeCovered,
  294. $linesToBeUsed
  295. );
  296. }
  297. if (empty($data)) {
  298. return;
  299. }
  300. $size = 'unknown';
  301. $status = null;
  302. if ($id instanceof TestCase) {
  303. $_size = $id->getSize();
  304. if ($_size == \PHPUnit\Util\Test::SMALL) {
  305. $size = 'small';
  306. } elseif ($_size == \PHPUnit\Util\Test::MEDIUM) {
  307. $size = 'medium';
  308. } elseif ($_size == \PHPUnit\Util\Test::LARGE) {
  309. $size = 'large';
  310. }
  311. $status = $id->getStatus();
  312. $id = get_class($id) . '::' . $id->getName();
  313. } elseif ($id instanceof PhptTestCase) {
  314. $size = 'large';
  315. $id = $id->getName();
  316. }
  317. $this->tests[$id] = ['size' => $size, 'status' => $status];
  318. foreach ($data as $file => $lines) {
  319. if (!$this->filter->isFile($file)) {
  320. continue;
  321. }
  322. foreach ($lines as $k => $v) {
  323. if ($v == Driver::LINE_EXECUTED) {
  324. if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) {
  325. $this->data[$file][$k][] = $id;
  326. }
  327. }
  328. }
  329. }
  330. $this->report = null;
  331. }
  332. /**
  333. * Merges the data from another instance.
  334. *
  335. * @param CodeCoverage $that
  336. */
  337. public function merge(CodeCoverage $that)
  338. {
  339. $this->filter->setWhitelistedFiles(
  340. array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
  341. );
  342. foreach ($that->data as $file => $lines) {
  343. if (!isset($this->data[$file])) {
  344. if (!$this->filter->isFiltered($file)) {
  345. $this->data[$file] = $lines;
  346. }
  347. continue;
  348. }
  349. foreach ($lines as $line => $data) {
  350. if ($data !== null) {
  351. if (!isset($this->data[$file][$line])) {
  352. $this->data[$file][$line] = $data;
  353. } else {
  354. $this->data[$file][$line] = array_unique(
  355. array_merge($this->data[$file][$line], $data)
  356. );
  357. }
  358. }
  359. }
  360. }
  361. $this->tests = array_merge($this->tests, $that->getTests());
  362. $this->report = null;
  363. }
  364. /**
  365. * @param bool $flag
  366. *
  367. * @throws InvalidArgumentException
  368. */
  369. public function setCacheTokens($flag)
  370. {
  371. if (!is_bool($flag)) {
  372. throw InvalidArgumentException::create(
  373. 1,
  374. 'boolean'
  375. );
  376. }
  377. $this->cacheTokens = $flag;
  378. }
  379. /**
  380. * @return bool
  381. */
  382. public function getCacheTokens()
  383. {
  384. return $this->cacheTokens;
  385. }
  386. /**
  387. * @param bool $flag
  388. *
  389. * @throws InvalidArgumentException
  390. */
  391. public function setCheckForUnintentionallyCoveredCode($flag)
  392. {
  393. if (!is_bool($flag)) {
  394. throw InvalidArgumentException::create(
  395. 1,
  396. 'boolean'
  397. );
  398. }
  399. $this->checkForUnintentionallyCoveredCode = $flag;
  400. }
  401. /**
  402. * @param bool $flag
  403. *
  404. * @throws InvalidArgumentException
  405. */
  406. public function setForceCoversAnnotation($flag)
  407. {
  408. if (!is_bool($flag)) {
  409. throw InvalidArgumentException::create(
  410. 1,
  411. 'boolean'
  412. );
  413. }
  414. $this->forceCoversAnnotation = $flag;
  415. }
  416. /**
  417. * @param bool $flag
  418. *
  419. * @throws InvalidArgumentException
  420. */
  421. public function setCheckForMissingCoversAnnotation($flag)
  422. {
  423. if (!is_bool($flag)) {
  424. throw InvalidArgumentException::create(
  425. 1,
  426. 'boolean'
  427. );
  428. }
  429. $this->checkForMissingCoversAnnotation = $flag;
  430. }
  431. /**
  432. * @param bool $flag
  433. *
  434. * @throws InvalidArgumentException
  435. */
  436. public function setCheckForUnexecutedCoveredCode($flag)
  437. {
  438. if (!is_bool($flag)) {
  439. throw InvalidArgumentException::create(
  440. 1,
  441. 'boolean'
  442. );
  443. }
  444. $this->checkForUnexecutedCoveredCode = $flag;
  445. }
  446. /**
  447. * @deprecated
  448. *
  449. * @param bool $flag
  450. *
  451. * @throws InvalidArgumentException
  452. */
  453. public function setMapTestClassNameToCoveredClassName($flag)
  454. {
  455. }
  456. /**
  457. * @param bool $flag
  458. *
  459. * @throws InvalidArgumentException
  460. */
  461. public function setAddUncoveredFilesFromWhitelist($flag)
  462. {
  463. if (!is_bool($flag)) {
  464. throw InvalidArgumentException::create(
  465. 1,
  466. 'boolean'
  467. );
  468. }
  469. $this->addUncoveredFilesFromWhitelist = $flag;
  470. }
  471. /**
  472. * @param bool $flag
  473. *
  474. * @throws InvalidArgumentException
  475. */
  476. public function setProcessUncoveredFilesFromWhitelist($flag)
  477. {
  478. if (!is_bool($flag)) {
  479. throw InvalidArgumentException::create(
  480. 1,
  481. 'boolean'
  482. );
  483. }
  484. $this->processUncoveredFilesFromWhitelist = $flag;
  485. }
  486. /**
  487. * @param bool $flag
  488. *
  489. * @throws InvalidArgumentException
  490. */
  491. public function setDisableIgnoredLines($flag)
  492. {
  493. if (!is_bool($flag)) {
  494. throw InvalidArgumentException::create(
  495. 1,
  496. 'boolean'
  497. );
  498. }
  499. $this->disableIgnoredLines = $flag;
  500. }
  501. /**
  502. * @param bool $flag
  503. *
  504. * @throws InvalidArgumentException
  505. */
  506. public function setIgnoreDeprecatedCode($flag)
  507. {
  508. if (!is_bool($flag)) {
  509. throw InvalidArgumentException::create(
  510. 1,
  511. 'boolean'
  512. );
  513. }
  514. $this->ignoreDeprecatedCode = $flag;
  515. }
  516. /**
  517. * @param array $whitelist
  518. */
  519. public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist)
  520. {
  521. $this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
  522. }
  523. /**
  524. * Applies the @covers annotation filtering.
  525. *
  526. * @param array $data
  527. * @param mixed $linesToBeCovered
  528. * @param array $linesToBeUsed
  529. *
  530. * @throws MissingCoversAnnotationException
  531. * @throws UnintentionallyCoveredCodeException
  532. */
  533. private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed)
  534. {
  535. if ($linesToBeCovered === false ||
  536. ($this->forceCoversAnnotation && empty($linesToBeCovered))) {
  537. if ($this->checkForMissingCoversAnnotation) {
  538. throw new MissingCoversAnnotationException;
  539. }
  540. $data = [];
  541. return;
  542. }
  543. if (empty($linesToBeCovered)) {
  544. return;
  545. }
  546. if ($this->checkForUnintentionallyCoveredCode &&
  547. (!$this->currentId instanceof TestCase ||
  548. (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
  549. $this->performUnintentionallyCoveredCodeCheck(
  550. $data,
  551. $linesToBeCovered,
  552. $linesToBeUsed
  553. );
  554. }
  555. if ($this->checkForUnexecutedCoveredCode) {
  556. $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
  557. }
  558. $data = array_intersect_key($data, $linesToBeCovered);
  559. foreach (array_keys($data) as $filename) {
  560. $_linesToBeCovered = array_flip($linesToBeCovered[$filename]);
  561. $data[$filename] = array_intersect_key(
  562. $data[$filename],
  563. $_linesToBeCovered
  564. );
  565. }
  566. }
  567. /**
  568. * Applies the whitelist filtering.
  569. *
  570. * @param array $data
  571. */
  572. private function applyListsFilter(array &$data)
  573. {
  574. foreach (array_keys($data) as $filename) {
  575. if ($this->filter->isFiltered($filename)) {
  576. unset($data[$filename]);
  577. }
  578. }
  579. }
  580. /**
  581. * Applies the "ignored lines" filtering.
  582. *
  583. * @param array $data
  584. */
  585. private function applyIgnoredLinesFilter(array &$data)
  586. {
  587. foreach (array_keys($data) as $filename) {
  588. if (!$this->filter->isFile($filename)) {
  589. continue;
  590. }
  591. foreach ($this->getLinesToBeIgnored($filename) as $line) {
  592. unset($data[$filename][$line]);
  593. }
  594. }
  595. }
  596. /**
  597. * @param array $data
  598. */
  599. private function initializeFilesThatAreSeenTheFirstTime(array $data)
  600. {
  601. foreach ($data as $file => $lines) {
  602. if ($this->filter->isFile($file) && !isset($this->data[$file])) {
  603. $this->data[$file] = [];
  604. foreach ($lines as $k => $v) {
  605. $this->data[$file][$k] = $v == -2 ? null : [];
  606. }
  607. }
  608. }
  609. }
  610. /**
  611. * Processes whitelisted files that are not covered.
  612. */
  613. private function addUncoveredFilesFromWhitelist()
  614. {
  615. $data = [];
  616. $uncoveredFiles = array_diff(
  617. $this->filter->getWhitelist(),
  618. array_keys($this->data)
  619. );
  620. foreach ($uncoveredFiles as $uncoveredFile) {
  621. if (!file_exists($uncoveredFile)) {
  622. continue;
  623. }
  624. if (!$this->processUncoveredFilesFromWhitelist) {
  625. $data[$uncoveredFile] = [];
  626. $lines = count(file($uncoveredFile));
  627. for ($i = 1; $i <= $lines; $i++) {
  628. $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
  629. }
  630. }
  631. }
  632. $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  633. }
  634. /**
  635. * Returns the lines of a source file that should be ignored.
  636. *
  637. * @param string $filename
  638. *
  639. * @return array
  640. *
  641. * @throws InvalidArgumentException
  642. */
  643. private function getLinesToBeIgnored($filename)
  644. {
  645. if (!is_string($filename)) {
  646. throw InvalidArgumentException::create(
  647. 1,
  648. 'string'
  649. );
  650. }
  651. if (!isset($this->ignoredLines[$filename])) {
  652. $this->ignoredLines[$filename] = [];
  653. if ($this->disableIgnoredLines) {
  654. return $this->ignoredLines[$filename];
  655. }
  656. $ignore = false;
  657. $stop = false;
  658. $lines = file($filename);
  659. $numLines = count($lines);
  660. foreach ($lines as $index => $line) {
  661. if (!trim($line)) {
  662. $this->ignoredLines[$filename][] = $index + 1;
  663. }
  664. }
  665. if ($this->cacheTokens) {
  666. $tokens = \PHP_Token_Stream_CachingFactory::get($filename);
  667. } else {
  668. $tokens = new \PHP_Token_Stream($filename);
  669. }
  670. $classes = array_merge($tokens->getClasses(), $tokens->getTraits());
  671. $tokens = $tokens->tokens();
  672. foreach ($tokens as $token) {
  673. switch (get_class($token)) {
  674. case \PHP_Token_COMMENT::class:
  675. case \PHP_Token_DOC_COMMENT::class:
  676. $_token = trim($token);
  677. $_line = trim($lines[$token->getLine() - 1]);
  678. if ($_token == '// @codeCoverageIgnore' ||
  679. $_token == '//@codeCoverageIgnore') {
  680. $ignore = true;
  681. $stop = true;
  682. } elseif ($_token == '// @codeCoverageIgnoreStart' ||
  683. $_token == '//@codeCoverageIgnoreStart') {
  684. $ignore = true;
  685. } elseif ($_token == '// @codeCoverageIgnoreEnd' ||
  686. $_token == '//@codeCoverageIgnoreEnd') {
  687. $stop = true;
  688. }
  689. if (!$ignore) {
  690. $start = $token->getLine();
  691. $end = $start + substr_count($token, "\n");
  692. // Do not ignore the first line when there is a token
  693. // before the comment
  694. if (0 !== strpos($_token, $_line)) {
  695. $start++;
  696. }
  697. for ($i = $start; $i < $end; $i++) {
  698. $this->ignoredLines[$filename][] = $i;
  699. }
  700. // A DOC_COMMENT token or a COMMENT token starting with "/*"
  701. // does not contain the final \n character in its text
  702. if (isset($lines[$i - 1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i - 1]), -2)) {
  703. $this->ignoredLines[$filename][] = $i;
  704. }
  705. }
  706. break;
  707. case \PHP_Token_INTERFACE::class:
  708. case \PHP_Token_TRAIT::class:
  709. case \PHP_Token_CLASS::class:
  710. case \PHP_Token_FUNCTION::class:
  711. /* @var \PHP_Token_Interface $token */
  712. $docblock = $token->getDocblock();
  713. $this->ignoredLines[$filename][] = $token->getLine();
  714. if (strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && strpos($docblock, '@deprecated'))) {
  715. $endLine = $token->getEndLine();
  716. for ($i = $token->getLine(); $i <= $endLine; $i++) {
  717. $this->ignoredLines[$filename][] = $i;
  718. }
  719. } elseif ($token instanceof \PHP_Token_INTERFACE ||
  720. $token instanceof \PHP_Token_TRAIT ||
  721. $token instanceof \PHP_Token_CLASS) {
  722. if (empty($classes[$token->getName()]['methods'])) {
  723. for ($i = $token->getLine();
  724. $i <= $token->getEndLine();
  725. $i++) {
  726. $this->ignoredLines[$filename][] = $i;
  727. }
  728. } else {
  729. $firstMethod = array_shift(
  730. $classes[$token->getName()]['methods']
  731. );
  732. do {
  733. $lastMethod = array_pop(
  734. $classes[$token->getName()]['methods']
  735. );
  736. } while ($lastMethod !== null &&
  737. substr($lastMethod['signature'], 0, 18) == 'anonymous function');
  738. if ($lastMethod === null) {
  739. $lastMethod = $firstMethod;
  740. }
  741. for ($i = $token->getLine();
  742. $i < $firstMethod['startLine'];
  743. $i++) {
  744. $this->ignoredLines[$filename][] = $i;
  745. }
  746. for ($i = $token->getEndLine();
  747. $i > $lastMethod['endLine'];
  748. $i--) {
  749. $this->ignoredLines[$filename][] = $i;
  750. }
  751. }
  752. }
  753. break;
  754. case \PHP_Token_ENUM::class:
  755. $this->ignoredLines[$filename][] = $token->getLine();
  756. break;
  757. case \PHP_Token_NAMESPACE::class:
  758. $this->ignoredLines[$filename][] = $token->getEndLine();
  759. // Intentional fallthrough
  760. case \PHP_Token_DECLARE::class:
  761. case \PHP_Token_OPEN_TAG::class:
  762. case \PHP_Token_CLOSE_TAG::class:
  763. case \PHP_Token_USE::class:
  764. $this->ignoredLines[$filename][] = $token->getLine();
  765. break;
  766. }
  767. if ($ignore) {
  768. $this->ignoredLines[$filename][] = $token->getLine();
  769. if ($stop) {
  770. $ignore = false;
  771. $stop = false;
  772. }
  773. }
  774. }
  775. $this->ignoredLines[$filename][] = $numLines + 1;
  776. $this->ignoredLines[$filename] = array_unique(
  777. $this->ignoredLines[$filename]
  778. );
  779. sort($this->ignoredLines[$filename]);
  780. }
  781. return $this->ignoredLines[$filename];
  782. }
  783. /**
  784. * @param array $data
  785. * @param array $linesToBeCovered
  786. * @param array $linesToBeUsed
  787. *
  788. * @throws UnintentionallyCoveredCodeException
  789. */
  790. private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
  791. {
  792. $allowedLines = $this->getAllowedLines(
  793. $linesToBeCovered,
  794. $linesToBeUsed
  795. );
  796. $unintentionallyCoveredUnits = [];
  797. foreach ($data as $file => $_data) {
  798. foreach ($_data as $line => $flag) {
  799. if ($flag == 1 && !isset($allowedLines[$file][$line])) {
  800. $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
  801. }
  802. }
  803. }
  804. $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
  805. if (!empty($unintentionallyCoveredUnits)) {
  806. throw new UnintentionallyCoveredCodeException(
  807. $unintentionallyCoveredUnits
  808. );
  809. }
  810. }
  811. /**
  812. * @param array $data
  813. * @param array $linesToBeCovered
  814. * @param array $linesToBeUsed
  815. *
  816. * @throws CoveredCodeNotExecutedException
  817. */
  818. private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
  819. {
  820. $executedCodeUnits = $this->coverageToCodeUnits($data);
  821. $message = '';
  822. foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
  823. if (!in_array($codeUnit, $executedCodeUnits)) {
  824. $message .= sprintf(
  825. '- %s is expected to be executed (@covers) but was not executed' . "\n",
  826. $codeUnit
  827. );
  828. }
  829. }
  830. foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
  831. if (!in_array($codeUnit, $executedCodeUnits)) {
  832. $message .= sprintf(
  833. '- %s is expected to be executed (@uses) but was not executed' . "\n",
  834. $codeUnit
  835. );
  836. }
  837. }
  838. if (!empty($message)) {
  839. throw new CoveredCodeNotExecutedException($message);
  840. }
  841. }
  842. /**
  843. * @param array $linesToBeCovered
  844. * @param array $linesToBeUsed
  845. *
  846. * @return array
  847. */
  848. private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
  849. {
  850. $allowedLines = [];
  851. foreach (array_keys($linesToBeCovered) as $file) {
  852. if (!isset($allowedLines[$file])) {
  853. $allowedLines[$file] = [];
  854. }
  855. $allowedLines[$file] = array_merge(
  856. $allowedLines[$file],
  857. $linesToBeCovered[$file]
  858. );
  859. }
  860. foreach (array_keys($linesToBeUsed) as $file) {
  861. if (!isset($allowedLines[$file])) {
  862. $allowedLines[$file] = [];
  863. }
  864. $allowedLines[$file] = array_merge(
  865. $allowedLines[$file],
  866. $linesToBeUsed[$file]
  867. );
  868. }
  869. foreach (array_keys($allowedLines) as $file) {
  870. $allowedLines[$file] = array_flip(
  871. array_unique($allowedLines[$file])
  872. );
  873. }
  874. return $allowedLines;
  875. }
  876. /**
  877. * @return Driver
  878. *
  879. * @throws RuntimeException
  880. */
  881. private function selectDriver()
  882. {
  883. $runtime = new Runtime;
  884. if (!$runtime->canCollectCodeCoverage()) {
  885. throw new RuntimeException('No code coverage driver available');
  886. }
  887. if ($runtime->isHHVM()) {
  888. return new HHVM;
  889. } elseif ($runtime->isPHPDBG()) {
  890. return new PHPDBG;
  891. } else {
  892. return new Xdebug;
  893. }
  894. }
  895. /**
  896. * @param array $unintentionallyCoveredUnits
  897. *
  898. * @return array
  899. */
  900. private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits)
  901. {
  902. $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
  903. sort($unintentionallyCoveredUnits);
  904. foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
  905. $unit = explode('::', $unintentionallyCoveredUnits[$k]);
  906. if (count($unit) != 2) {
  907. continue;
  908. }
  909. $class = new \ReflectionClass($unit[0]);
  910. foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
  911. if ($class->isSubclassOf($whitelisted)) {
  912. unset($unintentionallyCoveredUnits[$k]);
  913. break;
  914. }
  915. }
  916. }
  917. return array_values($unintentionallyCoveredUnits);
  918. }
  919. /**
  920. * If we are processing uncovered files from whitelist,
  921. * we can initialize the data before we start to speed up the tests
  922. */
  923. protected function initializeData()
  924. {
  925. $this->isInitialized = true;
  926. if ($this->processUncoveredFilesFromWhitelist) {
  927. $this->shouldCheckForDeadAndUnused = false;
  928. $this->driver->start(true);
  929. foreach ($this->filter->getWhitelist() as $file) {
  930. if ($this->filter->isFile($file)) {
  931. include_once($file);
  932. }
  933. }
  934. $data = [];
  935. $coverage = $this->driver->stop();
  936. foreach ($coverage as $file => $fileCoverage) {
  937. if ($this->filter->isFiltered($file)) {
  938. continue;
  939. }
  940. foreach (array_keys($fileCoverage) as $key) {
  941. if ($fileCoverage[$key] == Driver::LINE_EXECUTED) {
  942. $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
  943. }
  944. }
  945. $data[$file] = $fileCoverage;
  946. }
  947. $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  948. }
  949. }
  950. /**
  951. * @param array $data
  952. *
  953. * @return array
  954. */
  955. private function coverageToCodeUnits(array $data)
  956. {
  957. $codeUnits = [];
  958. foreach ($data as $filename => $lines) {
  959. foreach ($lines as $line => $flag) {
  960. if ($flag == 1) {
  961. $codeUnits[] = $this->wizard->lookup($filename, $line);
  962. }
  963. }
  964. }
  965. return array_unique($codeUnits);
  966. }
  967. /**
  968. * @param array $data
  969. *
  970. * @return array
  971. */
  972. private function linesToCodeUnits(array $data)
  973. {
  974. $codeUnits = [];
  975. foreach ($data as $filename => $lines) {
  976. foreach ($lines as $line) {
  977. $codeUnits[] = $this->wizard->lookup($filename, $line);
  978. }
  979. }
  980. return array_unique($codeUnits);
  981. }
  982. }