CodeCoverage.php 31 KB

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