Skip to content

Commit

Permalink
check "never" return type more strictly
Browse files Browse the repository at this point in the history
* 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: #8175
* Fix: #8178
  • Loading branch information
kkmuffme committed Dec 1, 2022
1 parent bce4b55 commit 694b7d8
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 27 deletions.
71 changes: 56 additions & 15 deletions src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
Expand Up @@ -48,13 +48,10 @@
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;
Expand Down Expand Up @@ -238,23 +235,12 @@ public static function verifyReturnType(
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
Expand Down Expand Up @@ -507,6 +493,61 @@ public static function verifyReturnType(

$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,
Expand Down
17 changes: 11 additions & 6 deletions src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php
Expand Up @@ -79,22 +79,27 @@ public static function getReturnTypes(
}

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,
Expand Down
20 changes: 20 additions & 0 deletions src/Psalm/Internal/Analyzer/ScopeAnalyzer.php
Expand Up @@ -411,4 +411,24 @@ public static function onlyThrowsOrExits(NodeTypeProvider $type_provider, array

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;
}
}
Expand Up @@ -45,7 +45,6 @@
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;
Expand Down Expand Up @@ -742,7 +741,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal
}

if ($attribute->fq_class_name === 'JetBrains\\PhpStorm\\NoReturn') {
$storage->return_type = new Union([new TNever()]);
$storage->return_type = Type::getNever();
}

$storage->attributes[] = $attribute;
Expand Down
Expand Up @@ -7,6 +7,7 @@
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;
Expand Down Expand Up @@ -39,7 +40,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$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') {
Expand Down
13 changes: 11 additions & 2 deletions src/Psalm/Internal/Type/TypeCombiner.php
Expand Up @@ -356,13 +356,22 @@ public static function combine(
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;
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Type/TypeExpander.php
Expand Up @@ -119,6 +119,7 @@ public static function expandUnion(
$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;

Expand Down
5 changes: 4 additions & 1 deletion src/Psalm/Type.php
Expand Up @@ -349,7 +349,6 @@ public static function getScalar(bool $from_docblock = false): Union
public static function getNever(bool $from_docblock = false): Union
{
$type = new TNever($from_docblock);

return new Union([$type]);
}

Expand Down Expand Up @@ -609,6 +608,10 @@ public static function combineUnionTypes(
$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;
}
Expand Down
15 changes: 15 additions & 0 deletions src/Psalm/Type/MutableUnion.php
Expand Up @@ -18,6 +18,7 @@
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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -244,6 +255,8 @@ public function setTypes(array $types): self
&& ($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;
Expand Down Expand Up @@ -291,6 +304,8 @@ public function addType(Atomic $type): self
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();
Expand Down
9 changes: 9 additions & 0 deletions src/Psalm/Type/Union.php
Expand Up @@ -29,6 +29,7 @@
* ignore_isset?: bool,
* possibly_undefined?: bool,
* possibly_undefined_from_try?: bool,
* explicit_never?: bool,
* had_template?: bool,
* from_template_default?: bool,
* by_ref?: bool,
Expand Down Expand Up @@ -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
*
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Type/UnionTrait.php
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -94,6 +95,8 @@ public function __construct(array $types, array $properties = [])
&& ($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;
Expand Down

0 comments on commit 694b7d8

Please sign in to comment.