<?php
/**
* A class for performing code analysis for php scripts
* It is designed to be the heart of a code limiting script
* to use with Savant {@link http://phpsavant.com}
*
* This code should be php4 compatiable but i've only run it in php5 and some of the Tokenizer constants have changed
*
* @author	Joshua Eichorn <josh@bluga.net>
* @copyright	Joshua Eichorn 2004
* @package	PHPCodeAnalyzer
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* @license http://www.gnu.org/copyleft/lesser.html LGPL
*/

/**#@+
* compat tokeniezer defines
*/
if (! defined('T_OLD_FUNCTION')) {
    define('T_OLD_FUNCTION', T_FUNCTION);
}
if (!defined('T_ML_COMMENT')) {
    define('T_ML_COMMENT', T_COMMENT);
} else {
    defined('T_DOC_COMMENT') ? null : define('T_DOC_COMMENT', T_ML_COMMENT);
}
/**#@-*/

/**
* Code Analysis class
*
* Example Usage:
* <code>
* $analyzer = new PHPCodeAnalyzer();
* $analyzer->source = file_get_contents(__FILE__);
* $analyzer->analyze();
* print_r($analyzer->calledMethods);
* </code>
*
* @todo is it important to grab the details from creating new functions defines classes?
* @todo support php5 only stuff like interface
*
* @version	0.4
* @license http://www.gnu.org/copyleft/lesser.html LGPL
* @copyright	Joshua Eichorn 2004
* @package	PHPCodeAnalyzer
* @author	Joshua Eichorn <josh@bluga.net>
*/
class PHPCodeAnalyzer
{
    /**
	* Source code to analyze
	*/
    var $source = "";

    /**
	* functions called
	*/
    var $calledFunctions = array();

    /**
	* Called constructs
	*/
    var $calledConstructs = array();

    /**
	* methods called
	*/
    var $calledMethods = array();

    /**
	* static methods called
	*/
    var $calledStaticMethods = array();

    /**
	* new classes instantiated 
	*/
    var $classesInstantiated = array();

    /**
	* variables used
	*/
    var $usedVariables = array();

    /**
	* member variables used
	*/
    var $usedMemberVariables = array();

    /**
	* classes created
	*/
    var $createdClasses = array();

    /**
	* functions created
	*/
    var $createdFunctions = array();

    /**
	* Files includes or requried
	*/
    var $filesIncluded = array();

    // private variables
    /**#@+
    * @access private
    */
    var $currentString = null;
    var $currentStrings = null;
    var $currentVar = false;
    var $staticClass = false;
    var $inNew = false;
    var $inInclude = false;
    var $lineNumber = 1;
    var $_previousToken = null;
    /**#@-*/

