1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Fix #2626 - allow assertions on resource types

Also fixes #2266
This commit is contained in:
Matthew Brown 2020-01-17 11:55:16 -05:00
parent 1697115861
commit 7f093ca8ed
8 changed files with 235 additions and 0 deletions

View File

@ -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,
];

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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();

View File

@ -0,0 +1,73 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\CodeLocation;
use Psalm\StatementsSource;
class TClosedResource extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'closed-resource';
}
/**
* @return string
*/
public function getKey()
{
return 'resource';
}
/**
* @return string
*/
public function getId()
{
return 'closed-resource';
}
/**
* @param string|null $namespace
* @param array<string> $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<string> $suppressed_issues
* @param array<string, bool> $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;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\CodeLocation;
use Psalm\StatementsSource;
class TOpenResource extends TResource
{
/**
* @return string
*/
public function getId()
{
return 'open-resource';
}
}

View File

@ -756,6 +756,28 @@ class RedundantConditionTest extends \Psalm\Tests\TestCase
if (!is_string($value)) {}
}'
],
'checkClosedResource' => [
'<?php
$fp = tmpfile();
if ($fp && is_resource($fp)) {
echo "foo", "\n";
} else {
echo "bar", "\n";
}
echo var_export([$fp, is_resource($fp), !! $fp], true);
fclose($fp);
if ($fp && is_resource($fp)) {
echo "baz", "\n";
} else {
echo "bat", "\n";
}
echo var_export([$fp, is_resource($fp), !! $fp], true);'
],
];
}
@ -1225,6 +1247,15 @@ class RedundantConditionTest extends \Psalm\Tests\TestCase
}',
'error_message' => 'RedundantCondition',
],
'checkResourceTwice' => [
'<?php
$fp = tmpfile();
if ($fp && is_resource($fp)) {
if (is_resource($fp)) {}
}',
'error_message' => 'RedundantCondition',
],
];
}
}