mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Fix deep project checks
This commit is contained in:
parent
107d8352fc
commit
5f54a9571c
@ -86,11 +86,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
*/
|
||||
protected $property_types = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected static $this_class = null;
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, string>>|null
|
||||
*/
|
||||
@ -147,10 +142,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
|
||||
self::$file_classes[$this->source->getFilePath()][] = $fq_class_name;
|
||||
|
||||
if (self::$this_class) {
|
||||
self::$class_checkers[$fq_class_name] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -493,7 +484,19 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod &&
|
||||
strtolower($stmt->name) === strtolower($method_name)
|
||||
) {
|
||||
$method_checker = new MethodChecker($stmt, $this);
|
||||
$project_checker = $this->getFileChecker()->project_checker;
|
||||
|
||||
$method_id = $this->fq_class_name . '::' . $stmt->name;
|
||||
|
||||
if ($project_checker->canCache() && isset($project_checker->method_checkers[$method_id])) {
|
||||
$method_checker = $project_checker->method_checkers[$method_id];
|
||||
} else {
|
||||
$method_checker = new MethodChecker($stmt, $this);
|
||||
|
||||
if ($project_checker->canCache()) {
|
||||
$project_checker->method_checkers[$method_id] = $method_checker;
|
||||
}
|
||||
}
|
||||
|
||||
$method_checker->analyze($context, null, true);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) {
|
||||
@ -795,45 +798,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in deep method evaluation, we get method checkers on the current or parent
|
||||
* classes
|
||||
*
|
||||
* @param string $method_id
|
||||
* @return MethodChecker
|
||||
*/
|
||||
public static function getMethodChecker($method_id)
|
||||
{
|
||||
/**
|
||||
if (isset(self::$method_checkers[$method_id])) {
|
||||
return self::$method_checkers[$method_id];
|
||||
}
|
||||
|
||||
MethodChecker::registerClassLikeMethod($method_id);
|
||||
|
||||
$declaring_method_id = MethodChecker::getDeclaringMethodId($method_id);
|
||||
$declaring_class = explode('::', $declaring_method_id)[0];
|
||||
|
||||
$class_checker = FileChecker::getClassLikeCheckerFromClass($declaring_class);
|
||||
|
||||
if (!$class_checker) {
|
||||
throw new \InvalidArgumentException('Could not get class checker for ' . $declaring_class);
|
||||
}
|
||||
|
||||
foreach ($class_checker->class->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
||||
if ($declaring_method_id === $class_checker->fq_class_name . '::' . strtolower($stmt->name)) {
|
||||
$method_checker = new MethodChecker($stmt, $class_checker);
|
||||
self::$method_checkers[$method_id] = $method_checker;
|
||||
return $method_checker;
|
||||
}
|
||||
}
|
||||
}
|
||||
**/
|
||||
|
||||
throw new \InvalidArgumentException('Method checker not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a class/interface exists
|
||||
*
|
||||
@ -1552,25 +1516,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $this_class
|
||||
* @return void
|
||||
*/
|
||||
public static function setThisClass($this_class)
|
||||
{
|
||||
self::$this_class = $this_class;
|
||||
|
||||
self::$class_checkers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getThisClass()
|
||||
{
|
||||
return self::$this_class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method_name
|
||||
* @return string
|
||||
@ -1662,8 +1607,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
*/
|
||||
public static function clearCache()
|
||||
{
|
||||
self::$this_class = null;
|
||||
|
||||
self::$file_classes = [];
|
||||
|
||||
self::$trait_checkers = [];
|
||||
|
@ -154,9 +154,9 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
protected $namespace_checkers = [];
|
||||
|
||||
/**
|
||||
* @var Context|null
|
||||
* @var Context
|
||||
*/
|
||||
public $context = null;
|
||||
public $context;
|
||||
|
||||
/**
|
||||
* @var ProjectChecker
|
||||
@ -181,6 +181,8 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
if ($preloaded_statements) {
|
||||
$this->preloaded_statements = $preloaded_statements;
|
||||
}
|
||||
|
||||
$this->context = new Context($this->file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -189,7 +191,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
*/
|
||||
public function visit(Context $file_context = null)
|
||||
{
|
||||
$this->context = $file_context ?: new Context($this->file_name);
|
||||
$this->context = $file_context ?: $this->context;
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
@ -296,12 +298,8 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
{
|
||||
$config = Config::getInstance();
|
||||
|
||||
if ($this->context === null) {
|
||||
throw new \UnexpectedValueException('Cannot check methods without a context');
|
||||
}
|
||||
|
||||
foreach ($this->namespace_checkers as $namespace_checker) {
|
||||
$namespace_checker->analyze(clone $this->context);
|
||||
$namespace_checker->analyze(clone $this->context, $preserve_checkers);
|
||||
}
|
||||
|
||||
foreach ($this->class_checkers as $class_checker) {
|
||||
@ -361,27 +359,13 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $original_method_id
|
||||
* @param string $method_id
|
||||
* @param Context $this_context
|
||||
* @return void
|
||||
*/
|
||||
public function getMethodMutations($original_method_id, Context &$this_context)
|
||||
public function getMethodMutations($method_id, Context &$this_context)
|
||||
{
|
||||
list($fq_class_name, $method_name) = explode('::', $original_method_id);
|
||||
$declaring_method_id = (string)MethodChecker::getDeclaringMethodId($original_method_id);
|
||||
list($declaring_fq_class_name, $declaring_method_name) = explode('::', $declaring_method_id);
|
||||
|
||||
if (!isset(ClassLikeChecker::$storage[strtolower($fq_class_name)])) {
|
||||
throw new \UnexpectedValueException('Cannot locate class storage for ' . $fq_class_name);
|
||||
}
|
||||
|
||||
$this_context->collect_mutations = true;
|
||||
|
||||
if (!$this_context->self) {
|
||||
$this_context->self = $fq_class_name;
|
||||
$this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
|
||||
}
|
||||
|
||||
list($fq_class_name, $method_name) = explode('::', $method_id);
|
||||
$call_context = new Context($this->file_name, (string) $this_context->vars_in_scope['$this']);
|
||||
$call_context->collect_mutations = true;
|
||||
|
||||
@ -399,12 +383,30 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
|
||||
$call_context->vars_in_scope['$this'] = $this_context->vars_in_scope['$this'];
|
||||
|
||||
$checked = false;
|
||||
|
||||
foreach ($this->class_checkers as $class_checker) {
|
||||
if (strtolower($class_checker->getFQCLN()) === strtolower($declaring_fq_class_name)) {
|
||||
$class_checker->getMethodMutations($declaring_method_name, $call_context);
|
||||
if (strtolower($class_checker->getFQCLN()) === strtolower($fq_class_name)) {
|
||||
$class_checker->getMethodMutations($method_name, $call_context);
|
||||
$checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->namespace_checkers as $namespace_checker) {
|
||||
foreach ($namespace_checker->class_checkers as $class_checker) {
|
||||
if (strtolower($class_checker->getFQCLN()) === strtolower($fq_class_name)) {
|
||||
$class_checker->getMethodMutations($method_name, $call_context);
|
||||
$checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$checked) {
|
||||
throw new \UnexpectedValueException('Method ' . $method_id . ' could not be checked');
|
||||
}
|
||||
|
||||
foreach ($call_context->vars_in_scope as $var => $type) {
|
||||
$this_context->vars_possibly_in_scope[$var] = true;
|
||||
}
|
||||
@ -446,10 +448,6 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
*/
|
||||
public function evaluateClassLike($fq_class_name, $visit_file)
|
||||
{
|
||||
if (!$this->context) {
|
||||
throw new \UnexpectedValueException('Not expecting $this->context to be empty');
|
||||
}
|
||||
|
||||
if (isset($this->interface_checkers_no_methods[$fq_class_name])) {
|
||||
$interface_checker = $this->interface_checkers_no_methods[$fq_class_name];
|
||||
|
||||
|
@ -99,6 +99,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
$cased_method_id = null;
|
||||
|
||||
if ($this->function instanceof ClassMethod) {
|
||||
$real_method_id = (string)$this->getMethodId();
|
||||
|
||||
$method_id = (string)$this->getMethodId($context->self);
|
||||
|
||||
if ($add_mutations) {
|
||||
$hash = $this->getMethodId() . json_encode([
|
||||
$context->vars_in_scope,
|
||||
@ -118,10 +122,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
$context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($context->self)]);
|
||||
}
|
||||
|
||||
$real_method_id = (string)$this->getMethodId();
|
||||
|
||||
$method_id = (string)$this->getMethodId($context->self);
|
||||
|
||||
$declaring_method_id = (string)MethodChecker::getDeclaringMethodId($method_id);
|
||||
|
||||
if ($declaring_method_id !== $real_method_id) {
|
||||
|
@ -29,7 +29,7 @@ class NamespaceChecker extends SourceChecker implements StatementsSource
|
||||
/**
|
||||
* @var array<int, ClassChecker>
|
||||
*/
|
||||
protected $class_checkers = [];
|
||||
public $class_checkers = [];
|
||||
|
||||
/**
|
||||
* A lookup table for public namespace constants
|
||||
@ -100,15 +100,18 @@ class NamespaceChecker extends SourceChecker implements StatementsSource
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
* @param bool $preserve_checkers
|
||||
* @return void
|
||||
*/
|
||||
public function analyze(Context $context)
|
||||
public function analyze(Context $context, $preserve_checkers = false)
|
||||
{
|
||||
foreach ($this->class_checkers as $class_checker) {
|
||||
$class_checker->analyze(null, $context);
|
||||
}
|
||||
|
||||
$this->class_checkers = [];
|
||||
if (!$preserve_checkers) {
|
||||
$this->class_checkers = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@
|
||||
namespace Psalm\Checker;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Storage\PropertyStorage;
|
||||
@ -47,6 +48,11 @@ class ProjectChecker
|
||||
*/
|
||||
public $debug_output = false;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $cache = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
@ -122,6 +128,11 @@ class ProjectChecker
|
||||
*/
|
||||
protected $file_checkers = [];
|
||||
|
||||
/**
|
||||
* @var array<string, MethodChecker>
|
||||
*/
|
||||
public $method_checkers = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
@ -482,7 +493,7 @@ class ProjectChecker
|
||||
|
||||
if (isset($filetype_handlers[$extension])) {
|
||||
/** @var FileChecker */
|
||||
return new $filetype_handlers[$extension]($file_path);
|
||||
return new $filetype_handlers[$extension]($file_path, $this);
|
||||
}
|
||||
|
||||
return new FileChecker($file_path, $this);
|
||||
@ -560,7 +571,7 @@ class ProjectChecker
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fq_class_name
|
||||
* @param string $fq_class_name
|
||||
* @return boolean
|
||||
* @psalm-suppress MixedMethodCall due to Reflection class weirdness
|
||||
*/
|
||||
@ -633,6 +644,98 @@ class ProjectChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableCache()
|
||||
{
|
||||
$this->cache = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableCache()
|
||||
{
|
||||
$this->cache = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function canCache()
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $original_method_id
|
||||
* @param Context $this_context
|
||||
* @return void
|
||||
*/
|
||||
public function getMethodMutations($original_method_id, Context $this_context)
|
||||
{
|
||||
list($fq_class_name, $method_name) = explode('::', $original_method_id);
|
||||
|
||||
$file_checker = $this->getVisitedFileCheckerForClassLike($fq_class_name);
|
||||
|
||||
$declaring_method_id = (string)MethodChecker::getDeclaringMethodId($original_method_id);
|
||||
list($declaring_fq_class_name, $declaring_method_name) = explode('::', $declaring_method_id);
|
||||
|
||||
if (strtolower($declaring_fq_class_name) !== strtolower($fq_class_name)) {
|
||||
$file_checker = $this->getVisitedFileCheckerForClassLike($declaring_fq_class_name);
|
||||
}
|
||||
|
||||
$file_checker->analyze(false, true);
|
||||
|
||||
if (!$this_context->self) {
|
||||
$this_context->self = $fq_class_name;
|
||||
$this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
|
||||
}
|
||||
|
||||
$file_checker->getMethodMutations($declaring_method_id, $this_context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fq_class_name
|
||||
* @return FileChecker
|
||||
*/
|
||||
public function getVisitedFileCheckerForClassLike($fq_class_name)
|
||||
{
|
||||
if (!$this->fake_files) {
|
||||
// this registers the class if it's not user defined
|
||||
if (!$this->fileExistsForClassLike($fq_class_name)) {
|
||||
throw new \UnexpectedValueException('File does not exist for ' . $fq_class_name);
|
||||
}
|
||||
|
||||
if (!isset($this->classlike_files[$fq_class_name])) {
|
||||
throw new \UnexpectedValueException('Class ' . $fq_class_name . ' is not user-defined');
|
||||
}
|
||||
|
||||
$file_path = $this->classlike_files[$fq_class_name];
|
||||
} else {
|
||||
$file_path = array_keys($this->fake_files)[0];
|
||||
}
|
||||
|
||||
if ($this->cache && isset($this->file_checkers[$file_path])) {
|
||||
return $this->file_checkers[$file_path];
|
||||
}
|
||||
|
||||
$file_checker = new FileChecker($file_path, $this);
|
||||
|
||||
$file_checker->visit();
|
||||
|
||||
if ($this->debug_output) {
|
||||
echo 'Visiting ' . $file_path . PHP_EOL;
|
||||
}
|
||||
|
||||
if ($this->cache) {
|
||||
$this->file_checkers[$file_path] = $file_checker;
|
||||
}
|
||||
|
||||
return $file_checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Config object from an XML file.
|
||||
*
|
||||
|
@ -473,7 +473,7 @@ class CallChecker
|
||||
|
||||
$method_id = $statements_checker->getFQCLN() . '::' . strtolower($stmt->name);
|
||||
|
||||
if ($file_checker->getMethodMutations($method_id, $context) === false) {
|
||||
if ($file_checker->project_checker->getMethodMutations($method_id, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -710,7 +710,7 @@ class CallChecker
|
||||
if ($context->collect_mutations) {
|
||||
$method_id = $fq_class_name . '::' . strtolower($stmt->name);
|
||||
|
||||
if ($file_checker->getMethodMutations($method_id, $context) === false) {
|
||||
if ($file_checker->project_checker->getMethodMutations($method_id, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -196,17 +196,16 @@ class ExpressionChecker
|
||||
$use_context = new Context($statements_checker->getFileName(), $context->self);
|
||||
|
||||
if (!$statements_checker->isStatic()) {
|
||||
$this_class = ClassLikeChecker::getThisClass();
|
||||
$this_class = $this_class &&
|
||||
if ($context->collect_mutations &&
|
||||
$context->self &&
|
||||
ClassChecker::classExtends(
|
||||
$this_class,
|
||||
$context->self,
|
||||
(string)$statements_checker->getFQCLN()
|
||||
)
|
||||
? $this_class
|
||||
: $context->self;
|
||||
|
||||
if ($this_class) {
|
||||
$use_context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($this_class)]);
|
||||
) {
|
||||
$use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this'];
|
||||
} elseif ($context->self) {
|
||||
$use_context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($context->self)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ class MethodMutationTest extends PHPUnit_Framework_TestCase
|
||||
$file_checker->visit($context);
|
||||
$file_checker->analyze(false, true);
|
||||
$method_context = new Context('somefile.php');
|
||||
$file_checker->getMethodMutations('FooController::barBar', $method_context);
|
||||
$this->project_checker->getMethodMutations('FooController::barBar', $method_context);
|
||||
|
||||
$this->assertEquals('UserViewData', (string)$method_context->vars_in_scope['$this->user_viewdata']);
|
||||
$this->assertEquals('string', (string)$method_context->vars_in_scope['$this->user_viewdata->name']);
|
||||
|
Loading…
Reference in New Issue
Block a user