    /**
	* parse source filling informational arrays
	*/
    function analyze()
    {
        $tokens = token_get_all($this->source);

        // mapping of token to method to call
        $handleMap = array(
        T_STRING => 'handleString',
        T_CONSTANT_ENCAPSED_STRING => 'handleString',
        T_ENCAPSED_AND_WHITESPACE => 'handleString',
        T_CHARACTER => 'handleString',
        T_NUM_STRING => 'handleString',
        T_DNUMBER => 'handleString',
        T_FUNC_C => 'handleString',
        T_CLASS_C => 'handleString',
        T_FILE => 'handleString',
        T_LINE => 'handleString',
        T_DOUBLE_ARROW => 'handleString',

        T_ARRAY => 'handleClearStrings',
        T_CONCAT_EQUAL => 'handleClearStrings',

        T_DOUBLE_COLON => 'handleDoubleColon',
        T_NEW => 'handleNew',
        T_OBJECT_OPERATOR => 'handleObjectOperator',
        T_VARIABLE => 'handleVariable',
        T_FUNCTION => 'handleFunction',
        T_OLD_FUNCTION => 'handleFunction',
        T_CLASS => 'handleClass',
        T_WHITESPACE => 'handleWhitespace',
        T_INLINE_HTML => 'handleWhitespace',
        T_OPEN_TAG => 'handleWhitespace',
        T_CLOSE_TAG => 'handleWhitespace',

        T_AS	=> 'handleAs',

        T_ECHO => 'handleConstruct',
        T_EVAL => 'handleConstruct',
        T_UNSET => 'handleConstruct',
        T_ISSET => 'handleConstruct',
        T_PRINT => 'handleConstruct',
        T_FOR	=> 'handleConstruct',
        T_FOREACH=> 'handleConstruct',
        T_EMPTY	=> 'handleConstruct',
        T_EXIT	=> 'handleConstruct',
        T_CASE	=> 'handleConstruct',
        T_GLOBAL=> 'handleConstruct',
        T_UNSET	=> 'handleConstruct',
        T_WHILE	=> 'handleConstruct',
        T_DO	=> 'handleConstruct',
        T_IF	=> 'handleConstruct',
        T_LIST	=> 'handleConstruct',
        T_RETURN=> 'handleConstruct',
        T_STATIC=> 'handleConstruct',
        T_ENDFOR=> 'handleConstruct',
        T_ENDFOREACH=> 'handleConstruct',
        T_ENDIF=> 'handleConstruct',
        T_ENDSWITCH=> 'handleConstruct',
        T_ENDWHILE=> 'handleConstruct',

        T_INCLUDE => 'handleInclude',
        T_INCLUDE_ONCE => 'handleInclude',
        T_REQUIRE => 'handleInclude',
        T_REQUIRE_ONCE => 'handleInclude',
        );

        foreach($tokens as $token)
        {
            if (is_string($token))
            {
                // we have a simple 1-character token
                $this->handleSimpleToken($token);
            }
            else
            {
                list($id, $text) = $token;
                if (isset($handleMap[$id]))
                {
                    $call = $handleMap[$id];
                    $this->$call($id,$text);
                }
                /*else * /
                {
                echo token_name($id).": $text<br>\n";
                } /* */
            }
        }
    }

    /**
	* Handle a 1 char token
	* @access private
	*/
    function handleSimpleToken($token)
    {
        if ($token !== ";")
        {
            $this->currentStrings .= $token;
        }
        switch($token)
        {
            case "(":
                // method is called
                if ($this->staticClass !== false)
                {
                    if (!isset($this->calledStaticMethods[$this->staticClass][$this->currentString]))
                    {
                        $this->calledStaticMethods[$this->staticClass][$this->currentString]
                        = array();
                    }
                    $this->calledStaticMethods[$this->staticClass][$this->currentString][]
                    = $this->lineNumber;
                    $this->staticClass = false;
                }
                else if ($this->currentVar !== false)
                {
                    if (!isset($this->calledMethods[$this->currentVar][$this->currentString]))
                    {
                        $this->calledMethods[$this->currentVar][$this->currentString] = array();
                    }
                    $this->calledMethods[$this->currentVar][$this->currentString][] = $this->lineNumber;
                    $this->currentVar = false;
                }
                else if ($this->inNew !== false)
                {
                    $this->classInstantiated();
                }
                else if ($this->currentString !== null)
                {
                    $this->functionCalled();
                }
                //$this->currentString = null;
                break;
            case "=":
            case ";":
            case "?":
            case ":":
                if ($this->inNew !== false)
                {
                    $this->classInstantiated();
                }
                else if ($this->inInclude !== false)
                {
                    $this->fileIncluded();
                }
                else if ($this->currentVar !== false)
                {
                    $this->useMemberVar();
                }
                $this->currentString = null;
                $this->currentStrings = null;
                break;

            case ']':
                $this->useMemberVar(false);
                break;
            case '{':
                if($this->_previousToken == ')'){
                    $this->currentVar = false;
                    $this->currentString = null;
                }
                break;
        }
        $this->_previousToken = $token;
    }

    /**
	* handle includes and requires
	* @access private
	*/
    function handleInclude($id,$text)
    {
        $this->inInclude = true;
        $this->handleConstruct($id,$text);
    }

    /**
	* handle String tokens
	* @access private
	*/
    function handleString($id,$text)
    {
        $this->currentString = $text;
        $this->currentStrings .= $text;
    }

