CodeCoverage.php 29 KB

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