diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index b00c0d6fc..a0b8eca65 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -58,6 +58,7 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer implements StatementsSou 'array' => true, 'object' => true, 'resource' => true, + 'resource (closed)' => true, 'NULL' => true, 'unknown type' => true, ]; diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index deeb9b35f..23613e397 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -235,6 +235,17 @@ class AssertionReconciler extends \Psalm\Type\Reconciler ); } + if ($assertion === 'resource' && !$existing_var_type->hasMixed()) { + return self::reconcileResource( + $existing_var_type, + $key, + $code_location, + $suppressed_issues, + $failed_reconciliation, + $is_equality + ); + } + if ($assertion === 'callable' && !$existing_var_type->hasMixed()) { return self::reconcileCallable( $codebase, @@ -1384,6 +1395,62 @@ class AssertionReconciler extends \Psalm\Type\Reconciler : Type::getEmpty(); } + /** + * @param string[] $suppressed_issues + * @param 0|1|2 $failed_reconciliation + */ + private static function reconcileResource( + Union $existing_var_type, + ?string $key, + ?CodeLocation $code_location, + array $suppressed_issues, + int &$failed_reconciliation, + bool $is_equality + ) : Union { + $old_var_type_string = $existing_var_type->getId(); + $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); + + $resource_types = []; + $did_remove_type = false; + + foreach ($existing_var_atomic_types as $type) { + if ($type instanceof TResource) { + if (!$type instanceof Type\Atomic\TOpenResource) { + $did_remove_type = true; + $type = new Type\Atomic\TOpenResource(); + } + + $resource_types[] = $type; + } else { + $did_remove_type = true; + } + } + + if ((!$resource_types || !$did_remove_type) && !$is_equality) { + if ($key && $code_location) { + self::triggerIssueForImpossible( + $existing_var_type, + $old_var_type_string, + $key, + 'resource', + !$did_remove_type, + $code_location, + $suppressed_issues + ); + } + } + + if ($resource_types) { + return new Type\Union($resource_types); + } + + $failed_reconciliation = 2; + + return $existing_var_type->from_docblock + ? Type::getMixed() + : Type::getEmpty(); + } + /** * @param string[] $suppressed_issues * @param 0|1|2 $failed_reconciliation diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index 9907c0e1d..6ff729078 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -159,6 +159,12 @@ class NegatedAssertionReconciler extends Reconciler ); } + if ($assertion === 'resource' && !$existing_var_type->hasMixed()) { + return self::reconcileResource( + $existing_var_type + ); + } + if ($assertion === 'scalar' && !$existing_var_type->hasMixed()) { return self::reconcileScalar( $existing_var_type, @@ -952,6 +958,26 @@ class NegatedAssertionReconciler extends Reconciler return Type::getMixed(); } + private static function reconcileResource( + Type\Union $existing_var_type + ) : Type\Union { + $non_resource_types = []; + + foreach ($existing_var_type->getAtomicTypes() as $type) { + if (!$type instanceof Type\Atomic\TResource) { + $non_resource_types[] = $type; + } else { + $non_resource_types[] = new Type\Atomic\TClosedResource(); + } + } + + $type = new Type\Union($non_resource_types); + $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; + $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; + $type->from_docblock = $existing_var_type->from_docblock; + return $type; + } + /** * @param string[] $suppressed_issues * @param 0|1|2 $failed_reconciliation diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index daecb436b..8e8c66e46 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -1214,6 +1214,24 @@ class TypeCombination } } + if ($type instanceof Type\Atomic\TOpenResource) { + $existing_resource = $combination->value_types['resource'] ?? null; + + if ($existing_resource instanceof Type\Atomic\TClosedResource + || ($existing_resource && get_class($existing_resource) === Type\Atomic\TResource::class) + ) { + $type = new Type\Atomic\TResource(); + } + } + + if ($type instanceof Type\Atomic\TClosedResource) { + $existing_resource = $combination->value_types['resource'] ?? null; + + if ($existing_resource instanceof Type\Atomic\TResource) { + $type = new Type\Atomic\TResource(); + } + } + $combination->value_types[$type_key] = $type; } diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index f07a9d953..2ab81c9e1 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -181,6 +181,9 @@ abstract class Atomic case 'resource': return $php_version !== null ? new TNamedObject($value) : new TResource(); + case 'resource (closed)': + return new Type\Atomic\TClosedResource(); + case 'numeric': return $php_version !== null ? new TNamedObject($value) : new TNumeric(); diff --git a/src/Psalm/Type/Atomic/TClosedResource.php b/src/Psalm/Type/Atomic/TClosedResource.php new file mode 100644 index 000000000..feff0df65 --- /dev/null +++ b/src/Psalm/Type/Atomic/TClosedResource.php @@ -0,0 +1,73 @@ + $aliased_classes + * @param string|null $this_class + * @param int $php_major_version + * @param int $php_minor_version + * + * @return null|string + */ + public function toPhpString( + $namespace, + array $aliased_classes, + $this_class, + $php_major_version, + $php_minor_version + ) { + return null; + } + + public function canBeFullyExpressedInPhp() + { + return false; + } + + /** + * @param StatementsSource $source + * @param CodeLocation $code_location + * @param array $suppressed_issues + * @param array $phantom_classes + * @param bool $inferred + * + * @return void + */ + public function check( + StatementsSource $source, + CodeLocation $code_location, + array $suppressed_issues, + array $phantom_classes = [], + bool $inferred = true, + bool $prevent_template_covariance = false + ) { + return; + } +} diff --git a/src/Psalm/Type/Atomic/TOpenResource.php b/src/Psalm/Type/Atomic/TOpenResource.php new file mode 100644 index 000000000..df6e66f94 --- /dev/null +++ b/src/Psalm/Type/Atomic/TOpenResource.php @@ -0,0 +1,16 @@ + [ + ' 'RedundantCondition', ], + 'checkResourceTwice' => [ + ' 'RedundantCondition', + ], ]; } }