5 * Copyright (c) 2009-2011, Sebastian Bergmann <sb@sebastian-bergmann.de>.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
12 * * Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
15 * * Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in
17 * the documentation and/or other materials provided with the
20 * * Neither the name of Sebastian Bergmann nor the names of his
21 * contributors may be used to endorse or promote products derived
22 * from this software without specific prior written permission.
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35 * POSSIBILITY OF SUCH DAMAGE.
38 * @package CodeCoverage
39 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
40 * @copyright 2009-2011 Sebastian Bergmann <sb@sebastian-bergmann.de>
41 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
42 * @link http://github.com/sebastianbergmann/php-code-coverage
43 * @since File available since Release 1.0.0
46 if (!defined('T_NAMESPACE')) {
47 define('T_NAMESPACE', 377);
50 require_once 'PHP/Token/Stream/CachingFactory.php';
56 * @package CodeCoverage
57 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
58 * @copyright 2009-2011 Sebastian Bergmann <sb@sebastian-bergmann.de>
59 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
60 * @version Release: 1.0.4
61 * @link http://github.com/sebastianbergmann/php-code-coverage
62 * @since Class available since Release 1.0.0
64 class PHP_CodeCoverage_Util
69 const REGEX = '(@covers\s+(?P<coveredElement>.*?)\s*$)m';
74 protected static $ignoredLines = array();
79 protected static $templateMethods = array(
80 'setUp', 'assertPreConditions', 'assertPostConditions', 'tearDown'
84 * Builds an array representation of the directory structure.
91 * [Money.php] => Array
96 * [MoneyBag.php] => Array
103 * is transformed into
110 * [Money.php] => Array
115 * [MoneyBag.php] => Array
123 * @param array $files
126 public static function buildDirectoryStructure($files)
130 foreach ($files as $path => $file) {
131 $path = explode('/', $path);
135 for ($i = 0; $i < $max; $i++) {
136 if ($i == ($max - 1)) {
142 $pointer = &$pointer[$path[$i] . $type];
152 * Calculates the Change Risk Anti-Patterns (CRAP) index for a unit of code
153 * based on its cyclomatic complexity and percentage of code coverage.
155 * @param integer $ccn
156 * @param float $coverage
159 public static function crap($ccn, $coverage)
161 if ($coverage == 0) {
162 return (string)pow($ccn, 2) + $ccn;
165 if ($coverage >= 95) {
170 '%01.2F', pow($ccn, 2) * pow(1 - $coverage/100, 3) + $ccn
175 * @param string $directory
177 * @throws RuntimeException
179 public static function getDirectory($directory)
181 if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) {
182 $directory .= DIRECTORY_SEPARATOR;
185 if (is_dir($directory)) {
189 if (mkdir($directory, 0777, TRUE)) {
193 throw new RuntimeException(
195 'Directory "%s" does not exist.',
202 * Returns the files and lines a test method wants to cover.
204 * @param string $className
205 * @param string $methodName
208 public static function getLinesToBeCovered($className, $methodName)
210 $codeToCoverList = array();
212 // @codeCoverageIgnoreStart
213 if (($pos = strpos($methodName, ' ')) !== FALSE) {
214 $methodName = substr($methodName, 0, $pos);
216 // @codeCoverageIgnoreEnd
217 $class = new ReflectionClass($className);
218 $method = new ReflectionMethod($className, $methodName);
219 $docComment = $class->getDocComment() . $method->getDocComment();
221 foreach (self::$templateMethods as $templateMethod) {
222 if ($class->hasMethod($templateMethod)) {
223 $reflector = $class->getMethod($templateMethod);
224 $docComment .= $reflector->getDocComment();
229 if (preg_match_all(self::REGEX, $docComment, $matches)) {
230 foreach ($matches['coveredElement'] as $coveredElement) {
231 $codeToCoverList = array_merge(
233 self::resolveCoversToReflectionObjects($coveredElement)
237 foreach ($codeToCoverList as $codeToCover) {
238 $fileName = $codeToCover->getFileName();
240 if (!isset($result[$fileName])) {
241 $result[$fileName] = array();
244 $result[$fileName] = array_unique(
248 $codeToCover->getStartLine(), $codeToCover->getEndLine()
259 * Returns the lines of a source file that should be ignored.
261 * @param string $filename
264 public static function getLinesToBeIgnored($filename)
266 if (!isset(self::$ignoredLines[$filename])) {
267 self::$ignoredLines[$filename] = array();
271 $tokens = PHP_Token_Stream_CachingFactory::get($filename)->tokens();
273 foreach ($tokens as $token) {
274 switch (get_class($token)) {
275 case 'PHP_Token_CLASS':
276 case 'PHP_Token_FUNCTION': {
277 $docblock = $token->getDocblock();
278 $endLine = $token->getEndLine();
280 if (strpos($docblock, '@codeCoverageIgnore')) {
281 for ($i = $token->getLine(); $i <= $endLine; $i++) {
282 self::$ignoredLines[$filename][$i] = TRUE;
288 case 'PHP_Token_COMMENT': {
289 $_token = trim($token);
291 if ($_token == '// @codeCoverageIgnoreStart' ||
292 $_token == '//@codeCoverageIgnoreStart') {
296 else if ($_token == '// @codeCoverageIgnoreEnd' ||
297 $_token == '//@codeCoverageIgnoreEnd') {
305 self::$ignoredLines[$filename][$token->getLine()] = TRUE;
315 return self::$ignoredLines[$filename];
319 * Returns the package information of a user-defined class.
321 * @param string $className
322 * @param string $docComment
325 public static function getPackageInformation($className, $docComment)
335 if (strpos($className, '\\') !== FALSE) {
336 $result['namespace'] = self::arrayToName(
337 explode('\\', $className)
341 if (preg_match('/@category[\s]+([\.\w]+)/', $docComment, $matches)) {
342 $result['category'] = $matches[1];
345 if (preg_match('/@package[\s]+([\.\w]+)/', $docComment, $matches)) {
346 $result['package'] = $matches[1];
347 $result['fullPackage'] = $matches[1];
350 if (preg_match('/@subpackage[\s]+([\.\w]+)/', $docComment, $matches)) {
351 $result['subpackage'] = $matches[1];
352 $result['fullPackage'] .= '.' . $matches[1];
355 if (empty($result['fullPackage'])) {
356 $result['fullPackage'] = self::arrayToName(
357 explode('_', str_replace('\\', '_', $className)), '.'
365 * Returns a filesystem safe version of the passed filename.
366 * This function does not operate on full paths, just filenames.
368 * @param string $filename
370 * @author Michael Lively Jr. <m@digitalsandwich.com>
372 public static function getSafeFilename($filename)
374 /* characters allowed: A-Z, a-z, 0-9, _ and . */
375 return preg_replace('#[^\w.]#', '_', $filename);
381 * @return float ($a / $b) * 100
383 public static function percent($a, $b, $asString = FALSE)
386 $percent = ($a / $b) * 100;
392 return sprintf('%01.2F', $percent);
399 * Reduces the paths by cutting the longest common start path.
406 * [/home/sb/Money/Money.php] => Array
411 * [/home/sb/Money/MoneyBag.php] => Array
423 * [Money.php] => Array
428 * [MoneyBag.php] => Array
435 * @param array $files
438 public static function reducePaths(&$files)
445 $paths = array_keys($files);
447 if (count($files) == 1) {
448 $commonPath = dirname($paths[0]) . '/';
449 $files[basename($paths[0])] = $files[$paths[0]];
451 unset($files[$paths[0]]);
456 $max = count($paths);
458 for ($i = 0; $i < $max; $i++) {
459 $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
461 if (empty($paths[$i][0])) {
462 $paths[$i][0] = DIRECTORY_SEPARATOR;
467 $max = count($paths);
470 for ($i = 0; $i < $max - 1; $i++) {
471 if (!isset($paths[$i][0]) ||
472 !isset($paths[$i+1][0]) ||
473 $paths[$i][0] != $paths[$i+1][0]) {
480 $commonPath .= $paths[0][0];
482 if ($paths[0][0] != DIRECTORY_SEPARATOR) {
483 $commonPath .= DIRECTORY_SEPARATOR;
486 for ($i = 0; $i < $max; $i++) {
487 array_shift($paths[$i]);
492 $original = array_keys($files);
493 $max = count($original);
495 for ($i = 0; $i < $max; $i++) {
496 $files[join('/', $paths[$i])] = $files[$original[$i]];
497 unset($files[$original[$i]]);
506 * Returns the package information of a user-defined class.
508 * @param array $parts
509 * @param string $join
512 protected static function arrayToName(array $parts, $join = '\\')
516 if (count($parts) > 1) {
519 $result = join($join, $parts);
526 * @param string $coveredElement
529 protected static function resolveCoversToReflectionObjects($coveredElement)
531 $codeToCoverList = array();
533 if (strpos($coveredElement, '::') !== FALSE) {
534 list($className, $methodName) = explode('::', $coveredElement);
536 if ($methodName[0] == '<') {
537 $classes = array($className);
539 foreach ($classes as $className) {
540 if (!class_exists($className) &&
541 !interface_exists($className)) {
542 throw new RuntimeException(
544 'Trying to @cover not existing class or ' .
551 $class = new ReflectionClass($className);
552 $methods = $class->getMethods();
553 $inverse = isset($methodName[1]) && $methodName[1] == '!';
555 if (strpos($methodName, 'protected')) {
556 $visibility = 'isProtected';
559 else if (strpos($methodName, 'private')) {
560 $visibility = 'isPrivate';
563 else if (strpos($methodName, 'public')) {
564 $visibility = 'isPublic';
567 foreach ($methods as $method) {
568 if ($inverse && !$method->$visibility()) {
569 $codeToCoverList[] = $method;
572 else if (!$inverse && $method->$visibility()) {
573 $codeToCoverList[] = $method;
578 $classes = array($className);
580 foreach ($classes as $className) {
581 if ($className == '' && function_exists($methodName)) {
582 $codeToCoverList[] = new ReflectionFunction(
586 if (!((class_exists($className) ||
587 interface_exists($className)) &&
588 method_exists($className, $methodName))) {
589 throw new RuntimeException(
591 'Trying to @cover not existing method "%s::%s".',
598 $codeToCoverList[] = new ReflectionMethod(
599 $className, $methodName
607 if (strpos($coveredElement, '<extended>') !== FALSE) {
608 $coveredElement = str_replace(
609 '<extended>', '', $coveredElement
615 $classes = array($coveredElement);
618 $classes = array_merge(
620 class_implements($coveredElement),
621 class_parents($coveredElement)
625 foreach ($classes as $className) {
626 if (!class_exists($className) &&
627 !interface_exists($className)) {
628 throw new RuntimeException(
630 'Trying to @cover not existing class or ' .
637 $codeToCoverList[] = new ReflectionClass($className);
641 return $codeToCoverList;