annotate vendor/phpunit/php-code-coverage/src/CodeCoverage.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 1fec387a4317
children
rev   line source
Chris@0 1 <?php
Chris@0 2 /*
Chris@14 3 * This file is part of the php-code-coverage package.
Chris@0 4 *
Chris@0 5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
Chris@0 6 *
Chris@0 7 * For the full copyright and license information, please view the LICENSE
Chris@0 8 * file that was distributed with this source code.
Chris@0 9 */
Chris@0 10
Chris@14 11 namespace SebastianBergmann\CodeCoverage;
Chris@14 12
Chris@14 13 use PHPUnit\Framework\TestCase;
Chris@14 14 use PHPUnit\Runner\PhptTestCase;
Chris@14 15 use SebastianBergmann\CodeCoverage\Driver\Driver;
Chris@14 16 use SebastianBergmann\CodeCoverage\Driver\HHVM;
Chris@14 17 use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
Chris@14 18 use SebastianBergmann\CodeCoverage\Driver\Xdebug;
Chris@14 19 use SebastianBergmann\CodeCoverage\Node\Builder;
Chris@14 20 use SebastianBergmann\CodeCoverage\Node\Directory;
Chris@14 21 use SebastianBergmann\CodeUnitReverseLookup\Wizard;
Chris@0 22 use SebastianBergmann\Environment\Runtime;
Chris@0 23
Chris@0 24 /**
Chris@0 25 * Provides collection functionality for PHP code coverage information.
Chris@0 26 */
Chris@14 27 class CodeCoverage
Chris@0 28 {
Chris@0 29 /**
Chris@14 30 * @var Driver
Chris@0 31 */
Chris@0 32 private $driver;
Chris@0 33
Chris@0 34 /**
Chris@14 35 * @var Filter
Chris@0 36 */
Chris@0 37 private $filter;
Chris@0 38
Chris@0 39 /**
Chris@14 40 * @var Wizard
Chris@14 41 */
Chris@14 42 private $wizard;
Chris@14 43
Chris@14 44 /**
Chris@0 45 * @var bool
Chris@0 46 */
Chris@0 47 private $cacheTokens = false;
Chris@0 48
Chris@0 49 /**
Chris@0 50 * @var bool
Chris@0 51 */
Chris@0 52 private $checkForUnintentionallyCoveredCode = false;
Chris@0 53
Chris@0 54 /**
Chris@0 55 * @var bool
Chris@0 56 */
Chris@0 57 private $forceCoversAnnotation = false;
Chris@0 58
Chris@0 59 /**
Chris@0 60 * @var bool
Chris@0 61 */
Chris@14 62 private $checkForUnexecutedCoveredCode = false;
Chris@14 63
Chris@14 64 /**
Chris@14 65 * @var bool
Chris@14 66 */
Chris@14 67 private $checkForMissingCoversAnnotation = false;
Chris@0 68
Chris@0 69 /**
Chris@0 70 * @var bool
Chris@0 71 */
Chris@0 72 private $addUncoveredFilesFromWhitelist = true;
Chris@0 73
Chris@0 74 /**
Chris@0 75 * @var bool
Chris@0 76 */
Chris@0 77 private $processUncoveredFilesFromWhitelist = false;
Chris@0 78
Chris@0 79 /**
Chris@14 80 * @var bool
Chris@14 81 */
Chris@14 82 private $ignoreDeprecatedCode = false;
Chris@14 83
Chris@14 84 /**
Chris@0 85 * @var mixed
Chris@0 86 */
Chris@0 87 private $currentId;
Chris@0 88
Chris@0 89 /**
Chris@0 90 * Code coverage data.
Chris@0 91 *
Chris@0 92 * @var array
Chris@0 93 */
Chris@14 94 private $data = [];
Chris@0 95
Chris@0 96 /**
Chris@0 97 * @var array
Chris@0 98 */
Chris@14 99 private $ignoredLines = [];
Chris@0 100
Chris@0 101 /**
Chris@0 102 * @var bool
Chris@0 103 */
Chris@0 104 private $disableIgnoredLines = false;
Chris@0 105
Chris@0 106 /**
Chris@0 107 * Test data.
Chris@0 108 *
Chris@0 109 * @var array
Chris@0 110 */
Chris@14 111 private $tests = [];
Chris@14 112
Chris@14 113 /**
Chris@14 114 * @var string[]
Chris@14 115 */
Chris@14 116 private $unintentionallyCoveredSubclassesWhitelist = [];
Chris@14 117
Chris@14 118 /**
Chris@14 119 * Determine if the data has been initialized or not
Chris@14 120 *
Chris@14 121 * @var bool
Chris@14 122 */
Chris@14 123 private $isInitialized = false;
Chris@14 124
Chris@14 125 /**
Chris@14 126 * Determine whether we need to check for dead and unused code on each test
Chris@14 127 *
Chris@14 128 * @var bool
Chris@14 129 */
Chris@14 130 private $shouldCheckForDeadAndUnused = true;
Chris@14 131
Chris@14 132 /**
Chris@14 133 * @var Directory
Chris@14 134 */
Chris@14 135 private $report;
Chris@0 136
Chris@0 137 /**
Chris@0 138 * Constructor.
Chris@0 139 *
Chris@14 140 * @param Driver $driver
Chris@14 141 * @param Filter $filter
Chris@14 142 *
Chris@14 143 * @throws RuntimeException
Chris@0 144 */
Chris@14 145 public function __construct(Driver $driver = null, Filter $filter = null)
Chris@0 146 {
Chris@0 147 if ($driver === null) {
Chris@0 148 $driver = $this->selectDriver();
Chris@0 149 }
Chris@0 150
Chris@0 151 if ($filter === null) {
Chris@14 152 $filter = new Filter;
Chris@0 153 }
Chris@0 154
Chris@0 155 $this->driver = $driver;
Chris@0 156 $this->filter = $filter;
Chris@14 157
Chris@14 158 $this->wizard = new Wizard;
Chris@0 159 }
Chris@0 160
Chris@0 161 /**
Chris@14 162 * Returns the code coverage information as a graph of node objects.
Chris@0 163 *
Chris@14 164 * @return Directory
Chris@0 165 */
Chris@0 166 public function getReport()
Chris@0 167 {
Chris@14 168 if ($this->report === null) {
Chris@14 169 $builder = new Builder;
Chris@0 170
Chris@14 171 $this->report = $builder->build($this);
Chris@14 172 }
Chris@14 173
Chris@14 174 return $this->report;
Chris@0 175 }
Chris@0 176
Chris@0 177 /**
Chris@0 178 * Clears collected code coverage data.
Chris@0 179 */
Chris@0 180 public function clear()
Chris@0 181 {
Chris@14 182 $this->isInitialized = false;
Chris@14 183 $this->currentId = null;
Chris@14 184 $this->data = [];
Chris@14 185 $this->tests = [];
Chris@14 186 $this->report = null;
Chris@0 187 }
Chris@0 188
Chris@0 189 /**
Chris@14 190 * Returns the filter object used.
Chris@0 191 *
Chris@14 192 * @return Filter
Chris@0 193 */
Chris@0 194 public function filter()
Chris@0 195 {
Chris@0 196 return $this->filter;
Chris@0 197 }
Chris@0 198
Chris@0 199 /**
Chris@0 200 * Returns the collected code coverage data.
Chris@0 201 * Set $raw = true to bypass all filters.
Chris@0 202 *
Chris@14 203 * @param bool $raw
Chris@14 204 *
Chris@0 205 * @return array
Chris@0 206 */
Chris@0 207 public function getData($raw = false)
Chris@0 208 {
Chris@0 209 if (!$raw && $this->addUncoveredFilesFromWhitelist) {
Chris@0 210 $this->addUncoveredFilesFromWhitelist();
Chris@0 211 }
Chris@0 212
Chris@0 213 return $this->data;
Chris@0 214 }
Chris@0 215
Chris@0 216 /**
Chris@0 217 * Sets the coverage data.
Chris@0 218 *
Chris@0 219 * @param array $data
Chris@0 220 */
Chris@0 221 public function setData(array $data)
Chris@0 222 {
Chris@14 223 $this->data = $data;
Chris@14 224 $this->report = null;
Chris@0 225 }
Chris@0 226
Chris@0 227 /**
Chris@0 228 * Returns the test data.
Chris@0 229 *
Chris@0 230 * @return array
Chris@0 231 */
Chris@0 232 public function getTests()
Chris@0 233 {
Chris@0 234 return $this->tests;
Chris@0 235 }
Chris@0 236
Chris@0 237 /**
Chris@0 238 * Sets the test data.
Chris@0 239 *
Chris@0 240 * @param array $tests
Chris@0 241 */
Chris@0 242 public function setTests(array $tests)
Chris@0 243 {
Chris@0 244 $this->tests = $tests;
Chris@0 245 }
Chris@0 246
Chris@0 247 /**
Chris@0 248 * Start collection of code coverage information.
Chris@0 249 *
Chris@14 250 * @param mixed $id
Chris@14 251 * @param bool $clear
Chris@14 252 *
Chris@14 253 * @throws InvalidArgumentException
Chris@0 254 */
Chris@0 255 public function start($id, $clear = false)
Chris@0 256 {
Chris@14 257 if (!\is_bool($clear)) {
Chris@14 258 throw InvalidArgumentException::create(
Chris@0 259 1,
Chris@0 260 'boolean'
Chris@0 261 );
Chris@0 262 }
Chris@0 263
Chris@0 264 if ($clear) {
Chris@0 265 $this->clear();
Chris@0 266 }
Chris@0 267
Chris@14 268 if ($this->isInitialized === false) {
Chris@14 269 $this->initializeData();
Chris@14 270 }
Chris@14 271
Chris@0 272 $this->currentId = $id;
Chris@0 273
Chris@14 274 $this->driver->start($this->shouldCheckForDeadAndUnused);
Chris@0 275 }
Chris@0 276
Chris@0 277 /**
Chris@0 278 * Stop collection of code coverage information.
Chris@0 279 *
Chris@14 280 * @param bool $append
Chris@14 281 * @param mixed $linesToBeCovered
Chris@14 282 * @param array $linesToBeUsed
Chris@14 283 * @param bool $ignoreForceCoversAnnotation
Chris@14 284 *
Chris@0 285 * @return array
Chris@14 286 *
Chris@14 287 * @throws \SebastianBergmann\CodeCoverage\RuntimeException
Chris@14 288 * @throws InvalidArgumentException
Chris@0 289 */
Chris@14 290 public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false)
Chris@0 291 {
Chris@14 292 if (!\is_bool($append)) {
Chris@14 293 throw InvalidArgumentException::create(
Chris@0 294 1,
Chris@0 295 'boolean'
Chris@0 296 );
Chris@0 297 }
Chris@0 298
Chris@14 299 if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) {
Chris@14 300 throw InvalidArgumentException::create(
Chris@0 301 2,
Chris@0 302 'array or false'
Chris@0 303 );
Chris@0 304 }
Chris@0 305
Chris@0 306 $data = $this->driver->stop();
Chris@14 307 $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation);
Chris@0 308
Chris@0 309 $this->currentId = null;
Chris@0 310
Chris@0 311 return $data;
Chris@0 312 }
Chris@0 313
Chris@0 314 /**
Chris@0 315 * Appends code coverage data.
Chris@0 316 *
Chris@14 317 * @param array $data
Chris@14 318 * @param mixed $id
Chris@14 319 * @param bool $append
Chris@14 320 * @param mixed $linesToBeCovered
Chris@14 321 * @param array $linesToBeUsed
Chris@14 322 * @param bool $ignoreForceCoversAnnotation
Chris@14 323 *
Chris@14 324 * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
Chris@14 325 * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException
Chris@14 326 * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
Chris@14 327 * @throws \ReflectionException
Chris@14 328 * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
Chris@14 329 * @throws RuntimeException
Chris@0 330 */
Chris@14 331 public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false)
Chris@0 332 {
Chris@0 333 if ($id === null) {
Chris@0 334 $id = $this->currentId;
Chris@0 335 }
Chris@0 336
Chris@0 337 if ($id === null) {
Chris@14 338 throw new RuntimeException;
Chris@0 339 }
Chris@0 340
Chris@0 341 $this->applyListsFilter($data);
Chris@0 342 $this->applyIgnoredLinesFilter($data);
Chris@0 343 $this->initializeFilesThatAreSeenTheFirstTime($data);
Chris@0 344
Chris@0 345 if (!$append) {
Chris@0 346 return;
Chris@0 347 }
Chris@0 348
Chris@14 349 if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') {
Chris@0 350 $this->applyCoversAnnotationFilter(
Chris@0 351 $data,
Chris@0 352 $linesToBeCovered,
Chris@14 353 $linesToBeUsed,
Chris@14 354 $ignoreForceCoversAnnotation
Chris@0 355 );
Chris@0 356 }
Chris@0 357
Chris@0 358 if (empty($data)) {
Chris@0 359 return;
Chris@0 360 }
Chris@0 361
Chris@0 362 $size = 'unknown';
Chris@0 363 $status = null;
Chris@0 364
Chris@14 365 if ($id instanceof TestCase) {
Chris@0 366 $_size = $id->getSize();
Chris@0 367
Chris@14 368 if ($_size === \PHPUnit\Util\Test::SMALL) {
Chris@0 369 $size = 'small';
Chris@14 370 } elseif ($_size === \PHPUnit\Util\Test::MEDIUM) {
Chris@0 371 $size = 'medium';
Chris@14 372 } elseif ($_size === \PHPUnit\Util\Test::LARGE) {
Chris@0 373 $size = 'large';
Chris@0 374 }
Chris@0 375
Chris@0 376 $status = $id->getStatus();
Chris@14 377 $id = \get_class($id) . '::' . $id->getName();
Chris@14 378 } elseif ($id instanceof PhptTestCase) {
Chris@0 379 $size = 'large';
Chris@0 380 $id = $id->getName();
Chris@0 381 }
Chris@0 382
Chris@14 383 $this->tests[$id] = ['size' => $size, 'status' => $status];
Chris@0 384
Chris@0 385 foreach ($data as $file => $lines) {
Chris@0 386 if (!$this->filter->isFile($file)) {
Chris@0 387 continue;
Chris@0 388 }
Chris@0 389
Chris@0 390 foreach ($lines as $k => $v) {
Chris@14 391 if ($v === Driver::LINE_EXECUTED) {
Chris@14 392 if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) {
Chris@0 393 $this->data[$file][$k][] = $id;
Chris@0 394 }
Chris@0 395 }
Chris@0 396 }
Chris@0 397 }
Chris@14 398
Chris@14 399 $this->report = null;
Chris@0 400 }
Chris@0 401
Chris@0 402 /**
Chris@14 403 * Merges the data from another instance.
Chris@0 404 *
Chris@14 405 * @param CodeCoverage $that
Chris@0 406 */
Chris@14 407 public function merge(self $that)
Chris@0 408 {
Chris@0 409 $this->filter->setWhitelistedFiles(
Chris@14 410 \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
Chris@0 411 );
Chris@0 412
Chris@0 413 foreach ($that->data as $file => $lines) {
Chris@0 414 if (!isset($this->data[$file])) {
Chris@0 415 if (!$this->filter->isFiltered($file)) {
Chris@0 416 $this->data[$file] = $lines;
Chris@0 417 }
Chris@0 418
Chris@0 419 continue;
Chris@0 420 }
Chris@0 421
Chris@0 422 foreach ($lines as $line => $data) {
Chris@0 423 if ($data !== null) {
Chris@0 424 if (!isset($this->data[$file][$line])) {
Chris@0 425 $this->data[$file][$line] = $data;
Chris@0 426 } else {
Chris@14 427 $this->data[$file][$line] = \array_unique(
Chris@14 428 \array_merge($this->data[$file][$line], $data)
Chris@0 429 );
Chris@0 430 }
Chris@0 431 }
Chris@0 432 }
Chris@0 433 }
Chris@0 434
Chris@14 435 $this->tests = \array_merge($this->tests, $that->getTests());
Chris@14 436 $this->report = null;
Chris@0 437 }
Chris@0 438
Chris@0 439 /**
Chris@14 440 * @param bool $flag
Chris@14 441 *
Chris@14 442 * @throws InvalidArgumentException
Chris@0 443 */
Chris@0 444 public function setCacheTokens($flag)
Chris@0 445 {
Chris@14 446 if (!\is_bool($flag)) {
Chris@14 447 throw InvalidArgumentException::create(
Chris@0 448 1,
Chris@0 449 'boolean'
Chris@0 450 );
Chris@0 451 }
Chris@0 452
Chris@0 453 $this->cacheTokens = $flag;
Chris@0 454 }
Chris@0 455
Chris@0 456 /**
Chris@14 457 * @return bool
Chris@0 458 */
Chris@0 459 public function getCacheTokens()
Chris@0 460 {
Chris@0 461 return $this->cacheTokens;
Chris@0 462 }
Chris@0 463
Chris@0 464 /**
Chris@14 465 * @param bool $flag
Chris@14 466 *
Chris@14 467 * @throws InvalidArgumentException
Chris@0 468 */
Chris@0 469 public function setCheckForUnintentionallyCoveredCode($flag)
Chris@0 470 {
Chris@14 471 if (!\is_bool($flag)) {
Chris@14 472 throw InvalidArgumentException::create(
Chris@0 473 1,
Chris@0 474 'boolean'
Chris@0 475 );
Chris@0 476 }
Chris@0 477
Chris@0 478 $this->checkForUnintentionallyCoveredCode = $flag;
Chris@0 479 }
Chris@0 480
Chris@0 481 /**
Chris@14 482 * @param bool $flag
Chris@14 483 *
Chris@14 484 * @throws InvalidArgumentException
Chris@0 485 */
Chris@0 486 public function setForceCoversAnnotation($flag)
Chris@0 487 {
Chris@14 488 if (!\is_bool($flag)) {
Chris@14 489 throw InvalidArgumentException::create(
Chris@0 490 1,
Chris@0 491 'boolean'
Chris@0 492 );
Chris@0 493 }
Chris@0 494
Chris@0 495 $this->forceCoversAnnotation = $flag;
Chris@0 496 }
Chris@0 497
Chris@0 498 /**
Chris@14 499 * @param bool $flag
Chris@14 500 *
Chris@14 501 * @throws InvalidArgumentException
Chris@0 502 */
Chris@14 503 public function setCheckForMissingCoversAnnotation($flag)
Chris@0 504 {
Chris@14 505 if (!\is_bool($flag)) {
Chris@14 506 throw InvalidArgumentException::create(
Chris@0 507 1,
Chris@0 508 'boolean'
Chris@0 509 );
Chris@0 510 }
Chris@0 511
Chris@14 512 $this->checkForMissingCoversAnnotation = $flag;
Chris@0 513 }
Chris@0 514
Chris@0 515 /**
Chris@14 516 * @param bool $flag
Chris@14 517 *
Chris@14 518 * @throws InvalidArgumentException
Chris@14 519 */
Chris@14 520 public function setCheckForUnexecutedCoveredCode($flag)
Chris@14 521 {
Chris@14 522 if (!\is_bool($flag)) {
Chris@14 523 throw InvalidArgumentException::create(
Chris@14 524 1,
Chris@14 525 'boolean'
Chris@14 526 );
Chris@14 527 }
Chris@14 528
Chris@14 529 $this->checkForUnexecutedCoveredCode = $flag;
Chris@14 530 }
Chris@14 531
Chris@14 532 /**
Chris@14 533 * @deprecated
Chris@14 534 *
Chris@14 535 * @param bool $flag
Chris@14 536 *
Chris@14 537 * @throws InvalidArgumentException
Chris@14 538 */
Chris@14 539 public function setMapTestClassNameToCoveredClassName($flag)
Chris@14 540 {
Chris@14 541 }
Chris@14 542
Chris@14 543 /**
Chris@14 544 * @param bool $flag
Chris@14 545 *
Chris@14 546 * @throws InvalidArgumentException
Chris@0 547 */
Chris@0 548 public function setAddUncoveredFilesFromWhitelist($flag)
Chris@0 549 {
Chris@14 550 if (!\is_bool($flag)) {
Chris@14 551 throw InvalidArgumentException::create(
Chris@0 552 1,
Chris@0 553 'boolean'
Chris@0 554 );
Chris@0 555 }
Chris@0 556
Chris@0 557 $this->addUncoveredFilesFromWhitelist = $flag;
Chris@0 558 }
Chris@0 559
Chris@0 560 /**
Chris@14 561 * @param bool $flag
Chris@14 562 *
Chris@14 563 * @throws InvalidArgumentException
Chris@0 564 */
Chris@0 565 public function setProcessUncoveredFilesFromWhitelist($flag)
Chris@0 566 {
Chris@14 567 if (!\is_bool($flag)) {
Chris@14 568 throw InvalidArgumentException::create(
Chris@0 569 1,
Chris@0 570 'boolean'
Chris@0 571 );
Chris@0 572 }
Chris@0 573
Chris@0 574 $this->processUncoveredFilesFromWhitelist = $flag;
Chris@0 575 }
Chris@0 576
Chris@0 577 /**
Chris@14 578 * @param bool $flag
Chris@14 579 *
Chris@14 580 * @throws InvalidArgumentException
Chris@0 581 */
Chris@0 582 public function setDisableIgnoredLines($flag)
Chris@0 583 {
Chris@14 584 if (!\is_bool($flag)) {
Chris@14 585 throw InvalidArgumentException::create(
Chris@0 586 1,
Chris@0 587 'boolean'
Chris@0 588 );
Chris@0 589 }
Chris@0 590
Chris@0 591 $this->disableIgnoredLines = $flag;
Chris@0 592 }
Chris@0 593
Chris@0 594 /**
Chris@14 595 * @param bool $flag
Chris@14 596 *
Chris@14 597 * @throws InvalidArgumentException
Chris@14 598 */
Chris@14 599 public function setIgnoreDeprecatedCode($flag)
Chris@14 600 {
Chris@14 601 if (!\is_bool($flag)) {
Chris@14 602 throw InvalidArgumentException::create(
Chris@14 603 1,
Chris@14 604 'boolean'
Chris@14 605 );
Chris@14 606 }
Chris@14 607
Chris@14 608 $this->ignoreDeprecatedCode = $flag;
Chris@14 609 }
Chris@14 610
Chris@14 611 /**
Chris@14 612 * @param array $whitelist
Chris@14 613 */
Chris@14 614 public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist)
Chris@14 615 {
Chris@14 616 $this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
Chris@14 617 }
Chris@14 618
Chris@14 619 /**
Chris@0 620 * Applies the @covers annotation filtering.
Chris@0 621 *
Chris@14 622 * @param array $data
Chris@14 623 * @param mixed $linesToBeCovered
Chris@14 624 * @param array $linesToBeUsed
Chris@14 625 * @param bool $ignoreForceCoversAnnotation
Chris@14 626 *
Chris@14 627 * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
Chris@14 628 * @throws \ReflectionException
Chris@14 629 * @throws MissingCoversAnnotationException
Chris@14 630 * @throws UnintentionallyCoveredCodeException
Chris@0 631 */
Chris@14 632 private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, $ignoreForceCoversAnnotation)
Chris@0 633 {
Chris@0 634 if ($linesToBeCovered === false ||
Chris@14 635 ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) {
Chris@14 636 if ($this->checkForMissingCoversAnnotation) {
Chris@14 637 throw new MissingCoversAnnotationException;
Chris@14 638 }
Chris@14 639
Chris@14 640 $data = [];
Chris@0 641
Chris@0 642 return;
Chris@0 643 }
Chris@0 644
Chris@0 645 if (empty($linesToBeCovered)) {
Chris@0 646 return;
Chris@0 647 }
Chris@0 648
Chris@14 649 if ($this->checkForUnintentionallyCoveredCode &&
Chris@14 650 (!$this->currentId instanceof TestCase ||
Chris@14 651 (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
Chris@14 652 $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
Chris@0 653 }
Chris@0 654
Chris@14 655 if ($this->checkForUnexecutedCoveredCode) {
Chris@14 656 $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
Chris@14 657 }
Chris@0 658
Chris@14 659 $data = \array_intersect_key($data, $linesToBeCovered);
Chris@0 660
Chris@14 661 foreach (\array_keys($data) as $filename) {
Chris@14 662 $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]);
Chris@14 663 $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered);
Chris@0 664 }
Chris@0 665 }
Chris@0 666
Chris@0 667 /**
Chris@14 668 * Applies the whitelist filtering.
Chris@0 669 *
Chris@0 670 * @param array $data
Chris@0 671 */
Chris@0 672 private function applyListsFilter(array &$data)
Chris@0 673 {
Chris@14 674 foreach (\array_keys($data) as $filename) {
Chris@0 675 if ($this->filter->isFiltered($filename)) {
Chris@0 676 unset($data[$filename]);
Chris@0 677 }
Chris@0 678 }
Chris@0 679 }
Chris@0 680
Chris@0 681 /**
Chris@0 682 * Applies the "ignored lines" filtering.
Chris@0 683 *
Chris@0 684 * @param array $data
Chris@14 685 *
Chris@14 686 * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
Chris@0 687 */
Chris@0 688 private function applyIgnoredLinesFilter(array &$data)
Chris@0 689 {
Chris@14 690 foreach (\array_keys($data) as $filename) {
Chris@0 691 if (!$this->filter->isFile($filename)) {
Chris@0 692 continue;
Chris@0 693 }
Chris@0 694
Chris@0 695 foreach ($this->getLinesToBeIgnored($filename) as $line) {
Chris@0 696 unset($data[$filename][$line]);
Chris@0 697 }
Chris@0 698 }
Chris@0 699 }
Chris@0 700
Chris@0 701 /**
Chris@0 702 * @param array $data
Chris@0 703 */
Chris@0 704 private function initializeFilesThatAreSeenTheFirstTime(array $data)
Chris@0 705 {
Chris@0 706 foreach ($data as $file => $lines) {
Chris@14 707 if (!isset($this->data[$file]) && $this->filter->isFile($file)) {
Chris@14 708 $this->data[$file] = [];
Chris@0 709
Chris@0 710 foreach ($lines as $k => $v) {
Chris@14 711 $this->data[$file][$k] = $v === -2 ? null : [];
Chris@0 712 }
Chris@0 713 }
Chris@0 714 }
Chris@0 715 }
Chris@0 716
Chris@0 717 /**
Chris@0 718 * Processes whitelisted files that are not covered.
Chris@0 719 */
Chris@0 720 private function addUncoveredFilesFromWhitelist()
Chris@0 721 {
Chris@14 722 $data = [];
Chris@14 723 $uncoveredFiles = \array_diff(
Chris@0 724 $this->filter->getWhitelist(),
Chris@14 725 \array_keys($this->data)
Chris@0 726 );
Chris@0 727
Chris@0 728 foreach ($uncoveredFiles as $uncoveredFile) {
Chris@14 729 if (!\file_exists($uncoveredFile)) {
Chris@0 730 continue;
Chris@0 731 }
Chris@0 732
Chris@14 733 $data[$uncoveredFile] = [];
Chris@0 734
Chris@14 735 $lines = \count(\file($uncoveredFile));
Chris@0 736
Chris@14 737 for ($i = 1; $i <= $lines; $i++) {
Chris@14 738 $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
Chris@0 739 }
Chris@0 740 }
Chris@0 741
Chris@0 742 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
Chris@0 743 }
Chris@0 744
Chris@0 745 /**
Chris@0 746 * Returns the lines of a source file that should be ignored.
Chris@0 747 *
Chris@14 748 * @param string $filename
Chris@14 749 *
Chris@0 750 * @return array
Chris@14 751 *
Chris@14 752 * @throws InvalidArgumentException
Chris@0 753 */
Chris@0 754 private function getLinesToBeIgnored($filename)
Chris@0 755 {
Chris@14 756 if (!\is_string($filename)) {
Chris@14 757 throw InvalidArgumentException::create(
Chris@0 758 1,
Chris@0 759 'string'
Chris@0 760 );
Chris@0 761 }
Chris@0 762
Chris@14 763 if (isset($this->ignoredLines[$filename])) {
Chris@14 764 return $this->ignoredLines[$filename];
Chris@14 765 }
Chris@0 766
Chris@14 767 $this->ignoredLines[$filename] = [];
Chris@14 768
Chris@14 769 $lines = \file($filename);
Chris@14 770
Chris@14 771 foreach ($lines as $index => $line) {
Chris@14 772 if (!\trim($line)) {
Chris@14 773 $this->ignoredLines[$filename][] = $index + 1;
Chris@14 774 }
Chris@14 775 }
Chris@14 776
Chris@14 777 if ($this->cacheTokens) {
Chris@14 778 $tokens = \PHP_Token_Stream_CachingFactory::get($filename);
Chris@14 779 } else {
Chris@14 780 $tokens = new \PHP_Token_Stream($filename);
Chris@14 781 }
Chris@14 782
Chris@14 783 foreach ($tokens->getInterfaces() as $interface) {
Chris@14 784 $interfaceStartLine = $interface['startLine'];
Chris@14 785 $interfaceEndLine = $interface['endLine'];
Chris@14 786
Chris@14 787 foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
Chris@14 788 $this->ignoredLines[$filename][] = $line;
Chris@14 789 }
Chris@14 790 }
Chris@14 791
Chris@14 792 foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
Chris@14 793 $classOrTraitStartLine = $classOrTrait['startLine'];
Chris@14 794 $classOrTraitEndLine = $classOrTrait['endLine'];
Chris@14 795
Chris@14 796 if (empty($classOrTrait['methods'])) {
Chris@14 797 foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
Chris@14 798 $this->ignoredLines[$filename][] = $line;
Chris@14 799 }
Chris@14 800
Chris@14 801 continue;
Chris@0 802 }
Chris@0 803
Chris@14 804 $firstMethod = \array_shift($classOrTrait['methods']);
Chris@14 805 $firstMethodStartLine = $firstMethod['startLine'];
Chris@14 806 $firstMethodEndLine = $firstMethod['endLine'];
Chris@14 807 $lastMethodEndLine = $firstMethodEndLine;
Chris@0 808
Chris@14 809 do {
Chris@14 810 $lastMethod = \array_pop($classOrTrait['methods']);
Chris@14 811 } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
Chris@14 812
Chris@14 813 if ($lastMethod !== null) {
Chris@14 814 $lastMethodEndLine = $lastMethod['endLine'];
Chris@14 815 }
Chris@14 816
Chris@14 817 foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
Chris@14 818 $this->ignoredLines[$filename][] = $line;
Chris@14 819 }
Chris@14 820
Chris@14 821 foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
Chris@14 822 $this->ignoredLines[$filename][] = $line;
Chris@14 823 }
Chris@14 824 }
Chris@14 825
Chris@14 826 if ($this->disableIgnoredLines) {
Chris@14 827 $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]);
Chris@14 828 \sort($this->ignoredLines[$filename]);
Chris@14 829
Chris@14 830 return $this->ignoredLines[$filename];
Chris@14 831 }
Chris@14 832
Chris@14 833 $ignore = false;
Chris@14 834 $stop = false;
Chris@14 835
Chris@14 836 foreach ($tokens->tokens() as $token) {
Chris@14 837 switch (\get_class($token)) {
Chris@14 838 case \PHP_Token_COMMENT::class:
Chris@14 839 case \PHP_Token_DOC_COMMENT::class:
Chris@14 840 $_token = \trim($token);
Chris@14 841 $_line = \trim($lines[$token->getLine() - 1]);
Chris@14 842
Chris@14 843 if ($_token === '// @codeCoverageIgnore' ||
Chris@14 844 $_token === '//@codeCoverageIgnore') {
Chris@14 845 $ignore = true;
Chris@14 846 $stop = true;
Chris@14 847 } elseif ($_token === '// @codeCoverageIgnoreStart' ||
Chris@14 848 $_token === '//@codeCoverageIgnoreStart') {
Chris@14 849 $ignore = true;
Chris@14 850 } elseif ($_token === '// @codeCoverageIgnoreEnd' ||
Chris@14 851 $_token === '//@codeCoverageIgnoreEnd') {
Chris@14 852 $stop = true;
Chris@14 853 }
Chris@14 854
Chris@14 855 if (!$ignore) {
Chris@14 856 $start = $token->getLine();
Chris@14 857 $end = $start + \substr_count($token, "\n");
Chris@14 858
Chris@14 859 // Do not ignore the first line when there is a token
Chris@14 860 // before the comment
Chris@14 861 if (0 !== \strpos($_token, $_line)) {
Chris@14 862 $start++;
Chris@14 863 }
Chris@14 864
Chris@14 865 for ($i = $start; $i < $end; $i++) {
Chris@14 866 $this->ignoredLines[$filename][] = $i;
Chris@14 867 }
Chris@14 868
Chris@14 869 // A DOC_COMMENT token or a COMMENT token starting with "/*"
Chris@14 870 // does not contain the final \n character in its text
Chris@14 871 if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
Chris@14 872 $this->ignoredLines[$filename][] = $i;
Chris@14 873 }
Chris@14 874 }
Chris@14 875
Chris@14 876 break;
Chris@14 877
Chris@14 878 case \PHP_Token_INTERFACE::class:
Chris@14 879 case \PHP_Token_TRAIT::class:
Chris@14 880 case \PHP_Token_CLASS::class:
Chris@14 881 case \PHP_Token_FUNCTION::class:
Chris@14 882 /* @var \PHP_Token_Interface $token */
Chris@14 883
Chris@14 884 $docblock = $token->getDocblock();
Chris@14 885
Chris@14 886 $this->ignoredLines[$filename][] = $token->getLine();
Chris@14 887
Chris@14 888 if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
Chris@14 889 $endLine = $token->getEndLine();
Chris@14 890
Chris@14 891 for ($i = $token->getLine(); $i <= $endLine; $i++) {
Chris@14 892 $this->ignoredLines[$filename][] = $i;
Chris@14 893 }
Chris@14 894 }
Chris@14 895
Chris@14 896 break;
Chris@14 897
Chris@14 898 case \PHP_Token_ENUM::class:
Chris@14 899 $this->ignoredLines[$filename][] = $token->getLine();
Chris@14 900
Chris@14 901 break;
Chris@14 902
Chris@14 903 case \PHP_Token_NAMESPACE::class:
Chris@14 904 $this->ignoredLines[$filename][] = $token->getEndLine();
Chris@14 905
Chris@14 906 // Intentional fallthrough
Chris@14 907 case \PHP_Token_DECLARE::class:
Chris@14 908 case \PHP_Token_OPEN_TAG::class:
Chris@14 909 case \PHP_Token_CLOSE_TAG::class:
Chris@14 910 case \PHP_Token_USE::class:
Chris@14 911 $this->ignoredLines[$filename][] = $token->getLine();
Chris@14 912
Chris@14 913 break;
Chris@14 914 }
Chris@14 915
Chris@14 916 if ($ignore) {
Chris@14 917 $this->ignoredLines[$filename][] = $token->getLine();
Chris@14 918
Chris@14 919 if ($stop) {
Chris@14 920 $ignore = false;
Chris@14 921 $stop = false;
Chris@0 922 }
Chris@0 923 }
Chris@14 924 }
Chris@0 925
Chris@14 926 $this->ignoredLines[$filename][] = \count($lines) + 1;
Chris@0 927
Chris@14 928 $this->ignoredLines[$filename] = \array_unique(
Chris@14 929 $this->ignoredLines[$filename]
Chris@14 930 );
Chris@0 931
Chris@14 932 $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]);
Chris@14 933 \sort($this->ignoredLines[$filename]);
Chris@0 934
Chris@0 935 return $this->ignoredLines[$filename];
Chris@0 936 }
Chris@0 937
Chris@0 938 /**
Chris@14 939 * @param array $data
Chris@14 940 * @param array $linesToBeCovered
Chris@14 941 * @param array $linesToBeUsed
Chris@14 942 *
Chris@14 943 * @throws \ReflectionException
Chris@14 944 * @throws UnintentionallyCoveredCodeException
Chris@0 945 */
Chris@0 946 private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
Chris@0 947 {
Chris@0 948 $allowedLines = $this->getAllowedLines(
Chris@0 949 $linesToBeCovered,
Chris@0 950 $linesToBeUsed
Chris@0 951 );
Chris@0 952
Chris@14 953 $unintentionallyCoveredUnits = [];
Chris@0 954
Chris@0 955 foreach ($data as $file => $_data) {
Chris@0 956 foreach ($_data as $line => $flag) {
Chris@14 957 if ($flag === 1 && !isset($allowedLines[$file][$line])) {
Chris@14 958 $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
Chris@0 959 }
Chris@0 960 }
Chris@0 961 }
Chris@0 962
Chris@14 963 $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
Chris@14 964
Chris@14 965 if (!empty($unintentionallyCoveredUnits)) {
Chris@14 966 throw new UnintentionallyCoveredCodeException(
Chris@14 967 $unintentionallyCoveredUnits
Chris@0 968 );
Chris@0 969 }
Chris@0 970 }
Chris@0 971
Chris@0 972 /**
Chris@14 973 * @param array $data
Chris@14 974 * @param array $linesToBeCovered
Chris@14 975 * @param array $linesToBeUsed
Chris@14 976 *
Chris@14 977 * @throws CoveredCodeNotExecutedException
Chris@14 978 */
Chris@14 979 private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
Chris@14 980 {
Chris@14 981 $executedCodeUnits = $this->coverageToCodeUnits($data);
Chris@14 982 $message = '';
Chris@14 983
Chris@14 984 foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
Chris@14 985 if (!\in_array($codeUnit, $executedCodeUnits)) {
Chris@14 986 $message .= \sprintf(
Chris@14 987 '- %s is expected to be executed (@covers) but was not executed' . "\n",
Chris@14 988 $codeUnit
Chris@14 989 );
Chris@14 990 }
Chris@14 991 }
Chris@14 992
Chris@14 993 foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
Chris@14 994 if (!\in_array($codeUnit, $executedCodeUnits)) {
Chris@14 995 $message .= \sprintf(
Chris@14 996 '- %s is expected to be executed (@uses) but was not executed' . "\n",
Chris@14 997 $codeUnit
Chris@14 998 );
Chris@14 999 }
Chris@14 1000 }
Chris@14 1001
Chris@14 1002 if (!empty($message)) {
Chris@14 1003 throw new CoveredCodeNotExecutedException($message);
Chris@14 1004 }
Chris@14 1005 }
Chris@14 1006
Chris@14 1007 /**
Chris@14 1008 * @param array $linesToBeCovered
Chris@14 1009 * @param array $linesToBeUsed
Chris@14 1010 *
Chris@0 1011 * @return array
Chris@0 1012 */
Chris@0 1013 private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
Chris@0 1014 {
Chris@14 1015 $allowedLines = [];
Chris@0 1016
Chris@14 1017 foreach (\array_keys($linesToBeCovered) as $file) {
Chris@0 1018 if (!isset($allowedLines[$file])) {
Chris@14 1019 $allowedLines[$file] = [];
Chris@0 1020 }
Chris@0 1021
Chris@14 1022 $allowedLines[$file] = \array_merge(
Chris@0 1023 $allowedLines[$file],
Chris@0 1024 $linesToBeCovered[$file]
Chris@0 1025 );
Chris@0 1026 }
Chris@0 1027
Chris@14 1028 foreach (\array_keys($linesToBeUsed) as $file) {
Chris@0 1029 if (!isset($allowedLines[$file])) {
Chris@14 1030 $allowedLines[$file] = [];
Chris@0 1031 }
Chris@0 1032
Chris@14 1033 $allowedLines[$file] = \array_merge(
Chris@0 1034 $allowedLines[$file],
Chris@0 1035 $linesToBeUsed[$file]
Chris@0 1036 );
Chris@0 1037 }
Chris@0 1038
Chris@14 1039 foreach (\array_keys($allowedLines) as $file) {
Chris@14 1040 $allowedLines[$file] = \array_flip(
Chris@14 1041 \array_unique($allowedLines[$file])
Chris@0 1042 );
Chris@0 1043 }
Chris@0 1044
Chris@0 1045 return $allowedLines;
Chris@0 1046 }
Chris@0 1047
Chris@0 1048 /**
Chris@14 1049 * @return Driver
Chris@14 1050 *
Chris@14 1051 * @throws RuntimeException
Chris@0 1052 */
Chris@0 1053 private function selectDriver()
Chris@0 1054 {
Chris@0 1055 $runtime = new Runtime;
Chris@0 1056
Chris@0 1057 if (!$runtime->canCollectCodeCoverage()) {
Chris@14 1058 throw new RuntimeException('No code coverage driver available');
Chris@0 1059 }
Chris@0 1060
Chris@0 1061 if ($runtime->isHHVM()) {
Chris@14 1062 return new HHVM;
Chris@14 1063 }
Chris@14 1064
Chris@14 1065 if ($runtime->isPHPDBG()) {
Chris@14 1066 return new PHPDBG;
Chris@14 1067 }
Chris@14 1068
Chris@14 1069 return new Xdebug;
Chris@14 1070 }
Chris@14 1071
Chris@14 1072 /**
Chris@14 1073 * @param array $unintentionallyCoveredUnits
Chris@14 1074 *
Chris@14 1075 * @return array
Chris@14 1076 *
Chris@14 1077 * @throws \ReflectionException
Chris@14 1078 */
Chris@14 1079 private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits)
Chris@14 1080 {
Chris@14 1081 $unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits);
Chris@14 1082 \sort($unintentionallyCoveredUnits);
Chris@14 1083
Chris@14 1084 foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) {
Chris@14 1085 $unit = \explode('::', $unintentionallyCoveredUnits[$k]);
Chris@14 1086
Chris@14 1087 if (\count($unit) !== 2) {
Chris@14 1088 continue;
Chris@14 1089 }
Chris@14 1090
Chris@14 1091 $class = new \ReflectionClass($unit[0]);
Chris@14 1092
Chris@14 1093 foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
Chris@14 1094 if ($class->isSubclassOf($whitelisted)) {
Chris@14 1095 unset($unintentionallyCoveredUnits[$k]);
Chris@14 1096
Chris@14 1097 break;
Chris@14 1098 }
Chris@14 1099 }
Chris@14 1100 }
Chris@14 1101
Chris@14 1102 return \array_values($unintentionallyCoveredUnits);
Chris@14 1103 }
Chris@14 1104
Chris@14 1105 /**
Chris@14 1106 * If we are processing uncovered files from whitelist,
Chris@14 1107 * we can initialize the data before we start to speed up the tests
Chris@14 1108 *
Chris@14 1109 * @throws \SebastianBergmann\CodeCoverage\RuntimeException
Chris@14 1110 */
Chris@14 1111 protected function initializeData()
Chris@14 1112 {
Chris@14 1113 $this->isInitialized = true;
Chris@14 1114
Chris@14 1115 if ($this->processUncoveredFilesFromWhitelist) {
Chris@14 1116 $this->shouldCheckForDeadAndUnused = false;
Chris@14 1117
Chris@14 1118 $this->driver->start(true);
Chris@14 1119
Chris@14 1120 foreach ($this->filter->getWhitelist() as $file) {
Chris@14 1121 if ($this->filter->isFile($file)) {
Chris@14 1122 include_once($file);
Chris@14 1123 }
Chris@14 1124 }
Chris@14 1125
Chris@14 1126 $data = [];
Chris@14 1127 $coverage = $this->driver->stop();
Chris@14 1128
Chris@14 1129 foreach ($coverage as $file => $fileCoverage) {
Chris@14 1130 if ($this->filter->isFiltered($file)) {
Chris@14 1131 continue;
Chris@14 1132 }
Chris@14 1133
Chris@14 1134 foreach (\array_keys($fileCoverage) as $key) {
Chris@14 1135 if ($fileCoverage[$key] === Driver::LINE_EXECUTED) {
Chris@14 1136 $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
Chris@14 1137 }
Chris@14 1138 }
Chris@14 1139
Chris@14 1140 $data[$file] = $fileCoverage;
Chris@14 1141 }
Chris@14 1142
Chris@14 1143 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
Chris@0 1144 }
Chris@0 1145 }
Chris@14 1146
Chris@14 1147 /**
Chris@14 1148 * @param array $data
Chris@14 1149 *
Chris@14 1150 * @return array
Chris@14 1151 */
Chris@14 1152 private function coverageToCodeUnits(array $data)
Chris@14 1153 {
Chris@14 1154 $codeUnits = [];
Chris@14 1155
Chris@14 1156 foreach ($data as $filename => $lines) {
Chris@14 1157 foreach ($lines as $line => $flag) {
Chris@14 1158 if ($flag === 1) {
Chris@14 1159 $codeUnits[] = $this->wizard->lookup($filename, $line);
Chris@14 1160 }
Chris@14 1161 }
Chris@14 1162 }
Chris@14 1163
Chris@14 1164 return \array_unique($codeUnits);
Chris@14 1165 }
Chris@14 1166
Chris@14 1167 /**
Chris@14 1168 * @param array $data
Chris@14 1169 *
Chris@14 1170 * @return array
Chris@14 1171 */
Chris@14 1172 private function linesToCodeUnits(array $data)
Chris@14 1173 {
Chris@14 1174 $codeUnits = [];
Chris@14 1175
Chris@14 1176 foreach ($data as $filename => $lines) {
Chris@14 1177 foreach ($lines as $line) {
Chris@14 1178 $codeUnits[] = $this->wizard->lookup($filename, $line);
Chris@14 1179 }
Chris@14 1180 }
Chris@14 1181
Chris@14 1182 return \array_unique($codeUnits);
Chris@14 1183 }
Chris@0 1184 }