1
0
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:
Matthew Brown 2017-01-12 00:54:41 -05:00
parent 107d8352fc
commit 5f54a9571c
8 changed files with 167 additions and 121 deletions

View File

@ -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 = [];

View File

@ -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];

View File

@ -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) {

View File

@ -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 = [];
}
}
/**

View File

@ -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.
*

View 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;
}
}

View File

@ -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)]);
}
}

View File

@ -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']);