    /**
	* handle String tokens
	* @access private
	*/
    function handleClearStrings($id,$text)
    {
        $this->currentString = '';
    }

    /**
	* handle variables
	* @access private
	*/
    function handleVariable($id,$text)
    {
        $this->currentString = $text;
        $this->currentStrings .= $text;
        $this->useVariable();
    }


    /**
	* handle Double Colon tokens
	* @access private
	*/
    function handleDoubleColon($id,$text)
    {
        $this->staticClass = $this->currentString;
        $this->currentString = null;
    }

    /**
	* handle new keyword
	* @access private
	*/
    function handleNew($id,$text)
    {
        $this->inNew = true;
    }

    /**
	* handle function
	* @access private
	*/
    function handleFunction($id,$text)
    {
        $this->createdFunctions[] = $this->lineNumber;
    }

    /**
	* handle class
	* @access private
	*/
    function handleClass($id,$text)
    {
        $this->createdClasses[] = $this->lineNumber;
    }

    /**
	* Handle ->
	* @access private
	*/
    function handleObjectOperator($id,$text)
    {
        $this->currentVar = $this->currentString;
        $this->currentString = null;
        $this->currentStrings .= $text;
    }

    /**
	* handle whitespace to figure out line counts
	* @access private
	*/
    function handleWhitespace($id,$text)
    {
        $this->lineNumber+=substr_count($text,"\n");
        if ($id == T_CLOSE_TAG)
        {
            $this->handleSimpleToken(";");
        }
    }


    /**
	* as has been used we must have a var before it
	*
	* @access private
	*/
    function handleAs($id,$text)
    {
        $this->handleSimpleToken(";");
    }

    /**
	* a language construct has been called record it
	* @access private
	*/
    function handleConstruct($id,$construct)
    {
        if (!isset($this->calledConstructs[$construct]))
        {
            $this->calledConstructs[$construct] = array();
        }
        $this->calledConstructs[$construct][] = $this->lineNumber;
        $this->currentString = null;
    }

    /**
	* a class was Instantiated record it
	* @access private
	*/
    function classInstantiated()
    {
        if (!isset($this->classesInstantiated[$this->currentString]))
        {
            $this->classesInstantiated[$this->currentString] = array();
        }
        $this->classesInstantiated[$this->currentString][] = $this->lineNumber;
        $this->inNew = false;
    }

    /**
	* a file was included record it
	* @access private
	*/
    function fileIncluded()
    {
        if (!isset($this->filesIncluded[$this->currentStrings]))
        {
            $this->filesIncluded[$this->currentStrings] = array();
        }
        $this->filesIncluded[$this->currentStrings][] = $this->lineNumber;
        $this->inInclude = false;
        $this->currentString = null;
        $this->currentStrings = "";
    }

    /**
	* a function was called record it
	* @access private
	*/
    function functionCalled($id = false)
    {
        $function_name = trim(rtrim($this->currentStrings,'('));
        if(empty($this->currentStrings) || substr($function_name,-1) != ']'){
            $function_name = $this->currentString;
            if(strstr('"\'',substr($function_name,-1))){
                $this->currentString = null;
                return ;
            }
        }

        if (!isset($this->calledFunctions[$function_name]))
        {
            $this->calledFunctions[$function_name] = array();
        }

        $this->calledFunctions[$function_name][] = $this->lineNumber;
        $this->currentString = null;
    }

    /**
	* we used a member variable record it
	* @access private
	*/
    function useMemberVar($reset = true)
    {
        if (!isset($this->usedMemberVariables[$this->currentVar][$this->currentString])){
            $this->usedMemberVariables[$this->currentVar][$this->currentString] = array();
        }
        $this->usedMemberVariables[$this->currentVar][$this->currentString][] = $this->lineNumber;
        if($reset){
            $this->currentVar = false;
            $this->currentString = null;
        }
    }

    /**
	* we used a variable record it
	* @access private
	*/
    function useVariable()
    {
        if (!isset($this->usedVariables[$this->currentString]))
        {
            $this->usedVariables[$this->currentString] = array();
        }
        $this->usedVariables[$this->currentString][] = $this->lineNumber;
    }
}
?>