1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Allow properties to be set regardless of visibility

This commit is contained in:
Matthew Brown 2019-03-01 17:30:55 -05:00
parent 4bbe13e28f
commit 5c76ab35c8
7 changed files with 104 additions and 35 deletions

View File

@ -23,6 +23,7 @@
<file name="vendor/phpunit/phpunit/src/Framework/TestCase.php"/> <file name="vendor/phpunit/phpunit/src/Framework/TestCase.php"/>
<file name="vendor/symfony/console/Command/Command.php" /> <file name="vendor/symfony/console/Command/Command.php" />
<file name="src/Psalm/Internal/Traverser/CustomTraverser.php"/> <file name="src/Psalm/Internal/Traverser/CustomTraverser.php"/>
<file name="vendor/felixfbecker/advanced-json-rpc/lib/Dispatcher.php" />
<file name="tests/performance/a.test"/> <file name="tests/performance/a.test"/>
<file name="tests/performance/b.test"/> <file name="tests/performance/b.test"/>
<file name="tests/ErrorBaselineTest.php"/> <file name="tests/ErrorBaselineTest.php"/>

View File

@ -965,7 +965,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new PropertyNotSetInConstructor( new PropertyNotSetInConstructor(
'Property ' . $property_id . ' is not defined in constructor of ' . 'Property ' . $property_id . ' is not defined in constructor of ' .
$this->fq_class_name . ' or in any private methods called in the constructor', $this->fq_class_name . ' or in any methods called in the constructor',
$property_storage->location, $property_storage->location,
$property_id $property_id
), ),

View File

@ -269,20 +269,22 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
* *
* @return void * @return void
*/ */
public function getMethodMutations($method_id, Context $this_context) public function getMethodMutations($method_id, Context $this_context, bool $from_project_analyzer = false)
{ {
list($fq_class_name, $method_name) = explode('::', $method_id); list($fq_class_name, $method_name) = explode('::', $method_id);
if (isset($this->class_analyzers_to_analyze[strtolower($fq_class_name)])) { if (isset($this->class_analyzers_to_analyze[strtolower($fq_class_name)])) {
$class_analyzer_to_examine = $this->class_analyzers_to_analyze[strtolower($fq_class_name)]; $class_analyzer_to_examine = $this->class_analyzers_to_analyze[strtolower($fq_class_name)];
} else { } else {
$this->project_analyzer->getMethodMutations($method_id, $this_context); if (!$from_project_analyzer) {
$this->project_analyzer->getMethodMutations($method_id, $this_context);
}
return; return;
} }
$call_context = new Context($this_context->self); $call_context = new Context($this_context->self);
$call_context->collect_mutations = true; $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->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;

View File

@ -846,7 +846,7 @@ class ProjectAnalyzer
$this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name); $this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
} }
$file_analyzer->getMethodMutations($appearing_method_id, $this_context); $file_analyzer->getMethodMutations($appearing_method_id, $this_context, true);
$file_analyzer->class_analyzers_to_analyze = []; $file_analyzer->class_analyzers_to_analyze = [];
$file_analyzer->interface_analyzers_to_analyze = []; $file_analyzer->interface_analyzers_to_analyze = [];

View File

