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:
parent
10931d2d68
commit
047b096227
@ -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];
|
||||
|
@ -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()) {
|
||||
|
@ -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 = [];
|
||||
|
@ -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') {
|
||||
|
@ -40,6 +40,11 @@ class ClassLikeDocblockComment
|
||||
*/
|
||||
public $sealed_methods = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $mocked_properties = false;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -92,6 +92,11 @@ class ClassLikeStorage
|
||||
*/
|
||||
public $sealed_methods = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $mocked_properties = false;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
|
@ -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";
|
||||
}
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user