Skip to content

Commit

Permalink
fix: transform ide-helper relationship docblocks into typed collections
Browse files Browse the repository at this point in the history
  • Loading branch information
mr-feek committed Feb 26, 2020
1 parent 8f22d05 commit 542f7a4
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 0 deletions.
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,8 @@ services:
class: NunoMaduro\Larastan\ReturnTypes\Helpers\AppExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: NunoMaduro\Larastan\Types\GenericEloquentCollectionTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension
74 changes: 74 additions & 0 deletions src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Types;

use function count;
use Illuminate\Database\Eloquent\Collection;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

/**
* @see https://github.com/nunomaduro/larastan/issues/476
* @see https://gist.github.com/ondrejmirtes/56af016d0595788d5400b8dfb6520adc
*
* This extension interprets docblocks like:
*
* \Illuminate\Database\Eloquent\Collection|\App\Account[] $accounts
*
* and transforms them into:
*
* \Illuminate\Database\Eloquent\Collection<\App\Account> $accounts
*
* Now IDE's can benefit from auto-completion, and we can benefit from the correct type passed to the generic collection
*/
class GenericEloquentCollectionTypeNodeResolverExtension implements TypeNodeResolverExtension
{
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
{
if ($typeNode instanceof UnionTypeNode && count($typeNode->types) === 2) {
$arrayTypeNode = null;
$identifierTypeNode = null;
foreach ($typeNode->types as $innerTypeNode) {
if ($innerTypeNode instanceof ArrayTypeNode) {
$arrayTypeNode = $innerTypeNode;
continue;
}

if ($innerTypeNode instanceof IdentifierTypeNode) {
$identifierTypeNode = $innerTypeNode;
}
}

if ($arrayTypeNode === null || $identifierTypeNode === null) {
return null;
}

$identifierTypeName = $nameScope->resolveStringName($identifierTypeNode->name);
if ($identifierTypeName !== Collection::class) {
return null;
}

$innerArrayTypeNode = $arrayTypeNode->type;
if (! $innerArrayTypeNode instanceof IdentifierTypeNode) {
return null;
}

$innerArrayTypeName = $nameScope->resolveStringName($innerArrayTypeNode->name);

return new GenericObjectType($identifierTypeName, [
new ObjectType($innerArrayTypeName),
]);
}

return null;
}
}
3 changes: 3 additions & 0 deletions tests/Application/app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use Illuminate\Notifications\Notifiable;
use Tests\Application\HasManySyncable;

/**
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Account[] $accounts
*/
class User extends Authenticatable
{
use Notifiable;
Expand Down
10 changes: 10 additions & 0 deletions tests/Features/Models/Relations.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ private function getUser(): User
{
return User::firstOrFail();
}

/**
* @see https://github.com/nunomaduro/larastan/issues/476
*/
public function testRelationshipPropertyHasCorrectReturnTypeWithIdeHelperDocblocks(): ?Account
{
$user = new User();

return $user->accounts->first();
}
}

/**
Expand Down

0 comments on commit 542f7a4

Please sign in to comment.