@ -105,43 +105,48 @@ class CallAnalyzer
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if (!$declaring_method_id) { if (isset($context->vars_in_scope['$this'])) {
if (isset($context->vars_in_scope['$this'])) { foreach ($context->vars_in_scope['$this']->getTypes() as $atomic_type) {
foreach ($context->vars_in_scope['$this']->getTypes() as $atomic_type) { if ($atomic_type instanceof TNamedObject) {
if ($atomic_type instanceof TNamedObject) { if ($fq_class_name === $atomic_type->value) {
$alt_declaring_method_id = $declaring_method_id;
} else {
$fq_class_name = $atomic_type->value; $fq_class_name = $atomic_type->value;
$method_id = $fq_class_name . '::' . strtolower($method_name); $method_id = $fq_class_name . '::' . strtolower($method_name);
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); $alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
}
if ($declaring_method_id) { if ($alt_declaring_method_id) {
break; $declaring_method_id = $alt_declaring_method_id;
} break;
}
if (!$atomic_type->extra_types) { if (!$atomic_type->extra_types) {
continue; continue;
} }
foreach ($atomic_type->extra_types as $intersection_type) { foreach ($atomic_type->extra_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject) { if ($intersection_type instanceof TNamedObject) {
$fq_class_name = $intersection_type->value; $fq_class_name = $intersection_type->value;
$method_id = $fq_class_name . '::' . strtolower($method_name); $method_id = $fq_class_name . '::' . strtolower($method_name);
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); $alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if ($declaring_method_id) { if ($alt_declaring_method_id) {
break; $declaring_method_id = $alt_declaring_method_id;
} break 2;
} }
} }
} }
} }
} }
}
if (!$declaring_method_id) { if (!$declaring_method_id) {
// can happen for __call // can happen for __call
return; return;
}
} }
if (isset($context->initialized_methods[$declaring_method_id])) { if (isset($context->initialized_methods[$declaring_method_id])) {
@ -158,9 +163,7 @@ class CallAnalyzer
$class_analyzer = $source->getSource(); $class_analyzer = $source->getSource();
if ($class_analyzer instanceof ClassLikeAnalyzer && if ($class_analyzer instanceof ClassLikeAnalyzer && !$method_storage->is_static) {
($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 = [];
@ -176,7 +179,16 @@ class CallAnalyzer
} }
} }
$class_analyzer->getMethodMutations(strtolower($method_name), $context); if ($fq_class_name === $source->getFQCLN()) {
$class_analyzer->getMethodMutations(strtolower($method_name), $context);
} else {
list($declaring_fq_class_name) = explode('::', $declaring_method_id);
$old_self = $context->self;
$context->self = $declaring_fq_class_name;
$project_analyzer->getMethodMutations($declaring_method_id, $context);
$context->self = $old_self;
}
foreach ($local_vars_in_scope as $var => $type) { foreach ($local_vars_in_scope as $var => $type) {
$context->vars_in_scope[$var] = $type; $context->vars_in_scope[$var] = $type;

View File

@ -92,6 +92,7 @@ class MethodMutationTest extends TestCase
new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php');
$this->project_analyzer->getCodebase()->scanFiles(); $this->project_analyzer->getCodebase()->scanFiles();
$method_context = new Context(); $method_context = new Context();
$method_context->collect_mutations = true;
$this->project_analyzer->getMethodMutations('FooController::barBar', $method_context); $this->project_analyzer->getMethodMutations('FooController::barBar', $method_context);
$this->assertSame('UserViewData', (string)$method_context->vars_in_scope['$this->user_viewdata']); $this->assertSame('UserViewData', (string)$method_context->vars_in_scope['$this->user_viewdata']);
@ -129,6 +130,7 @@ class MethodMutationTest extends TestCase
new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php');
$this->project_analyzer->getCodebase()->scanFiles(); $this->project_analyzer->getCodebase()->scanFiles();
$method_context = new Context(); $method_context = new Context();
$method_context->collect_mutations = true;
$this->project_analyzer->getMethodMutations('FooController::__construct', $method_context); $this->project_analyzer->getMethodMutations('FooController::__construct', $method_context);
$this->assertSame('Foo', (string)$method_context->vars_in_scope['$this->foo']); $this->assertSame('Foo', (string)$method_context->vars_in_scope['$this->foo']);
@ -165,6 +167,7 @@ class MethodMutationTest extends TestCase
new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php');
$this->project_analyzer->getCodebase()->scanFiles(); $this->project_analyzer->getCodebase()->scanFiles();
$method_context = new Context(); $method_context = new Context();
$method_context->collect_mutations = true;
$this->project_analyzer->getMethodMutations('FooController::__construct', $method_context); $this->project_analyzer->getMethodMutations('FooController::__construct', $method_context);
$this->assertSame('Foo', (string)$method_context->vars_in_scope['$this->foo']); $this->assertSame('Foo', (string)$method_context->vars_in_scope['$this->foo']);

View File

@ -613,8 +613,7 @@ class PropertyTypeTest extends TestCase
} }
class B extends A { class B extends A {
public function foo(): void public function foo(): void {
{
$this->bar = "hello"; $this->bar = "hello";
} }
}', }',
@ -623,6 +622,29 @@ class PropertyTypeTest extends TestCase
'PropertyNotSetInConstructor' => Config::REPORT_INFO, 'PropertyNotSetInConstructor' => Config::REPORT_INFO,
], ],
], ],
'callsPrivateParentMethodThenUsesParentInitializedProperty' => [
'<?php
abstract class A {
/** @var string */
public $bar;
public function __construct() {
$this->setBar();
}
private function setBar(): void {
$this->bar = "hello";
}
}
class B extends A {
public function __construct() {
parent::__construct();
echo $this->bar;
}
}',
],
'setInFinalMethod' => [ 'setInFinalMethod' => [
'<?php '<?php
class C class C
@ -1421,6 +1443,31 @@ class PropertyTypeTest extends TestCase
} }
}' }'
], ],
'propertySetInProtectedMethodWithConstant' => [
'<?php
class A {
/** @var int */
public $a;
public function __construct() {
$this->foo();
}
protected function foo(): void {
$this->a = 5;
}
}
class B extends A {
const HELLO = "HELLO";
protected function foo() : void {
$this->a = 6;
echo self::HELLO;
}
}',
],
]; ];
} }
@ -1843,6 +1890,10 @@ class PropertyTypeTest extends TestCase
protected function foo(): void { protected function foo(): void {
$this->a = 5; $this->a = 5;
} }
}
class B extends A {
protected function foo() : void {}
}', }',
'error_message' => 'PropertyNotSetInConstructor', 'error_message' => 'PropertyNotSetInConstructor',
], ],
@ -1934,7 +1985,7 @@ class PropertyTypeTest extends TestCase
} }
} }
}', }',
'error_message' => 'PropertyNotSetInConstructor', 'error_message' => 'InaccessibleProperty',
], ],
'privatePropertySetInParentConstructor' => [ 'privatePropertySetInParentConstructor' => [
'<?php '<?php