mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Merge pull request #9656 from boesing/bugfix/issue-8981
(re-)implement object-shape assertions
This commit is contained in:
commit
a9bc87e729
@ -125,7 +125,19 @@ class AtomicPropertyFetchAnalyzer
|
||||
$statements_analyzer->node_data->setType(
|
||||
$stmt,
|
||||
Type::combineUnionTypes(
|
||||
$lhs_type_part->properties[$prop_name],
|
||||
TypeExpander::expandUnion(
|
||||
$statements_analyzer->getCodebase(),
|
||||
$lhs_type_part->properties[$prop_name],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
$stmt_type,
|
||||
),
|
||||
);
|
||||
@ -133,11 +145,19 @@ class AtomicPropertyFetchAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
$intersection_types = [];
|
||||
if (!$lhs_type_part instanceof TObject) {
|
||||
$intersection_types = $lhs_type_part->getIntersectionTypes();
|
||||
}
|
||||
|
||||
// stdClass and SimpleXMLElement are special cases where we cannot infer the return types
|
||||
// but we don't want to throw an error
|
||||
// Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164
|
||||
if ($lhs_type_part instanceof TObject
|
||||
|| in_array(strtolower($lhs_type_part->value), Config::getInstance()->getUniversalObjectCrates(), true)
|
||||
|| (
|
||||
in_array(strtolower($lhs_type_part->value), Config::getInstance()->getUniversalObjectCrates(), true)
|
||||
&& $intersection_types === []
|
||||
)
|
||||
) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
|
||||
|
||||
@ -149,8 +169,6 @@ class AtomicPropertyFetchAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
$intersection_types = $lhs_type_part->getIntersectionTypes() ?: [];
|
||||
|
||||
$fq_class_name = $lhs_type_part->value;
|
||||
|
||||
$override_property_visibility = false;
|
||||
@ -237,39 +255,60 @@ class AtomicPropertyFetchAnalyzer
|
||||
// add method before changing fq_class_name
|
||||
$get_method_id = new MethodIdentifier($fq_class_name, '__get');
|
||||
|
||||
if (!$naive_property_exists
|
||||
&& $class_storage->namedMixins
|
||||
) {
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_property_id = $mixin->value . '::$' . $prop_name;
|
||||
if (!$naive_property_exists) {
|
||||
if ($class_storage->namedMixins) {
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_property_id = $mixin->value . '::$' . $prop_name;
|
||||
|
||||
try {
|
||||
$new_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$new_class_storage = null;
|
||||
}
|
||||
|
||||
if ($new_class_storage
|
||||
&& ($codebase->properties->propertyExists(
|
||||
$new_property_id,
|
||||
!$in_assignment,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
: null,
|
||||
)
|
||||
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name]))
|
||||
) {
|
||||
$fq_class_name = $mixin->value;
|
||||
$lhs_type_part = $mixin;
|
||||
$class_storage = $new_class_storage;
|
||||
|
||||
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) {
|
||||
$naive_property_exists = true;
|
||||
try {
|
||||
$new_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$new_class_storage = null;
|
||||
}
|
||||
|
||||
$property_id = $new_property_id;
|
||||
if ($new_class_storage
|
||||
&& ($codebase->properties->propertyExists(
|
||||
$new_property_id,
|
||||
!$in_assignment,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
: null,
|
||||
)
|
||||
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name]))
|
||||
) {
|
||||
$fq_class_name = $mixin->value;
|
||||
$lhs_type_part = $mixin;
|
||||
$class_storage = $new_class_storage;
|
||||
|
||||
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) {
|
||||
$naive_property_exists = true;
|
||||
}
|
||||
|
||||
$property_id = $new_property_id;
|
||||
}
|
||||
}
|
||||
} elseif ($intersection_types !== [] && !$class_storage->final) {
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
self::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
$in_assignment,
|
||||
$var_id,
|
||||
$stmt_var_id,
|
||||
$stmt_var_type,
|
||||
$intersection_type,
|
||||
$prop_name,
|
||||
$has_valid_fetch_type,
|
||||
$invalid_fetch_types,
|
||||
$is_static_access,
|
||||
);
|
||||
|
||||
if ($has_valid_fetch_type) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ use Psalm\Storage\Assertion\NonEmpty;
|
||||
use Psalm\Storage\Assertion\NonEmptyCountable;
|
||||
use Psalm\Storage\Assertion\Truthy;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\Scalar;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
@ -292,7 +293,9 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
|
||||
if ($assertion_type instanceof TObject) {
|
||||
return self::reconcileObject(
|
||||
$codebase,
|
||||
$assertion,
|
||||
$assertion_type,
|
||||
$existing_var_type,
|
||||
$key,
|
||||
$negated,
|
||||
@ -1580,7 +1583,9 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
* @param Reconciler::RECONCILIATION_* $failed_reconciliation
|
||||
*/
|
||||
private static function reconcileObject(
|
||||
Codebase $codebase,
|
||||
Assertion $assertion,
|
||||
TObject $assertion_type,
|
||||
Union $existing_var_type,
|
||||
?string $key,
|
||||
bool $negated,
|
||||
@ -1600,7 +1605,17 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
$redundant = true;
|
||||
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if ($type->isObjectType()) {
|
||||
if (Type::isIntersectionType($assertion_type)
|
||||
&& self::areIntersectionTypesAllowed($codebase, $type)
|
||||
) {
|
||||
$object_types[] = $type->addIntersectionType($assertion_type);
|
||||
$redundant = false;
|
||||
} elseif ($type instanceof TNamedObject
|
||||
&& $codebase->classlike_storage_provider->has($type->value)
|
||||
&& $codebase->classlike_storage_provider->get($type->value)->final
|
||||
) {
|
||||
$redundant = false;
|
||||
} elseif ($type->isObjectType()) {
|
||||
$object_types[] = $type;
|
||||
} elseif ($type instanceof TCallable) {
|
||||
$callable_object = new TCallableObject($type->from_docblock, $type);
|
||||
@ -1614,8 +1629,17 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
$redundant = false;
|
||||
} elseif ($type instanceof TTemplateParam) {
|
||||
if ($type->as->hasObject() || $type->as->hasMixed()) {
|
||||
$type = $type->replaceAs(self::reconcileObject(
|
||||
/**
|
||||
* @psalm-suppress PossiblyInvalidArgument This looks wrong, psalm assumes that $assertion_type
|
||||
* can contain TNamedObject due to the reconciliation above
|
||||
* regarding {@see Type::isIntersectionType}. Due to the
|
||||
* native argument type `TObject`, the variable object will
|
||||
* never be `TNamedObject`.
|
||||
*/
|
||||
$reconciled_type = self::reconcileObject(
|
||||
$codebase,
|
||||
$assertion,
|
||||
$assertion_type,
|
||||
$type->as,
|
||||
null,
|
||||
false,
|
||||
@ -1623,7 +1647,8 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
$suppressed_issues,
|
||||
$failed_reconciliation,
|
||||
$is_equality,
|
||||
));
|
||||
);
|
||||
$type = $type->replaceAs($reconciled_type);
|
||||
|
||||
$object_types[] = $type;
|
||||
}
|
||||
@ -2920,4 +2945,22 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
|
||||
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type
|
||||
*/
|
||||
private static function areIntersectionTypesAllowed(Codebase $codebase, Atomic $type): bool
|
||||
{
|
||||
if ($type instanceof TObjectWithProperties || $type instanceof TCallableObject) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$type instanceof TNamedObject || !$codebase->classlike_storage_provider->has($type->value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($type->value);
|
||||
|
||||
return !$class_storage->final;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
use Psalm\Type\Atomic\TBool;
|
||||
use Psalm\Type\Atomic\TCallableObject;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
@ -962,10 +963,18 @@ abstract class Type
|
||||
|
||||
private static function hasIntersection(Atomic $type): bool
|
||||
{
|
||||
return ($type instanceof TIterable
|
||||
|| $type instanceof TNamedObject
|
||||
|| $type instanceof TTemplateParam
|
||||
|| $type instanceof TObjectWithProperties
|
||||
) && $type->extra_types;
|
||||
return self::isIntersectionType($type) && $type->extra_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-assert-if-true TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $type
|
||||
*/
|
||||
public static function isIntersectionType(Atomic $type): bool
|
||||
{
|
||||
return $type instanceof TNamedObject
|
||||
|| $type instanceof TTemplateParam
|
||||
|| $type instanceof TIterable
|
||||
|| $type instanceof TObjectWithProperties
|
||||
|| $type instanceof TCallableObject;
|
||||
}
|
||||
}
|
||||
|
@ -916,6 +916,27 @@ class FunctionTemplateAssertTest extends TestCase
|
||||
$numbersT->assert($mixed);
|
||||
acceptsArray($mixed);',
|
||||
],
|
||||
'assertObjectShape' => [
|
||||
'code' => '<?php
|
||||
final class Foo
|
||||
{
|
||||
public const STATUS_OK = "ok";
|
||||
public const STATUS_FAIL = "fail";
|
||||
}
|
||||
|
||||
$foo = new stdClass();
|
||||
|
||||
/** @psalm-assert object{status: Foo::STATUS_*} $bar */
|
||||
function assertObjectShape(object $bar): void {
|
||||
}
|
||||
|
||||
assertObjectShape($foo);
|
||||
$status = $foo->status;
|
||||
',
|
||||
'assertions' => [
|
||||
'$status===' => "'fail'|'ok'",
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1196,6 +1217,23 @@ class FunctionTemplateAssertTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'InvalidDocblock',
|
||||
],
|
||||
'assertObjectShapeOnFinalClass' => [
|
||||
'code' => '<?php
|
||||
final class Foo
|
||||
{
|
||||
}
|
||||
|
||||
$foo = new Foo();
|
||||
|
||||
/** @psalm-assert object{status: string} $bar */
|
||||
function assertObjectShape(object $bar): void {
|
||||
}
|
||||
|
||||
assertObjectShape($foo);
|
||||
$status = $foo->status;
|
||||
',
|
||||
'error_message' => 'Type Foo for $foo is never',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user