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

Add better support for intersection properties and mocks

This commit is contained in:
Matthew Brown 2018-11-24 18:31:00 -05:00
parent 10931d2d68
commit 047b096227
8 changed files with 209 additions and 76 deletions

View File

@ -511,6 +511,10 @@ class CommentAnalyzer
$info->sealed_methods = true;
}
if (isset($comments['specials']['psalm-mock-properties'])) {
$info->mocked_properties = true;
}
if (isset($comments['specials']['psalm-suppress'])) {
foreach ($comments['specials']['psalm-suppress'] as $suppress_entry) {
$info->suppressed_issues[] = preg_split('/[\s]+/', $suppress_entry)[0];

View File

@ -224,42 +224,68 @@ class PropertyAssignmentAnalyzer
return null;
}
$intersection_types = $lhs_type_part->getIntersectionTypes() ?: [];
$fq_class_name = $lhs_type_part->value;
$mocked_properties = false;
if (!$codebase->classExists($lhs_type_part->value)) {
$class_exists = false;
if ($codebase->interfaceExists($lhs_type_part->value)) {
$interface_storage = $codebase->classlike_storage_provider->get($lhs_type_part->value);
$mocked_properties = $interface_storage->mocked_properties;
foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject
&& $codebase->classExists($intersection_type->value)
) {
$fq_class_name = $intersection_type->value;
$class_exists = true;
break;
}
}
if (!$class_exists) {
if (IssueBuffer::accepts(
new NoInterfaceProperties(
'Interfaces cannot have properties',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return null;
}
}
if (!$class_exists) {
if (IssueBuffer::accepts(
new NoInterfaceProperties(
'Interfaces cannot have properties',
new CodeLocation($statements_analyzer->getSource(), $stmt)
new UndefinedClass(
'Cannot set properties of undefined class ' . $lhs_type_part->value,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$lhs_type_part->value
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
// fall through
}
return null;
}
if (IssueBuffer::accepts(
new UndefinedClass(
'Cannot set properties of undefined class ' . $lhs_type_part->value,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$lhs_type_part->value
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
return null;
}
$property_id = $lhs_type_part->value . '::$' . $prop_name;
$property_id = $fq_class_name . '::$' . $prop_name;
$property_ids[] = $property_id;
if ($codebase->methodExists($lhs_type_part->value . '::__set')
if ($codebase->methodExists($fq_class_name . '::__set')
&& (!$codebase->properties->propertyExists($property_id)
|| ($lhs_var_id !== '$this'
&& $lhs_type_part->value !== $context->self
&& $fq_class_name !== $context->self
&& ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
@ -270,7 +296,7 @@ class PropertyAssignmentAnalyzer
) !== true)
)
) {
$class_storage = $codebase->classlike_storage_provider->get((string)$lhs_type_part);
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
if ($var_id) {
if (isset($class_storage->pseudo_property_set_types['$' . $prop_name])) {
@ -355,29 +381,32 @@ class PropertyAssignmentAnalyzer
$property_exists = true;
if (!$context->collect_mutations) {
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
} else {
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues(),
false
) !== true) {
continue;
if (!$mocked_properties) {
if (!$context->collect_mutations) {
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
} else {
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues(),
false
) !== true) {
continue;
}
}
}
$declaring_property_class = $codebase->properties->getDeclaringClassForProperty(
$property_id
);
@ -422,8 +451,8 @@ class PropertyAssignmentAnalyzer
$class_property_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$class_property_type,
$lhs_type_part->value,
$lhs_type_part->value
$fq_class_name,
$lhs_type_part
);
if (!$class_property_type->isMixed() && $assignment_value_type->isMixed()) {

View File

@ -252,36 +252,62 @@ class PropertyFetchAnalyzer
continue;
}
$intersection_types = $lhs_type_part->getIntersectionTypes() ?: [];
$fq_class_name = $lhs_type_part->value;
$mocked_properties = false;
if (!$codebase->classExists($lhs_type_part->value)) {
$class_exists = false;
if ($codebase->interfaceExists($lhs_type_part->value)) {
$interface_storage = $codebase->classlike_storage_provider->get($lhs_type_part->value);
$mocked_properties = $interface_storage->mocked_properties;
foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject
&& $codebase->classExists($intersection_type->value)
) {
$fq_class_name = $intersection_type->value;
$class_exists = true;
break;
}
}
if (!$class_exists) {
if (IssueBuffer::accepts(
new NoInterfaceProperties(
'Interfaces cannot have properties',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return null;
}
}
if (!$class_exists) {
if (IssueBuffer::accepts(
new NoInterfaceProperties(
'Interfaces cannot have properties',
new CodeLocation($statements_analyzer->getSource(), $stmt)
new UndefinedClass(
'Cannot set properties of undefined class ' . $lhs_type_part->value,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$lhs_type_part->value
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
// fall through
}
continue;
return null;
}
if (IssueBuffer::accepts(
new UndefinedClass(
'Cannot get properties of undefined class ' . $lhs_type_part->value,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$lhs_type_part->value
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
continue;
}
$property_id = $lhs_type_part->value . '::$' . $prop_name;
$property_id = $fq_class_name . '::$' . $prop_name;
if ($codebase->server_mode) {
$codebase->analyzer->addNodeReference(
@ -291,10 +317,10 @@ class PropertyFetchAnalyzer
);
}
if ($codebase->methodExists($lhs_type_part->value . '::__get')
if ($codebase->methodExists($fq_class_name . '::__get')
&& (!$codebase->properties->propertyExists($property_id)
|| ($stmt_var_id !== '$this'
&& $lhs_type_part->value !== $context->self
&& $fq_class_name !== $context->self
&& ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
@ -305,7 +331,7 @@ class PropertyFetchAnalyzer
) !== true)
)
) {
$class_storage = $codebase->classlike_storage_provider->get((string)$lhs_type_part);
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
if (isset($class_storage->pseudo_property_get_types['$' . $prop_name])) {
$stmt->inferredType = clone $class_storage->pseudo_property_get_types['$' . $prop_name];
@ -317,7 +343,7 @@ class PropertyFetchAnalyzer
* If we have an explicit list of all allowed magic properties on the class, and we're
* not in that list, fall through
*/
if (!$class_storage->sealed_properties) {
if (!$class_storage->sealed_properties && !$mocked_properties) {
continue;
}
}
@ -369,14 +395,16 @@ class PropertyFetchAnalyzer
return;
}
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
if (!$mocked_properties) {
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context->self,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
}
$declaring_property_class = $codebase->properties->getDeclaringClassForProperty($property_id);
@ -405,7 +433,7 @@ class PropertyFetchAnalyzer
if ($class_property_type === false) {
if (IssueBuffer::accepts(
new MissingPropertyType(
'Property ' . $lhs_type_part->value . '::$' . $prop_name
'Property ' . $fq_class_name . '::$' . $prop_name
. ' does not have a declared type',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
@ -424,7 +452,7 @@ class PropertyFetchAnalyzer
);
if ($lhs_type_part instanceof TGenericObject) {
$class_storage = $codebase->classlike_storage_provider->get($lhs_type_part->value);
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
if ($class_storage->template_types) {
$class_template_params = [];

View File

@ -880,6 +880,31 @@ class ExpressionAnalyzer
$static_class_type = null
) {
if ($return_type instanceof TNamedObject) {
if ($return_type->extra_types) {
$new_intersection_types = [];
foreach ($return_type->extra_types as &$extra_type) {
self::fleshOutAtomicType(
$codebase,
$extra_type,
$self_class,
$static_class_type
);
if ($extra_type instanceof TNamedObject && $extra_type->extra_types) {
$new_intersection_types = array_merge(
$new_intersection_types,
$extra_type->extra_types
);
$extra_type->extra_types = [];
}
}
if ($new_intersection_types) {
$return_type->extra_types = array_merge($return_type->extra_types, $new_intersection_types);
}
}
$return_type_lc = strtolower($return_type->value);
if ($return_type_lc === 'static' || $return_type_lc === '$this') {

View File

@ -40,6 +40,11 @@ class ClassLikeDocblockComment
*/
public $sealed_methods = false;
/**
* @var bool
*/
public $mocked_properties = false;
/**
* @var array<int, string>
*/

View File

@ -718,6 +718,8 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$storage->sealed_properties = $docblock_info->sealed_properties;
$storage->sealed_methods = $docblock_info->sealed_methods;
$storage->mocked_properties = $docblock_info->mocked_properties;
$storage->suppressed_issues = $docblock_info->suppressed_issues;
foreach ($docblock_info->methods as $method) {

View File

@ -92,6 +92,11 @@ class ClassLikeStorage
*/
public $sealed_methods = false;
/**
* @var bool
*/
public $mocked_properties = false;
/**
* @var array<int, string>
*/

View File

@ -456,6 +456,41 @@ class InterfaceTest extends TestCase
$o = new C;
f($o, $o);',
],
'interfacePropertyIntersection' => [
'<?php
class A {
/** @var ?string */
public $a;
}
class B extends A implements I {}
interface I {}
function takeI(I $i) : void {
if ($i instanceof A) {
echo $i->a;
$i->a = "hello";
}
}',
],
'interfacePropertyIntersectionMockAccess' => [
'<?php
class A {
/** @var ?string */
private $a;
}
/** @psalm-mock-properties */
interface I {}
function takeI(I $i) : void {
if ($i instanceof A) {
echo $i->a;
$i->a = "hello";
}
}',
],
];
}