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
56 changes: 56 additions & 0 deletions src/Rules/FunctionReturnTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
use Generator;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
Expand Down Expand Up @@ -38,6 +43,7 @@
): array
{
$returnType = TypeUtils::resolveLateResolvableTypes($returnType);
$returnType = $this->specifyTemplateTypesFromScope($scope, $returnType);

if ($returnType instanceof NeverType && $returnType->isExplicit()) {
return [
Expand Down Expand Up @@ -110,4 +116,54 @@
return [];
}

/**
* Resolves template types in the declared return type that have been pinned
* to an exact class by narrowing a `class-string<T>` parameter to a constant
* class-string (e.g. `if ($className === Foo::class)`). In such a branch the
* caller's `T` is known to be exactly that class, so returning a value of that
* class satisfies `@return T`.
*/
private function specifyTemplateTypesFromScope(Scope $scope, Type $returnType): Type
{
if (!$returnType->hasTemplateOrLateResolvableType()) {
return $returnType;
}

$function = $scope->getFunction();
if ($function === null || $scope->isInAnonymousFunction()) {
return $returnType;
}

$map = TemplateTypeMap::createEmpty();
foreach ($function->getParameters() as $parameter) {
$parameterType = $parameter->getType();
if (!$parameterType->isClassString()->yes()) {

Check warning on line 140 in src/Rules/FunctionReturnTypeCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $map = TemplateTypeMap::createEmpty(); foreach ($function->getParameters() as $parameter) { $parameterType = $parameter->getType(); - if (!$parameterType->isClassString()->yes()) { + if ($parameterType->isClassString()->no()) { continue; }

Check warning on line 140 in src/Rules/FunctionReturnTypeCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $map = TemplateTypeMap::createEmpty(); foreach ($function->getParameters() as $parameter) { $parameterType = $parameter->getType(); - if (!$parameterType->isClassString()->yes()) { + if ($parameterType->isClassString()->no()) { continue; }
continue;
}

$scopeType = $scope->getType(new Variable($parameter->getName()));
$constantStrings = $scopeType->getConstantStrings();
if ($constantStrings === [] || !TypeCombinator::union(...$constantStrings)->equals($scopeType)) {
continue;
}

$map = $map->union($parameterType->inferTemplateTypes($scopeType));
}

if ($map->isEmpty()) {
return $returnType;
}

return TypeTraverser::map($returnType, static function (Type $type, callable $traverse) use ($map): Type {
if ($type instanceof TemplateType) {
$specifiedType = $map->getType($type->getName());
if ($specifiedType !== null && !$specifiedType instanceof ErrorType) {
return $specifiedType;
}
}

return $traverse($type);
});
}

}
17 changes: 17 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ public function testBug2723(): void
]);
}

public function testBug2955(): void
{
$this->checkExplicitMixed = false;
$this->checkNullables = true;
$this->analyse([__DIR__ . '/data/bug-2955.php'], [
[
'Function Bug2955\returnsWrongClass() should return stdClass but returns Bug2955\Foo.',
40,
],
[
'Function Bug2955\notPinnedByIsA() should return T of object but returns Bug2955\Foo.',
53,
'Type Bug2955\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
],
]);
}

public function testBug5706(): void
{
$this->checkExplicitMixed = false;
Expand Down
57 changes: 57 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-2955.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);

namespace Bug2955;

/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
function test(string $className): object {
if ($className === \stdClass::class) {
return (object) [];
}

return new $className();
}

/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
function test2(string $className): object {
if ($className === \stdClass::class) {
return new \stdClass();
}

return new $className();
}

class Foo {}

/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
function returnsWrongClass(string $className): object {
if ($className === \stdClass::class) {
return new Foo();
}

return new $className();
}

/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
function notPinnedByIsA(string $className): object {
if (is_a($className, Foo::class, true)) {
return new Foo();
}

return new $className();
}
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,16 @@ public function testBug2676(): void
$this->analyse([__DIR__ . '/data/bug-2676.php'], []);
}

public function testBug2955(): void
{
$this->analyse([__DIR__ . '/data/bug-2955.php'], [
[
'Method Bug2955Method\Factory::makeWrong() should return stdClass but returns Bug2955Method\Foo.',
32,
],
]);
}

public function testBug2885(): void
{
$this->analyse([__DIR__ . '/data/bug-2885.php'], []);
Expand Down
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-2955.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace Bug2955Method;

class Foo {}

class Factory
{

/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
public function make(string $className): object
{
if ($className === \stdClass::class) {
return (object) [];
}

return new $className();
}

/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
public function makeWrong(string $className): object
{
if ($className === \stdClass::class) {
return new Foo();
}

return new $className();
}

}
Loading