1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix #3201 - add additional constraints on constructor initialisation

This commit is contained in:
Matthew Brown 2020-04-21 00:04:47 -04:00
parent 8f57d0c738
commit 1b752d06ab
6 changed files with 52 additions and 8 deletions

View File

@ -170,12 +170,19 @@ class Context
public $collect_mutations = false;
/**
* Whether or not to do a deep analysis and collect initializations from private methods
* Whether or not to do a deep analysis and collect initializations from private or final methods
*
* @var bool
*/
public $collect_initializations = false;
/**
* Whether or not to do a deep analysis and collect initializations from public non-final methods
*
* @var bool
*/
public $collect_nonprivate_initializations = false;
/**
* Stored to prevent re-analysing methods when checking for initialised properties
*

View File

@ -1147,6 +1147,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$uninitialized_variables = [];
$uninitialized_properties = [];
$uninitialized_typed_properties = [];
$uninitialized_private_properties = false;
foreach ($storage->appearing_property_ids as $property_name => $appearing_property_id) {
$property_class_name = $codebase->properties->getDeclaringClassForProperty(
@ -1206,6 +1207,10 @@ class ClassAnalyzer extends ClassLikeAnalyzer
strtolower($property_class_name) . '::$' . $property_name
);
if ($property->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE) {
$uninitialized_private_properties = true;
}
$uninitialized_variables[] = '$this->' . $property_name;
$uninitialized_properties[$property_class_name . '::$' . $property_name] = $property;
@ -1305,6 +1310,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$was_collecting_initializations = $class_context->collect_initializations;
$class_context->collect_initializations = true;
$class_context->collect_nonprivate_initializations = !$uninitialized_private_properties;
$constructor_analyzer = $this->analyzeClassMethod(
$fake_stmt,
@ -1324,6 +1330,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
if ($constructor_analyzer) {
$method_context = clone $class_context;
$method_context->collect_initializations = true;
$method_context->collect_nonprivate_initializations = !$uninitialized_private_properties;
$method_context->self = $fq_class_name;
$this_atomic_object_type = new Type\Atomic\TNamedObject($fq_class_name);
@ -1379,11 +1386,16 @@ class ClassAnalyzer extends ClassLikeAnalyzer
&& $error_location
&& (!$end_type->initialized || $property_storage !== $constructor_class_property_storage)
) {
$expected_visibility = $uninitialized_private_properties
? 'private or final '
: '';
if (IssueBuffer::accepts(
new PropertyNotSetInConstructor(
'Property ' . $class_storage->name . '::$' . $property_name
. ' is not defined in constructor of ' .
$this->fq_class_name . ' and in any methods called in the constructor',
. ' is not defined in constructor of '
. $this->fq_class_name . ' and in any ' . $expected_visibility
. 'methods called in the constructor',
$error_location,
$property_id
),

View File

@ -348,6 +348,7 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
$call_context = new Context($this_context->self);
$call_context->collect_mutations = $this_context->collect_mutations;
$call_context->collect_initializations = $this_context->collect_initializations;
$call_context->collect_nonprivate_initializations = $this_context->collect_nonprivate_initializations;
$call_context->initialized_methods = $this_context->initialized_methods;
$call_context->include_location = $this_context->include_location;
$call_context->calling_method_id = $this_context->calling_method_id;

View File

@ -204,7 +204,12 @@ class CallAnalyzer
$class_analyzer = $source->getSource();
if ($class_analyzer instanceof ClassLikeAnalyzer && !$method_storage->is_static) {
if ($class_analyzer instanceof ClassLikeAnalyzer
&& !$method_storage->is_static
&& ($context->collect_nonprivate_initializations
|| $method_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|| $method_storage->final)
) {
$local_vars_in_scope = [];
$local_vars_possibly_in_scope = [];

View File

@ -1513,7 +1513,7 @@ class PropertyTypeTest extends TestCase
parent::__construct();
}
protected function overriddenByB(): void {
protected final function overriddenByB(): void {
$this->foo = 1;
$this->bar = 1;
}
@ -1538,7 +1538,7 @@ class PropertyTypeTest extends TestCase
/** @var int */
protected $bar;
protected function overriddenByB(): void {
protected final function overriddenByB(): void {
$this->foo = 1;
$this->bar = 1;
}
@ -3040,6 +3040,25 @@ class PropertyTypeTest extends TestCase
}',
'error_message' => 'InvalidPropertyAssignmentValue'
],
'overriddenConstructorCalledMethod' => [
'<?php
class ParentClass {
private string $prop;
public function __construct() {
$this->init();
}
public function init(): void {
$this->prop = "zxc";
}
}
class ChildClass extends ParentClass {
public function init(): void {}
}',
'error_message' => 'PropertyNotSetInConstructor'
],
];
}
}

View File

@ -68,11 +68,11 @@ class PureAnnotationTest extends TestCase
return $this->options;
}
function setOptions(array $options): void {
public final function setOptions(array $options): void {
$this->options = $options;
}
function setDefaultOptions(array $defaultOptions): void {
public final function setDefaultOptions(array $defaultOptions): void {
$this->defaultOptions = $defaultOptions;
}
}',