Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,7 @@ public function enterClassMethod(
$isConstructor,
$this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
$phpDocPureUnlessCallableIsImpureParameters,
$this->phpVersion,
),
!$classMethod->isStatic(),
);
Expand Down Expand Up @@ -1736,6 +1737,7 @@ public function enterPropertyHook(
false,
$this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)),
[],
$this->phpVersion,
),
true,
);
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ public function supportsReadonlyPropertyReinitializationOnClone(): bool
return $this->versionId >= 80300;
}

public function supportsNeverReturnType(): bool
{
return $this->versionId >= 80100;
}

public function supportsNeverReturnTypeInArrowFunction(): bool
{
return $this->versionId >= 80200;
Expand Down
59 changes: 33 additions & 26 deletions src/Reflection/Php/PhpMethodFromParserNodeReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\AttributeReflection;
Expand All @@ -18,6 +19,7 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StringType;
Expand Down Expand Up @@ -73,6 +75,7 @@ public function __construct(
private bool $isConstructor,
array $attributes,
array $pureUnlessCallableIsImpureParameters,
PhpVersion $phpVersion,
)
{
if ($this->classMethod instanceof Node\PropertyHook) {
Expand All @@ -87,34 +90,38 @@ public function __construct(
if ($this->isConstructor) {
$realReturnType = new VoidType();
}
if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) {
$realReturnType = new VoidType();
}
if ($name === '__tostring') {
$realReturnType = new StringType();
}
if ($name === '__isset') {
$realReturnType = new BooleanType();
}
if ($name === '__sleep') {
$realReturnType = new ArrayType(new IntegerType(), new StringType());
}
if ($name === '__set_state') {
$realReturnType = TypeCombinator::intersect(new ObjectWithoutClassType(), $realReturnType);
}
if ($name === '__set') {
$realReturnType = new VoidType();
}
// Since PHP 8.1 "never" is always accepted as the declared return type of a magic method,
// even when PHP otherwise mandates a specific one. Before 8.1 "never" is just a class name.
if (!$phpVersion->supportsNeverReturnType() || !$realReturnType instanceof NeverType) {
if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) {
$realReturnType = new VoidType();
}
if ($name === '__tostring') {
$realReturnType = new StringType();
}
if ($name === '__isset') {
$realReturnType = new BooleanType();
}
if ($name === '__sleep') {
$realReturnType = new ArrayType(new IntegerType(), new StringType());
}
if ($name === '__set_state') {
$realReturnType = TypeCombinator::intersect(new ObjectWithoutClassType(), $realReturnType);
}
if ($name === '__set') {
$realReturnType = new VoidType();
}

if ($name === '__debuginfo') {
$realReturnType = TypeCombinator::intersect(new UnionType([new ArrayType(new MixedType(true), new MixedType(true)), new NullType()]), $realReturnType);
}
if ($name === '__debuginfo') {
$realReturnType = TypeCombinator::intersect(new UnionType([new ArrayType(new MixedType(true), new MixedType(true)), new NullType()]), $realReturnType);
}

if ($name === '__unserialize') {
$realReturnType = new VoidType();
}
if ($name === '__serialize') {
$realReturnType = new ArrayType(new MixedType(true), new MixedType(true));
if ($name === '__unserialize') {
$realReturnType = new VoidType();
}
if ($name === '__serialize') {
$realReturnType = new ArrayType(new MixedType(true), new MixedType(true));
}
}

parent::__construct(
Expand Down
7 changes: 7 additions & 0 deletions src/Rules/Playground/MethodNeverRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function count;
use function in_array;
use function sprintf;
use function strtolower;

/**
* @implements Rule<MethodReturnStatementsNode>
Expand All @@ -33,6 +35,11 @@ public function processNode(Node $node, Scope $scope): array

$method = $node->getMethodReflection();

// PHP does not allow declaring a return type on these at all
if (in_array(strtolower($method->getName()), ['__construct', '__destruct'], true)) {
return [];
}

$returnType = $method->getReturnType();
$helperResult = $this->helper->shouldReturnNever($node, $returnType);
if ($helperResult === false) {
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1364,4 +1364,31 @@ public function testBug14893(): void
$this->analyse([__DIR__ . '/data/bug-14893.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testNeverMagicMethodReturnType(): void
{
$this->analyse([__DIR__ . '/data/never-magic-method-return-type.php'], [
[
'Method NeverMagicMethodReturnType\Foo::__clone() should never return but return statement found.',
10,
],
[
'Method NeverMagicMethodReturnType\Foo::__serialize() should never return but return statement found.',
15,
],
[
'Method NeverMagicMethodReturnType\Foo::__toString() should never return but return statement found.',
20,
],
[
'Method NeverMagicMethodReturnType\Foo::__isset() should never return but return statement found.',
25,
],
[
'Method NeverMagicMethodReturnType\Foo::__sleep() should never return but return statement found.',
31,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php // lint >= 8.1

namespace NeverMagicMethodReturnType;

class Foo
{

public function __clone(): never
{
return;
}

public function __serialize(): never
{
return [];
}

public function __toString(): never
{
return 'foo';
}

public function __isset($name): never
{
return true;
}

public function __sleep(): never
{
if (rand(0, 1)) {
return ['a'];
}

throw new \Exception();
}

public function __clone2(): never
{
throw new \Exception();
}

}

class Bar
{

public function __clone(): void
{
return;
}

public function __toString(): string
{
return 'foo';
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,28 @@ public function testBug5681(): void
$this->analyse([__DIR__ . '/data/bug-5681.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testNeverMagicMethod(): void
{
$this->checkExplicitMixedMissingReturn = true;
$this->analyse([__DIR__ . '/data/never-magic-method.php'], [
[
'Method MissingReturnNeverMagicMethod\Foo::__toString() should always throw an exception or terminate script execution but doesn\'t do that.',
8,
],
[
'Method MissingReturnNeverMagicMethod\Foo::__clone() should always throw an exception or terminate script execution but doesn\'t do that.',
12,
],
[
'Method MissingReturnNeverMagicMethod\Foo::__serialize() should always throw an exception or terminate script execution but doesn\'t do that.',
16,
],
[
'Method MissingReturnNeverMagicMethod\Foo::__isset() should always throw an exception or terminate script execution but doesn\'t do that.',
20,
],
]);
}

}
43 changes: 43 additions & 0 deletions tests/PHPStan/Rules/Missing/data/never-magic-method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php // lint >= 8.1

namespace MissingReturnNeverMagicMethod;

class Foo
{

public function __toString(): never
{
}

public function __clone(): never
{
}

public function __serialize(): never
{
}

public function __isset($name): never
{
}

public function __sleep(): never
{
throw new \Exception();
}

}

class Bar
{

public function __toString(): string
{
return 'foo';
}

public function __clone(): void
{
}

}
42 changes: 42 additions & 0 deletions tests/PHPStan/Rules/Playground/MethodNeverRulePhp80Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Playground;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<MethodNeverRule>
*/
class MethodNeverRulePhp80Test extends RuleTestCase
{

protected function getRule(): Rule
{
return new MethodNeverRule(new NeverRuleHelper());
}

public function testMagicMethods(): void
{
// "never" is not accepted as a magic method return type before PHP 8.1,
// so the return type PHP mandates still wins.
$this->analyse([__DIR__ . '/data/method-never-php80.php'], [
[
'Method MethodNeverPhp80\MagicMethods::__clone() always throws an exception, it should have return type "never".',
8,
],
[
'Method MethodNeverPhp80\MagicMethods::__toString() always throws an exception, it should have return type "never".',
13,
],
]);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/data/method-never-php-8.0.neon',
];
}

}
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public function testRule(): void
'Method MethodNever\Foo::doBaz() always terminates script execution, it should have return type "never".',
31,
],
[
'Method MethodNever\MagicMethodsWithoutNever::__clone() always throws an exception, it should have return type "never".',
132,
],
[
'Method MethodNever\MagicMethodsWithoutNever::__toString() always throws an exception, it should have return type "never".',
137,
],
]);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Rules/Playground/data/method-never-php-8.0.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
phpVersion: 80000
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Playground/data/method-never-php80.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php // lint >= 8.1

namespace MethodNeverPhp80;

class MagicMethods
{

public function __clone(): never
{
throw new \Exception();
}

public function __toString(): never
{
throw new \Exception();
}

}
Loading
Loading