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; 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 * @var bool
*/ */
public $collect_initializations = false; 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 * Stored to prevent re-analysing methods when checking for initialised properties
* *

View File

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

View File

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

View File

@ -204,7 +204,12 @@ class CallAnalyzer
$class_analyzer = $source->getSource(); $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_in_scope = [];
$local_vars_possibly_in_scope = []; $local_vars_possibly_in_scope = [];

View File

@ -1513,7 +1513,7 @@ class PropertyTypeTest extends TestCase
parent::__construct(); parent::__construct();
} }
protected function overriddenByB(): void { protected final function overriddenByB(): void {
$this->foo = 1; $this->foo = 1;
$this->bar = 1; $this->bar = 1;
} }
@ -1538,7 +1538,7 @@ class PropertyTypeTest extends TestCase
/** @var int */ /** @var int */
protected $bar; protected $bar;
protected function overriddenByB(): void { protected final function overriddenByB(): void {
$this->foo = 1; $this->foo = 1;
$this->bar = 1; $this->bar = 1;
} }
@ -3040,6 +3040,25 @@ class PropertyTypeTest extends TestCase
}', }',
'error_message' => 'InvalidPropertyAssignmentValue' '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; return $this->options;
} }
function setOptions(array $options): void { public final function setOptions(array $options): void {
$this->options = $options; $this->options = $options;
} }
function setDefaultOptions(array $defaultOptions): void { public final function setDefaultOptions(array $defaultOptions): void {
$this->defaultOptions = $defaultOptions; $this->defaultOptions = $defaultOptions;
} }
}', }',