<?php
/**
 * PHPUnit
 *
 * Copyright (c) 2002-2008, Sebastian Bergmann <sb@sebastian-bergmann.de>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *
 *   * Neither the name of Sebastian Bergmann nor the names of his
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * @category   Testing
 * @package    PHPUnit
 * @author     Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @copyright  2002-2008 Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version    SVN: $Id: Function.php 1985 2007-12-26 18:11:55Z sb $
 * @link       http://www.phpunit.de/
 * @since      File available since Release 3.2.0
 */

require_once 'PHPUnit/Util/Class.php';
require_once 'PHPUnit/Util/Filter.php';

PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');

/**
 * Function- and Method-Level Metrics.
 *
 * @category   Testing
 * @package    PHPUnit
 * @author     Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @copyright  2002-2008 Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version    Release: 3.2.9
 * @link       http://www.phpunit.de/
 * @since      Class available since Release 3.2.0
 */
class PHPUnit_Util_Metrics_Function extends PHPUnit_Util_Metrics
{
    protected $ccn           = 1;
    protected $npath         = 1;
    protected $coverage      = 0;
    protected $crap;
    protected $loc           = 0;
    protected $locExecutable = 0;
    protected $locExecuted   = 0;
    protected $parameters    = 0;

    protected $function;
    protected $scope;
    protected $tokens;

    protected $dependencies = array();

    protected static $cache = array();

    /**
     * Constructor.
     *
     * @param  string                              $scope
     * @param  ReflectionFunction|ReflectionMethod $function
     * @param  array                               $codeCoverage
     * @access protected
     */
    protected function __construct($scope, $function, &$codeCoverage = array())
    {
        $this->scope    = $scope;
        $this->function = $function;

        $source = PHPUnit_Util_Class::getMethodSource(
          $scope, $function->getName()
        );

        if ($source !== FALSE) {
            $this->tokens     = token_get_all('<?php' . $source . '?>');
            $this->parameters = $function->getNumberOfParameters();

            $this->calculateCCN();
            $this->calculateNPath();
            $this->calculateDependencies();
        }

        $this->setCoverage($codeCoverage);
    }

    /**
     * Factory.
     *
     * @param  ReflectionFunction|ReflectionMethod $function
     * @param  array                               $codeCoverage
     * @return PHPUnit_Util_Metrics_Method
     * @access public
     * @static
     */
    public static function factory($function, &$codeCoverage = array())
    {
        if ($function instanceof ReflectionMethod) {
            $scope = $function->getDeclaringClass()->getName();
        } else {
            $scope = 'global';
        }

        $name = $function->getName();

        if (!isset(self::$cache[$scope][$name])) {
            self::$cache[$scope][$name] = new PHPUnit_Util_Metrics_Function($scope, $function, $codeCoverage);
        }

        else if (!empty($codeCoverage) && self::$cache[$scope][$name]->getCoverage() == 0) {
            self::$cache[$scope][$name]->setCoverage($codeCoverage);
        }

        return self::$cache[$scope][$name];
    }

    /**
     * @param  array $codeCoverage
     * @access public
     */
    public function setCoverage(array &$codeCoverage)
    {
        if (!empty($codeCoverage)) {
            $this->calculateCodeCoverage($codeCoverage);
            $this->calculateCrapIndex();
        }
    }

    /**
     * Returns the function.
     *
     * @return ReflectionFunction
     * @access public
     */
    public function getFunction()
    {
        return $this->function;
    }

    /**
     * Returns the method.
     * Alias for getFunction().
     *
     * @return ReflectionMethod
     * @access public
     */
    public function getMethod()
    {
        return $this->function;
    }

    /**
     * Returns the names of the classes this function or method depends on.
     *
     * @return array
     * @access public
     */
    public function getDependencies()
    {
        return $this->dependencies;
    }

    /**
     * Lines of Code (LOC).
     *
     * @return int
     * @access public
     */
    public function getLoc()
    {
        return $this->loc;
    }

    /**
     * Executable Lines of Code (ELOC).
     *
     * @return int
     * @access public
     */
    public function getLocExecutable()
    {
        return $this->locExecutable;
    }

    /**
     * Executed Lines of Code.
     *
     * @return int
     * @access public
     */
    public function getLocExecuted()
    {
        return $this->locExecuted;
    }

    /**
     * Number of Parameters.
     *
     * @return int
     * @access public
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * Returns the Cyclomatic Complexity Number (CCN) for the method.
     * This is also known as the McCabe metric.
     *
     * Each method has a minimum value of 1 per default. For each of the
     * following PHP keywords/statements this value gets incremented by one:
     *
     *   - if
     *   - for
     *   - foreach
     *   - while
     *   - case
     *   - catch
     *   - AND, &&
     *   - OR, ||
     *   - ?
     *
     * Note that 'else', 'default', and 'finally' don't increment the value
     * any further. On the other hand, a simple method with a 'switch'
     * statement and a huge block of 'case 'statements can have a surprisingly
     * high value (still it has the same value when converting a 'switch'
     * block to an equivalent sequence of 'if' statements).
     *
     * @return integer
     * @access public
     * @see    http://en.wikipedia.org/wiki/Cyclomatic_complexity
     */
    public function getCCN()
    {
        return $this->ccn;
    }

