2016-01-08 00:28:27 +01:00
|
|
|
<?php
|
|
|
|
|
2016-01-08 00:36:55 +01:00
|
|
|
namespace CodeInspector;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
2016-04-17 22:20:36 +02:00
|
|
|
use CodeInspector\Exception\UndefinedMethodException;
|
2016-04-18 19:31:59 +02:00
|
|
|
use CodeInspector\Exception\InaccessibleMethodException;
|
2016-02-04 15:22:46 +01:00
|
|
|
use PhpParser;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
|
|
|
class ClassMethodChecker extends FunctionChecker
|
|
|
|
{
|
2016-04-16 22:28:25 +02:00
|
|
|
protected static $_method_comments = [];
|
|
|
|
protected static $_method_files = [];
|
|
|
|
protected static $_method_params = [];
|
|
|
|
protected static $_method_namespaces = [];
|
|
|
|
protected static $_method_return_types = [];
|
|
|
|
protected static $_static_methods = [];
|
|
|
|
protected static $_declaring_classes = [];
|
|
|
|
protected static $_existing_methods = [];
|
2016-04-17 17:22:18 +02:00
|
|
|
protected static $_have_reflected = [];
|
2016-04-17 18:27:47 +02:00
|
|
|
protected static $_have_registered = [];
|
2016-04-17 17:26:29 +02:00
|
|
|
protected static $_method_custom_calls = [];
|
2016-04-17 18:27:47 +02:00
|
|
|
protected static $_inherited_methods = [];
|
2016-04-18 19:31:59 +02:00
|
|
|
protected static $_method_visibility = [];
|
|
|
|
|
|
|
|
const VISIBILITY_PUBLIC = 1;
|
|
|
|
const VISIBILITY_PROTECTED = 2;
|
|
|
|
const VISIBILITY_PRIVATE = 3;
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
const TYPE_REGEX = '(\\\?[A-Za-z0-9\<\>\[\]|\\\]+[A-Za-z0-9\<\>\[\]]|\$[a-zA-Z_0-9\<\>\[\]]+)';
|
|
|
|
|
|
|
|
public function __construct(PhpParser\Node\FunctionLike $function, StatementsSource $source)
|
|
|
|
{
|
|
|
|
parent::__construct($function, $source);
|
|
|
|
|
|
|
|
$this->_registerMethod($function);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getMethodParams($method_id)
|
|
|
|
{
|
2016-04-18 19:31:59 +02:00
|
|
|
self::_populateData($method_id);
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
return self::$_method_params[$method_id];
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getMethodReturnTypes($method_id)
|
|
|
|
{
|
2016-04-18 19:31:59 +02:00
|
|
|
self::_populateData($method_id);
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
$return_types = self::$_method_return_types[$method_id];
|
|
|
|
|
|
|
|
return $return_types;
|
|
|
|
}
|
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
public static function extractReflectionMethodInfo($method_id)
|
2016-04-16 22:28:25 +02:00
|
|
|
{
|
2016-04-17 17:22:18 +02:00
|
|
|
if (isset(self::$_have_reflected[$method_id])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-16 22:28:25 +02:00
|
|
|
$method = new \ReflectionMethod($method_id);
|
2016-04-17 17:22:18 +02:00
|
|
|
self::$_have_reflected[$method_id] = true;
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
self::$_static_methods[$method_id] = $method->isStatic();
|
|
|
|
self::$_method_files[$method_id] = $method->getFileName();
|
|
|
|
self::$_method_namespaces[$method_id] = $method->getDeclaringClass()->getNamespaceName();
|
|
|
|
self::$_declaring_classes[$method_id] = $method->getDeclaringClass()->name;
|
2016-04-18 19:31:59 +02:00
|
|
|
self::$_method_visibility[$method_id] = $method->isPrivate() ?
|
|
|
|
self::VISIBILITY_PRIVATE :
|
|
|
|
($method->isProtected() ? self::VISIBILITY_PROTECTED : self::VISIBILITY_PUBLIC);
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
$params = $method->getParameters();
|
|
|
|
|
|
|
|
self::$_method_params[$method_id] = [];
|
|
|
|
foreach ($params as $param) {
|
|
|
|
$param_type = null;
|
|
|
|
|
|
|
|
if ($param->isArray()) {
|
|
|
|
$param_type = 'array';
|
|
|
|
|
|
|
|
} elseif ($param->getClass() && self::$_method_files[$method_id]) {
|
|
|
|
$param_type = $param->getClass()->getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
$is_nullable = false;
|
|
|
|
|
|
|
|
try {
|
|
|
|
$is_nullable = $param->getDefaultValue() === null;
|
|
|
|
}
|
|
|
|
catch (\ReflectionException $e) {
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
self::$_method_params[$method_id][] = [
|
|
|
|
'name' => $param->getName(),
|
|
|
|
'by_ref' => $param->isPassedByReference(),
|
|
|
|
'type' => $param_type,
|
|
|
|
'is_nullable' => $is_nullable
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_types = [];
|
|
|
|
|
|
|
|
$comments = StatementsChecker::parseDocComment($method->getDocComment() ?: '');
|
|
|
|
|
|
|
|
if ($comments) {
|
|
|
|
if (isset($comments['specials']['return'])) {
|
|
|
|
$return_blocks = explode(' ', $comments['specials']['return'][0]);
|
|
|
|
foreach ($return_blocks as $block) {
|
|
|
|
if ($block && preg_match('/^' . self::TYPE_REGEX . '$/', $block)) {
|
|
|
|
$return_types = explode('|', $block);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($comments['specials']['call'])) {
|
|
|
|
self::$_method_custom_calls[$method_id] = [];
|
|
|
|
|
|
|
|
$call_blocks = $comments['specials']['call'];
|
|
|
|
foreach ($comments['specials']['call'] as $block) {
|
|
|
|
if ($block) {
|
|
|
|
self::$_method_custom_calls[$method_id][] = trim($block);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_types = array_filter($return_types, function ($entry) {
|
|
|
|
return !empty($entry) && $entry !== '[type]';
|
|
|
|
});
|
|
|
|
|
|
|
|
if ($return_types) {
|
|
|
|
foreach ($return_types as &$return_type) {
|
|
|
|
$return_type = self::_fixUpReturnType($return_type, $method_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
self::$_method_return_types[$method_id] = $return_types;
|
|
|
|
}
|
|
|
|
|
2016-04-17 18:27:47 +02:00
|
|
|
protected static function _copyToChildMethod($method_id, $child_method_id)
|
2016-04-17 17:22:18 +02:00
|
|
|
{
|
2016-04-17 18:27:47 +02:00
|
|
|
if (!isset(self::$_have_registered[$method_id]) && !isset(self::$_have_reflected[$method_id])) {
|
|
|
|
self::extractReflectionMethodInfo($method_id);
|
|
|
|
}
|
|
|
|
|
2016-04-18 19:31:59 +02:00
|
|
|
if (self::$_method_visibility[$method_id] !== self::VISIBILITY_PRIVATE) {
|
|
|
|
self::$_method_files[$child_method_id] = self::$_method_files[$method_id];
|
|
|
|
self::$_method_params[$child_method_id] = self::$_method_params[$method_id];
|
|
|
|
self::$_method_namespaces[$child_method_id] = self::$_method_namespaces[$method_id];
|
|
|
|
self::$_method_return_types[$child_method_id] = self::$_method_return_types[$method_id];
|
|
|
|
self::$_static_methods[$child_method_id] = self::$_static_methods[$method_id];
|
|
|
|
self::$_method_visibility[$child_method_id] = self::$_method_visibility[$method_id];
|
2016-04-17 17:22:18 +02:00
|
|
|
|
2016-04-18 19:31:59 +02:00
|
|
|
self::$_declaring_classes[$child_method_id] = self::$_declaring_classes[$method_id];
|
|
|
|
self::$_existing_methods[$child_method_id] = 1;
|
|
|
|
}
|
2016-04-17 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
2016-04-16 22:28:25 +02:00
|
|
|
/**
|
|
|
|
* Determines whether a given method is static or not
|
|
|
|
* @param string $method_id
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public static function isGivenMethodStatic($method_id)
|
|
|
|
{
|
2016-04-18 19:31:59 +02:00
|
|
|
self::_populateData($method_id);
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
return self::$_static_methods[$method_id];
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function _registerMethod(PhpParser\Node\Stmt\ClassMethod $method)
|
|
|
|
{
|
|
|
|
$method_id = $this->_absolute_class . '::' . $method->name;
|
2016-04-17 18:27:47 +02:00
|
|
|
self::$_have_registered[$method_id] = true;
|
2016-04-16 22:28:25 +02:00
|
|
|
|
|
|
|
self::$_declaring_classes[$method_id] = $this->_absolute_class;
|
|
|
|
self::$_static_methods[$method_id] = $method->isStatic();
|
|
|
|
self::$_method_comments[$method_id] = $method->getDocComment() ?: '';
|
|
|
|
|
|
|
|
self::$_method_namespaces[$method_id] = $this->_namespace;
|
|
|
|
self::$_method_files[$method_id] = $this->_file_name;
|
|
|
|
self::$_existing_methods[$method_id] = 1;
|
2016-04-18 19:31:59 +02:00
|
|
|
self::$_method_visibility[$method_id] = $method->isPrivate() ?
|
|
|
|
self::VISIBILITY_PRIVATE :
|
|
|
|
($method->isProtected() ? self::VISIBILITY_PROTECTED : self::VISIBILITY_PUBLIC);
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
$comments = StatementsChecker::parseDocComment($method->getDocComment());
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
$return_types = [];
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
if (isset($comments['specials']['return'])) {
|
|
|
|
$return_blocks = explode(' ', $comments['specials']['return'][0]);
|
|
|
|
foreach ($return_blocks as $block) {
|
|
|
|
if ($block) {
|
|
|
|
if ($block && preg_match('/^' . self::TYPE_REGEX . '$/', $block)) {
|
|
|
|
$return_types = explode('|', $block);
|
|
|
|
break;
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-04-17 17:22:18 +02:00
|
|
|
}
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
if (isset($comments['specials']['call'])) {
|
|
|
|
self::$_method_custom_calls[$method_id] = [];
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
$call_blocks = $comments['specials']['call'];
|
|
|
|
foreach ($comments['specials']['call'] as $block) {
|
|
|
|
if ($block) {
|
|
|
|
self::$_method_custom_calls[$method_id][] = trim($block);
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
|
|
|
}
|
2016-04-17 17:22:18 +02:00
|
|
|
}
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
$return_types = array_filter($return_types, function ($entry) {
|
|
|
|
return !empty($entry) && $entry !== '[type]';
|
|
|
|
});
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
foreach ($return_types as &$return_type) {
|
|
|
|
$return_type = $this->_fixUpLocalReturnType($return_type, $method_id, $this->_namespace, $this->_aliased_classes);
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
|
|
|
|
2016-04-17 17:22:18 +02:00
|
|
|
self::$_method_return_types[$method_id] = $return_types;
|
|
|
|
|
2016-04-16 22:28:25 +02:00
|
|
|
self::$_method_params[$method_id] = [];
|
|
|
|
|
|
|
|
foreach ($method->getParams() as $param) {
|
|
|
|
$param_type = null;
|
|
|
|
|
|
|
|
if ($param->type) {
|
|
|
|
if (is_string($param->type)) {
|
|
|
|
$param_type = $param->type;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if ($param->type instanceof PhpParser\Node\Name\FullyQualified) {
|
|
|
|
$param_type = implode('\\', $param->type->parts);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$param_type = ClassChecker::getAbsoluteClassFromString(implode('\\', $param->type->parts), $this->_namespace, $this->_aliased_classes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$is_nullable = $param->default !== null &&
|
|
|
|
$param->default instanceof \PhpParser\Node\Expr\ConstFetch &&
|
|
|
|
$param->default->name instanceof PhpParser\Node\Name &&
|
|
|
|
$param->default->name->parts = ['null'];
|
|
|
|
|
|
|
|
self::$_method_params[$method_id][] = [
|
|
|
|
'name' => $param->name,
|
|
|
|
'by_ref' => $param->byRef,
|
|
|
|
'type' => $param_type,
|
|
|
|
'is_nullable' => $is_nullable
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected static function _fixUpLocalReturnType($return_type, $method_id, $namespace, $aliased_classes)
|
|
|
|
{
|
|
|
|
if (strpos($return_type, '[') !== false) {
|
|
|
|
$return_type = TypeChecker::convertSquareBrackets($return_type);
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_type_tokens = TypeChecker::tokenize($return_type);
|
|
|
|
|
|
|
|
foreach ($return_type_tokens as &$return_type_token) {
|
|
|
|
if ($return_type_token[0] === '\\') {
|
|
|
|
$return_type_token = substr($return_type_token, 1);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_array($return_type_token, ['<', '>'])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($return_type_token[0] === strtoupper($return_type_token[0])) {
|
|
|
|
$absolute_class = explode('::', $method_id)[0];
|
|
|
|
|
|
|
|
if ($return_type === '$this') {
|
|
|
|
$return_type_token = $absolute_class;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_type_token = ClassChecker::getAbsoluteClassFromString($return_type_token, $namespace, $aliased_classes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode('', $return_type_tokens);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected static function _fixUpReturnType($return_type, $method_id)
|
|
|
|
{
|
|
|
|
if (strpos($return_type, '[') !== false) {
|
|
|
|
$return_type = TypeChecker::convertSquareBrackets($return_type);
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_type_tokens = TypeChecker::tokenize($return_type);
|
|
|
|
|
|
|
|
foreach ($return_type_tokens as &$return_type_token) {
|
|
|
|
if ($return_type_token[0] === '\\') {
|
|
|
|
$return_type_token = substr($return_type_token, 1);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_array($return_type_token, ['<', '>'])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($return_type_token[0] === strtoupper($return_type_token[0])) {
|
|
|
|
$absolute_class = explode('::', $method_id)[0];
|
|
|
|
|
|
|
|
if ($return_type_token === '$this') {
|
|
|
|
$return_type_token = $absolute_class;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_type_token = FileChecker::getAbsoluteClassFromNameInFile($return_type_token, self::$_method_namespaces[$method_id], self::$_method_files[$method_id]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode('', $return_type_tokens);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function checkMethodExists($method_id, $file_name, $stmt)
|
|
|
|
{
|
|
|
|
if (isset(self::$_existing_methods[$method_id])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-18 19:31:59 +02:00
|
|
|
$method_parts = explode('::', $method_id);
|
|
|
|
|
|
|
|
if (method_exists($method_parts[0], $method_parts[1])) {
|
2016-04-16 22:28:25 +02:00
|
|
|
self::$_existing_methods[$method_id] = 1;
|
|
|
|
return;
|
2016-04-18 19:31:59 +02:00
|
|
|
}
|
2016-04-16 22:28:25 +02:00
|
|
|
|
2016-04-18 19:31:59 +02:00
|
|
|
throw new UndefinedMethodException('Method ' . $method_id . ' does not exist', $file_name, $stmt->getLine());
|
|
|
|
}
|
|
|
|
|
|
|
|
protected static function _populateData($method_id)
|
|
|
|
{
|
|
|
|
if (!isset(self::$_have_registered[$method_id]) && !isset(self::$_have_registered[$method_id])) {
|
|
|
|
if (isset(self::$_inherited_methods[$method_id])) {
|
|
|
|
self::_copyToChildMethod(self::$_inherited_methods[$method_id], $method_id);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
self::extractReflectionMethodInfo($method_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function checkMethodVisibility($method_id, $calling_context, $file_name, $line_number)
|
|
|
|
{
|
|
|
|
self::_populateData($method_id);
|
|
|
|
|
|
|
|
$method_class = explode('::', $method_id)[0];
|
|
|
|
|
|
|
|
if (!isset(self::$_method_visibility[$method_id])) {
|
|
|
|
throw new InaccessibleMethodException('Cannot access method ' . $method_id, $file_name, $line_number);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (self::$_method_visibility[$method_id]) {
|
|
|
|
case self::VISIBILITY_PUBLIC:
|
|
|
|
return;
|
|
|
|
|
|
|
|
case self::VISIBILITY_PRIVATE:
|
|
|
|
if (!$calling_context || $method_class !== $calling_context) {
|
|
|
|
throw new InaccessibleMethodException('Cannot access private method ' . $method_id, $file_name, $line_number);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
|
|
|
|
case self::VISIBILITY_PROTECTED:
|
|
|
|
if ($method_class === $calling_context) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$calling_context) {
|
|
|
|
throw new InaccessibleMethodException('Cannot access protected method ' . $method_id, $file_name, $line_number);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_subclass_of($calling_context, $method_class)) {
|
|
|
|
throw new InaccessibleMethodException('Cannot access protected method ' . $method_id . ' from context ' . $calling_context, $file_name, $line_number);
|
|
|
|
}
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
|
|
|
}
|
2016-04-17 18:27:47 +02:00
|
|
|
|
|
|
|
public static function registerInheritedMethod($parent_method_id, $method_id)
|
|
|
|
{
|
|
|
|
self::$_inherited_methods[$method_id] = $parent_method_id;
|
|
|
|
}
|
2016-02-04 15:22:46 +01:00
|
|
|
}
|