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

check "never" return type more strictly

* require explicit "never" return type when function always exits, except if it only throws
* error if function does not exit, but return type explicitly contains "never"
* Fix: https://github.com/vimeo/psalm/issues/8175
* Fix: https://github.com/vimeo/psalm/issues/8178
This commit is contained in:
kkmuffme 2022-10-27 11:56:29 +02:00
parent bce4b55525
commit 694b7d8975
12 changed files with 204 additions and 27 deletions

View File

@ -48,13 +48,10 @@ use Psalm\Storage\FunctionLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
use function array_diff;
use function array_filter;
use function array_values;
use function count;
use function implode;
use function in_array;
@ -238,23 +235,12 @@ class ReturnTypeAnalyzer
return null;
}
$number_of_types = count($inferred_return_type_parts);
// we filter TNever that have no bearing on the return type
if ($number_of_types > 1) {
$inferred_return_type_parts = array_filter(
$inferred_return_type_parts,
static fn(Union $union_type): bool => !$union_type->isNever()
);
}
$inferred_return_type_parts = array_values($inferred_return_type_parts);
$inferred_return_type = $inferred_return_type_parts
? Type::combineUnionTypeArray($inferred_return_type_parts, $codebase)
: Type::getVoid();
if ($function_always_exits) {
$inferred_return_type = new Union([new TNever]);
$inferred_return_type = Type::getNever();
}
$inferred_yield_type = $inferred_yield_types
@ -507,6 +493,61 @@ class ReturnTypeAnalyzer
$union_comparison_results = new TypeComparisonResult();
if ($declared_return_type->explicit_never === true && $inferred_return_type->explicit_never === false) {
if (IssueBuffer::accepts(
new MoreSpecificReturnType(
'The declared return type \'' . $declared_return_type->getId() . '|never\' for '
. $cased_method_id . ' is more specific than the inferred return type '
. '\'' . $inferred_return_type->getId() . '\'',
$return_type_location
),
$suppressed_issues
)) {
return false;
}
}
if (!$declared_return_type->isNever()
&& $function_always_exits
// never return type only available from PHP 8.1 in non-docblock
&& ($declared_return_type->from_docblock || $codebase->analysis_php_version_id >= 8_10_00)
// no error for single throw, as extending a class might not work without errors
// https://3v4l.org/vCSF4#v8.1.12
&& !ScopeAnalyzer::onlyThrows($function_stmts)
) {
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['InvalidReturnType'])
&& !in_array('InvalidReturnType', $suppressed_issues)
) {
self::addOrUpdateReturnType(
$function,
$project_analyzer,
Type::getNever(),
$source,
($project_analyzer->only_replace_php_types_with_non_docblock_types
|| $unsafe_return_type)
&& $inferred_return_type->from_docblock,
$function_like_storage
);
return null;
}
if (IssueBuffer::accepts(
new InvalidReturnType(
'The declared return type \''
. $declared_return_type->getId()
. '\' for ' . $cased_method_id
. ' is incorrect, got \'never\'',
$return_type_location
),
$suppressed_issues,
true
)) {
return false;
}
}
if (!UnionTypeComparator::isContainedBy(
$codebase,
$inferred_return_type,

View File

@ -79,22 +79,27 @@ class ReturnTypeCollector
}
if ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
if ($collapse_types) {
$return_types[] = Type::getNever();
}
$return_types[] = Type::getNever();
break;
}
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
if ($stmt->expr instanceof PhpParser\Node\Expr\Exit_) {
if ($collapse_types) {
$return_types[] = Type::getNever();
}
$return_types[] = Type::getNever();
break;
}
if ($stmt->expr instanceof PhpParser\Node\Expr\FuncCall) {
$stmt_type = $nodes->getType($stmt->expr);
if ($stmt_type && ($stmt_type->isNever() || $stmt_type->explicit_never)) {
$return_types[] = Type::getNever();
break;
}
}
if ($stmt->expr instanceof PhpParser\Node\Expr\Assign) {
$return_types = [
...$return_types,

View File

@ -411,4 +411,24 @@ class ScopeAnalyzer
return false;
}
/**
* @param array<PhpParser\Node> $stmts
*
*/
public static function onlyThrows(array $stmts): bool
{
$stmts_count = count($stmts);
if ($stmts_count !== 1) {
return false;
}
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
return true;
}
}
return false;
}
}

View File

@ -45,7 +45,6 @@ use Psalm\Storage\Possibilities;
use Psalm\Storage\PropertyStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
use ReflectionFunction;
@ -742,7 +741,7 @@ class FunctionLikeNodeScanner
}
if ($attribute->fq_class_name === 'JetBrains\\PhpStorm\\NoReturn') {
$storage->return_type = new Union([new TNever()]);
$storage->return_type = Type::getNever();
}
$storage->attributes[] = $attribute;

View File

