1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 13:51:54 +01:00
psalm/src/Psalm/FunctionChecker.php

239 lines
8.4 KiB
PHP
Raw Normal View History

2016-01-07 18:28:27 -05:00
<?php
2016-07-25 18:37:44 -04:00
namespace Psalm;
2016-01-07 18:28:27 -05:00
ini_set('xdebug.max_nesting_level', 512);
2016-01-07 18:28:27 -05:00
2016-02-04 09:22:46 -05:00
use PhpParser;
2016-01-07 18:28:27 -05:00
class FunctionChecker implements StatementsSource
2016-01-07 18:28:27 -05:00
{
protected $_function;
protected $_aliased_classes = [];
protected $_namespace;
protected $_file_name;
2016-01-11 17:18:19 -05:00
protected $_is_static = false;
protected $_absolute_class;
protected $_statements_checker;
protected $_source;
2016-05-16 16:12:02 -04:00
protected $_return_vars_in_scope = [];
protected $_return_vars_possibly_in_scope = [];
/**
* @var array
*/
protected $_suppressed_issues;
2016-05-16 18:10:59 -04:00
protected static $_no_effects_hashes = [];
protected $_function_params = [];
2016-01-07 18:28:27 -05:00
public function __construct(PhpParser\Node\FunctionLike $function, StatementsSource $source)
2016-01-07 18:28:27 -05:00
{
$this->_function = $function;
$this->_aliased_classes = $source->getAliasedClasses();
$this->_namespace = $source->getNamespace();
$this->_class_name = $source->getClassName();
$this->_class_extends = $source->getParentClass();
$this->_file_name = $source->getFileName();
$this->_absolute_class = $source->getAbsoluteClass();
$this->_source = $source;
$this->_suppressed_issues = $source->getSuppressedIssues();
2016-01-07 18:28:27 -05:00
}
public function check(Context $context, $check_methods = true)
2016-01-07 18:28:27 -05:00
{
if ($this->_function->stmts) {
2016-06-20 01:29:30 -04:00
$has_context = (bool) count($context->vars_in_scope);
2016-06-14 19:22:29 -04:00
if ($this instanceof ClassMethodChecker) {
if (ClassChecker::getThisClass()) {
$hash = $this->getMethodId() . json_encode([$context->vars_in_scope, $context->vars_possibly_in_scope]);
2016-05-16 18:10:59 -04:00
2016-06-14 19:22:29 -04:00
// if we know that the function has no effects on vars, we don't bother rechecking
if (isset(self::$_no_effects_hashes[$hash])) {
list($context->vars_in_scope, $context->vars_possibly_in_scope) = self::$_no_effects_hashes[$hash];
2016-05-20 00:24:26 -04:00
2016-06-14 19:22:29 -04:00
return;
}
}
else {
2016-08-08 14:36:18 -04:00
$context->vars_in_scope['this'] = new Type\Union([new Type\Atomic($context->self)]);
2016-05-16 18:10:59 -04:00
}
}
$statements_checker = new StatementsChecker($this, $has_context, $check_methods);
if ($this->_function instanceof PhpParser\Node\Stmt\ClassMethod) {
$method_params = ClassMethodChecker::getMethodParams($this->getMethodId());
foreach ($method_params as $method_param) {
2016-08-09 15:02:30 -04:00
$context->vars_in_scope[$method_param['name']] = StatementsChecker::fleshOutTypes(
clone $method_param['type'],
[],
$context->self,
$this->getMethodId()
);
$statements_checker->registerVariable($method_param['name'], $this->_function->getLine());
}
}
else {
// @todo deprecate this code
foreach ($this->_function->params as $param) {
if ($param->type) {
if ($param->type instanceof PhpParser\Node\Name) {
if (!in_array($param->type->parts[0], ['self', 'parent'])) {
ClassChecker::checkClassName($param->type, $this->_namespace, $this->_aliased_classes, $this->_file_name, $this->_suppressed_issues);
}
2016-01-08 18:30:51 -05:00
}
}
2016-01-11 11:05:24 -05:00
$is_nullable = $param->default !== null &&
$param->default instanceof \PhpParser\Node\Expr\ConstFetch &&
$param->default->name instanceof PhpParser\Node\Name &&
$param->default->name->parts = ['null'];
if ($param->type) {
if ($param->type instanceof Type) {
$context->vars_in_scope[$param->name] = clone $param->type;
}
else {
if (is_string($param->type)) {
$param_type_string = $param->type;
}
elseif ($param->type instanceof PhpParser\Node\Name) {
$param_type_string = $param->type->parts === ['self']
? $this->_absolute_class
: ClassChecker::getAbsoluteClassFromName($param->type, $this->_namespace, $this->_aliased_classes);
}
if ($is_nullable) {
$param_type_string .= '|null';
}
$context->vars_in_scope[$param->name] = Type::parseString($param_type_string);
}
}
else {
$context->vars_in_scope[$param->name] = Type::getMixed();
}
2016-04-30 14:14:22 -04:00
$context->vars_possibly_in_scope[$param->name] = true;
$statements_checker->registerVariable($param->name, $param->getLine());
}
2016-01-11 09:27:34 -05:00
}
2016-01-07 23:40:29 -05:00
$statements_checker->check($this->_function->stmts, $context);
2016-05-16 16:12:02 -04:00
if (isset($this->_return_vars_in_scope[''])) {
$context->vars_in_scope = TypeChecker::combineKeyedTypes($context->vars_in_scope, $this->_return_vars_in_scope['']);
2016-05-16 16:12:02 -04:00
}
if (isset($this->_return_vars_possibly_in_scope[''])) {
$context->vars_possibly_in_scope = array_merge($context->vars_possibly_in_scope, $this->_return_vars_possibly_in_scope['']);
2016-05-16 16:12:02 -04:00
}
foreach ($context->vars_in_scope as $var => $type) {
2016-05-16 16:12:02 -04:00
if (strpos($var, 'this->') !== 0) {
unset($context->vars_in_scope[$var]);
2016-05-16 16:12:02 -04:00
}
}
foreach ($context->vars_possibly_in_scope as $var => $type) {
2016-05-16 16:12:02 -04:00
if (strpos($var, 'this->') !== 0) {
unset($context->vars_possibly_in_scope[$var]);
2016-05-16 16:12:02 -04:00
}
}
2016-05-16 18:10:59 -04:00
if (ClassChecker::getThisClass() && $this instanceof ClassMethodChecker) {
self::$_no_effects_hashes[$hash] = [$context->vars_in_scope, $context->vars_possibly_in_scope];
2016-05-16 18:10:59 -04:00
}
2016-05-16 16:12:02 -04:00
}
}
2016-06-15 20:16:40 -04:00
/**
* Adds return types for the given function
* @param string $return_type
* @param array<Type> $context->vars_in_scope
* @param array<bool> $context->vars_possibly_in_scope
2016-06-15 20:16:40 -04:00
*/
public function addReturnTypes($return_type, Context $context)
2016-05-16 16:12:02 -04:00
{
if (isset($this->_return_vars_in_scope[$return_type])) {
$this->_return_vars_in_scope[$return_type] = TypeChecker::combineKeyedTypes($context->vars_in_scope, $this->_return_vars_in_scope[$return_type]);
2016-05-16 16:12:02 -04:00
}
else {
$this->_return_vars_in_scope[$return_type] = $context->vars_in_scope;
2016-05-16 16:12:02 -04:00
}
if (isset($this->_return_vars_possibly_in_scope[$return_type])) {
$this->_return_vars_possibly_in_scope[$return_type] = array_merge($context->vars_possibly_in_scope, $this->_return_vars_possibly_in_scope[$return_type]);
2016-05-16 16:12:02 -04:00
}
else {
$this->_return_vars_possibly_in_scope[$return_type] = $context->vars_possibly_in_scope;
2016-01-07 23:40:29 -05:00
}
2016-01-07 18:28:27 -05:00
}
2016-04-26 18:42:48 -04:00
/**
* @return null|string
2016-04-26 18:42:48 -04:00
*/
public function getMethodId()
{
if ($this->_function instanceof PhpParser\Node\Expr\Closure) {
return null;
}
return $this->getAbsoluteClass() . '::' . $this->_function->name;
}
public function getNamespace()
2016-01-07 18:28:27 -05:00
{
return $this->_namespace;
2016-01-07 18:28:27 -05:00
}
public function getAliasedClasses()
2016-01-07 18:28:27 -05:00
{
return $this->_aliased_classes;
2016-01-07 18:28:27 -05:00
}
public function getAbsoluteClass()
2016-01-07 18:28:27 -05:00
{
return $this->_absolute_class;
2016-01-07 18:28:27 -05:00
}
public function getClassName()
2016-01-07 18:28:27 -05:00
{
return $this->_class_name;
2016-01-07 18:28:27 -05:00
}
public function getClassChecker()
{
return $this->_source->getClassChecker();
}
public function getParentClass()
2016-01-07 18:28:27 -05:00
{
return $this->_class_extends;
2016-01-07 18:28:27 -05:00
}
public function getFileName()
2016-01-07 18:28:27 -05:00
{
return $this->_file_name;
2016-01-07 18:28:27 -05:00
}
public function isStatic()
2016-01-07 18:28:27 -05:00
{
return $this->_is_static;
2016-01-07 18:28:27 -05:00
}
public function getSource()
{
return $this->_source;
}
public function getSuppressedIssues()
{
return $this->_suppressed_issues;
}
2016-01-07 18:28:27 -05:00
}