    /**
     * Returns the Change Risk Analysis and Predictions (CRAP) index for the
     * method.
     *
     * @return float
     * @access public
     * @see    http://www.artima.com/weblogs/viewpost.jsp?thread=210575
     */
    public function getCrapIndex()
    {
        return $this->crap;
    }

    /**
     * Returns the Code Coverage for the method.
     *
     * @return float
     * @access public
     */
    public function getCoverage()
    {
        return $this->coverage;
    }

    /**
     * Returns the NPath Complexity for the method.
     *
     * @return integer
     * @access public
     */
    public function getNPath()
    {
        return $this->npath;
    }

    /**
     * Calculates the Cyclomatic Complexity Number (CCN) for the method.
     *
     * @access protected
     */
    protected function calculateCCN()
    {
        foreach ($this->tokens as $token) {
            if (is_string($token)) {
                $token = trim($token);

                if ($token == '?') {
                    $this->ccn++;
                }

                continue;
            }

            list ($token, $value) = $token;

            switch ($token) {
                case T_IF:
                case T_FOR:
                case T_FOREACH:
                case T_WHILE:
                case T_CASE:
                case T_CATCH:
                case T_BOOLEAN_AND:
                case T_LOGICAL_AND:
                case T_BOOLEAN_OR:
                case T_LOGICAL_OR: {
                    $this->ccn++;
                }
                break;
            }
        }
    }

    /**
     * Calculates the NPath Complexity for the method.
     *
     * @access protected
     */
    protected function calculateNPath()
    {
        $npathStack = array();
        $stack      = array();

        foreach ($this->tokens as $token) {
            if (is_string($token)) {
                $token = trim($token);

                if ($token == '?') {
                    $this->npath = ($this->npath + 1) * $this->npath;
                }

                if ($token == '{') {
                    if (isset($scope)) {
                        array_push($stack, $scope);
                        array_push($npathStack, $this->npath);
                        $this->npath = 1;
                    } else {
                        array_push($stack, NULL);
                    }
                }

                if ($token == '}') {
                    $scope = array_pop($stack);

                    if ($scope !== NULL) {
                        switch ($scope) {
                            case T_WHILE:
                            case T_DO:
                            case T_FOR:
                            case T_FOREACH:
                            case T_IF:
                            case T_TRY:
                            case T_SWITCH: {
                                $this->npath = ($this->npath + 1) * array_pop($npathStack);
                            }
                            break;

                            case T_ELSE:
                            case T_CATCH:
                            case T_CASE: {
                                $this->npath = ($this->npath - 1) + array_pop($npathStack);
                            }
                            break;
                        }
                    }
                }

                continue;
            }

            list ($token, $value) = $token;

            switch ($token) {
                case T_WHILE:
                case T_DO:
                case T_FOR:
                case T_FOREACH:
                case T_IF:
                case T_TRY:
                case T_SWITCH:
                case T_ELSE:
                case T_CATCH:
                case T_CASE: {
                    $scope = $token;
                }
                break;
            }
        }
    }

    /**
     * Calculates the Code Coverage for the method.
     *
     * @param  array $codeCoverage
     * @access protected
     */
    protected function calculateCodeCoverage(&$codeCoverage)
    {
        $statistics = PHPUnit_Util_CodeCoverage::getStatistics(
          $codeCoverage,
          $this->function->getFileName(),
          $this->function->getStartLine(),
          $this->function->getEndLine()
        );

        $this->coverage      = $statistics['coverage'];
        $this->loc           = $statistics['loc'];
        $this->locExecutable = $statistics['locExecutable'];
        $this->locExecuted   = $statistics['locExecuted'];
    }

    /**
     * Calculates the Change Risk Analysis and Predictions (CRAP) index for the
     * method.
     *
     * @access public
     */
    protected function calculateCrapIndex()
    {
        if ($this->coverage == 0) {
            $this->crap = pow($this->ccn, 2) + $this->ccn;
        }

        else if ($this->coverage >= 95) {
            $this->crap = $this->ccn;
        }

        else {
            $this->crap = pow($this->ccn, 2) * (pow(1 - $this->coverage/100, 3) + $this->ccn);
        }
    }

    /**
     * Calculates the dependencies for this function or method.
     *
     * @access public
     */
    protected function calculateDependencies()
    {
        foreach ($this->function->getParameters() as $parameter) {
            try {
                $class = $parameter->getClass();

                if ($class) {
                    $className = $class->getName();

                    if ($className != $this->scope && !in_array($className, $this->dependencies)) {
                        $this->dependencies[] = $className;
                    }
                }
            }

            catch (ReflectionException $e) {
            }
        }

        $inNew = FALSE;

        foreach ($this->tokens as $token) {
            if (is_string($token)) {
                if (trim($token) == ';') {
                    $inNew = FALSE;
                }

                continue;
            }

            list ($token, $value) = $token;

            switch ($token) {
                case T_NEW: {
                    $inNew = TRUE;
                }
                break;

                case T_STRING: {
                    if ($inNew) {
                        if ($value != $this->scope && class_exists($value, FALSE)) {
                            try {
                                $class = new ReflectionClass($value);

                                if ($class->isUserDefined() && !in_array($value, $this->dependencies)) {
                                    $this->dependencies[] = $value;
                                }
                            }

                            catch (ReflectionException $e) {
                            }
                        }
                    }

                    $inNew = FALSE;
                }
                break;
            }
        }
    }
}
?>