@ -7,6 +7,7 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Internal\Type\TypeCombiner;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TLiteralInt;
@ -39,7 +40,7 @@ class TriggerErrorReturnTypeProvider implements FunctionReturnTypeProviderInterf
$codebase = $event->getStatementsSource()->getCodebase();
$config = $codebase->config;
if ($config->trigger_error_exits === 'always') {
return new Union([new TNever()]);
return Type::getNever();
}
if ($config->trigger_error_exits === 'never') {

View File

@ -356,13 +356,22 @@ class TypeCombiner
if (!$new_types && !$has_never) {
throw new UnexpectedValueException('There should be types here');
} elseif (!$new_types && $has_never) {
$union_type = Type::getNever();
$union_type = Type::getNever($from_docblock);
} else {
$union_type = new Union($new_types);
}
$union_properties = [];
if ($from_docblock) {
return $union_type->setProperties(['from_docblock' => true]);
$union_properties['from_docblock'] = true;
}
if ($has_never) {
$union_properties['explicit_never'] = true;
}
if ($union_properties !== []) {
return $union_type->setProperties($union_properties);
}
return $union_type;

View File

@ -119,6 +119,7 @@ class TypeExpander
$fleshed_out_type->initialized = $return_type->initialized;
$fleshed_out_type->from_property = $return_type->from_property;
$fleshed_out_type->from_static_property = $return_type->from_static_property;
$fleshed_out_type->explicit_never = $return_type->explicit_never;
$fleshed_out_type->had_template = $return_type->had_template;
$fleshed_out_type->parent_nodes = $return_type->parent_nodes;

View File

@ -349,7 +349,6 @@ abstract class Type
public static function getNever(bool $from_docblock = false): Union
{
$type = new TNever($from_docblock);
return new Union([$type]);
}
@ -609,6 +608,10 @@ abstract class Type
$combined_type->ignore_falsable_issues = true;
}
if ($type_1->explicit_never && $type_2->explicit_never) {
$combined_type->explicit_never = true;
}
if ($type_1->had_template && $type_2->had_template) {
$combined_type->had_template = true;
}

View File

@ -18,6 +18,7 @@ use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTrue;
@ -131,6 +132,16 @@ final class MutableUnion implements TypeNode, Stringable
*/
public $possibly_undefined_from_try = false;
/**
* whether this type had never set explicitly
* since it's the bottom type, it's combined into everything else and lost
*
* @psalm-suppress PossiblyUnusedProperty used in setTypes and addType
*
* @var bool
*/
public $explicit_never = false;
/**
* Whether or not this union had a template, since replaced
*
@ -244,6 +255,8 @@ final class MutableUnion implements TypeNode, Stringable
&& ($type->as_type || $type instanceof TTemplateParamClass)
) {
$this->typed_class_strings[$key] = $type;
} elseif ($type instanceof TNever) {
$this->explicit_never = true;
}
$from_docblock = $from_docblock || $type->from_docblock;
@ -291,6 +304,8 @@ final class MutableUnion implements TypeNode, Stringable
foreach ($this->literal_float_types as $key => $_) {
unset($this->literal_float_types[$key], $this->types[$key]);
}
} elseif ($type instanceof TNever) {
$this->explicit_never = true;
}
$this->bustCache();

View File

@ -29,6 +29,7 @@ use function get_object_vars;
* ignore_isset?: bool,
* possibly_undefined?: bool,
* possibly_undefined_from_try?: bool,
* explicit_never?: bool,
* had_template?: bool,
* from_template_default?: bool,
* by_ref?: bool,
@ -144,6 +145,14 @@ final class Union implements TypeNode, Stringable
*/
public $possibly_undefined_from_try = false;
/**
* whether this type had never set explicitly
* since it's the bottom type, it's combined into everything else and lost
*
* @var bool
*/
public $explicit_never = false;
/**
* Whether or not this union had a template, since replaced
*

View File

@ -31,6 +31,7 @@ use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TLowercaseString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TNonEmptyLowercaseString;
use Psalm\Type\Atomic\TNonspecificLiteralInt;
use Psalm\Type\Atomic\TNonspecificLiteralString;
@ -94,6 +95,8 @@ trait UnionTrait
&& ($type->as_type || $type instanceof TTemplateParamClass)
) {
$this->typed_class_strings[$key] = $type;
} elseif ($type instanceof TNever) {
$this->explicit_never = true;
}
$from_docblock = $from_docblock || $type->from_docblock;

View File

@ -1957,6 +1957,30 @@ class FunctionCallTest extends TestCase
'ignored_issues' => [],
'php_version' => '8.0',
],
'noNeverReturnError' => [
'code' => '<?php
/**
* @return string
*/
function foo() {
if (random_int(0, 1)) {
exit;
}
return "foobar";
}
'
],
'noNeverReturnErrorOnlyThrows' => [
'code' => '<?php
/**
* https://3v4l.org/vCSF4#v8.1.12
*/
function foo(): string {
throw new \Exception("foo");
}
'
],
];
}
@ -2536,6 +2560,53 @@ class FunctionCallTest extends TestCase
',
'error_message' => 'InvalidArgument',
],
'shouldReturnNeverNotString' => [
'code' => '<?php
/**
* @return string
*/
function finalFunc() {
exit;
}
finalFunc();',
'error_message' => 'InvalidReturnType'
],
'shouldReturnNeverNotStringCaller' => [
'code' => '<?php
/**
* @return string
*/
function foo() {
finalFunc();
}
/**
* @return never
*/
function finalFunc() {
exit;
}
foo();',
'error_message' => 'InvalidReturnType'
],
'shouldReturnNeverNotStringNoDocblockCaller' => [
'code' => '<?php
/**
* @return string
*/
function foo() {
finalFunc();
}
function finalFunc() {
exit;
}
foo();',
'error_message' => 'InvalidReturnType'
],
];
}