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