Skip to content

Commit

Permalink
feat: adds throw types for macros
Browse files Browse the repository at this point in the history
  • Loading branch information
canvural committed Nov 7, 2021
1 parent a2db045 commit 3b7e6ea
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 8 deletions.
2 changes: 2 additions & 0 deletions bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
} elseif ($app instanceof LumenApplication) {
$app->boot();
}

define('LARAVEL_VERSION', $app->version());
36 changes: 28 additions & 8 deletions src/Methods/Macro.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

namespace NunoMaduro\Larastan\Methods;

use function array_map;
use Closure;
use ErrorException;
use Illuminate\Validation\ValidationException;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
Expand All @@ -14,9 +17,12 @@
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;
use ReflectionFunction;
use ReflectionParameter;
use ReflectionType;
use stdClass;

final class Macro implements MethodReflection
Expand Down Expand Up @@ -54,6 +60,16 @@ final class Macro implements MethodReflection
*/
private $isStatic = false;

/**
* Map of macro methods and thrown exception classes.
*
* @var string[]
*/
private $methodThrowTypeMap = [
'validate' => ValidationException::class,
'validateWithBag' => ValidationException::class,
];

public function __construct(ClassReflection $classReflection, string $methodName, ReflectionFunction $reflectionFunction)
{
$this->classReflection = $classReflection;
Expand Down Expand Up @@ -134,7 +150,7 @@ public function getName(): string
/** @return ParameterReflection[] */
public function getParameters(): array
{
return \array_map(function (\ReflectionParameter $reflection): ParameterReflection {
return array_map(function (ReflectionParameter $reflection): ParameterReflection {
return new class($reflection) implements ParameterReflection
{
/**
Expand All @@ -157,7 +173,7 @@ public function isOptional(): bool
return $this->reflection->isOptional();
}

public function getType(): \PHPStan\Type\Type
public function getType(): Type
{
$type = $this->reflection->getType();

Expand All @@ -168,7 +184,7 @@ public function getType(): \PHPStan\Type\Type
return TypehintHelper::decideTypeFromReflection($this->reflection->getType());
}

public function passedByReference(): \PHPStan\Reflection\PassedByReference
public function passedByReference(): PassedByReference
{
return PassedByReference::createNo();
}
Expand All @@ -178,7 +194,7 @@ public function isVariadic(): bool
return $this->reflection->isVariadic();
}

public function getDefaultValue(): ?\PHPStan\Type\Type
public function getDefaultValue(): ?Type
{
return null;
}
Expand All @@ -197,7 +213,7 @@ public function setParameters(array $parameters): void
$this->parameters = $parameters;
}

public function getReturnType(): ?\ReflectionType
public function getReturnType(): ?ReflectionType
{
return $this->reflectionFunction->getReturnType();
}
Expand All @@ -207,7 +223,7 @@ public function isDeprecated(): TrinaryLogic
return TrinaryLogic::createFromBoolean($this->reflectionFunction->isDeprecated());
}

public function getPrototype(): \PHPStan\Reflection\ClassMemberReflection
public function getPrototype(): ClassMemberReflection
{
return $this;
}
Expand All @@ -227,12 +243,16 @@ public function getDeprecatedDescription(): ?string
return null;
}

public function getThrowType(): ?\PHPStan\Type\Type
public function getThrowType(): ?Type
{
if (array_key_exists($this->methodName, $this->methodThrowTypeMap)) {
return new ObjectType($this->methodThrowTypeMap[$this->methodName]);
}

return null;
}

public function hasSideEffects(): \PHPStan\TrinaryLogic
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
Expand Down
93 changes: 93 additions & 0 deletions tests/Reflection/MacroMethodsClassReflectionExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Reflection;

use Generator;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use NunoMaduro\Larastan\Methods\Extension;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\ObjectType;

class MacroMethodsClassReflectionExtensionTest extends PHPStanTestCase
{
/**
* @var ReflectionProvider
*/
private $reflectionProvider;

/**
* @var Extension
*/
private $reflectionExtension;

/** @var string */
private $laravelVersion;

protected function setUp(): void
{
parent::setUp();

$this->reflectionProvider = $this->createReflectionProvider();
$this->reflectionExtension = new Extension(
self::getContainer()->getByType(PhpMethodReflectionFactory::class),
$this->reflectionProvider
);
$this->laravelVersion = LARAVEL_VERSION;
}

/**
* @test
*
* @dataProvider methodAndClassProvider
*/
public function it_can_find_macros_on_a_class(string $class, string $methodName, string $laravelVersion)
{
if ($laravelVersion !== '' && version_compare($this->laravelVersion, $laravelVersion, '<')) {
$this->markTestSkipped('This test requires Laravel 8.0 or higher.');
}

$requestClass = $this->reflectionProvider->getClass($class);

$this->assertTrue($this->reflectionExtension->hasMethod($requestClass, $methodName));
}

/**
* @test
*
* @dataProvider methodAndThrowTypeProvider
*/
public function it_can_set_throw_type_for_macros(string $class, string $methodName, string $exceptionClass)
{
$requestClass = $this->reflectionProvider->getClass($class);

$this->assertTrue($this->reflectionExtension->hasMethod($requestClass, $methodName));

$method = $this->reflectionExtension->getMethod($requestClass, $methodName);

$this->assertNotNull($method->getThrowType());
$this->assertInstanceOf(ObjectType::class, $method->getThrowType());
$this->assertSame($exceptionClass, $method->getThrowType()->getClassName());
}

public function methodAndClassProvider(): Generator
{
yield [Request::class, 'validate', ''];
yield [Request::class, 'validateWithBag', ''];
yield [Request::class, 'hasValidSignature', ''];
yield [Request::class, 'hasValidRelativeSignature', '8.0'];
}

public function methodAndThrowTypeProvider(): Generator
{
yield [Request::class, 'validate', ValidationException::class];
yield [Request::class, 'validateWithBag', ValidationException::class];
}

public static function getAdditionalConfigFiles(): array
{
return [__DIR__.'/../../extension.neon'];
}
}
30 changes: 30 additions & 0 deletions tests/Type/MethodsClassReflectionExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Type;

class MethodsClassReflectionExtensionTest extends \PHPStan\Testing\TypeInferenceTestCase
{
/**
* @return iterable<mixed>
*/
public function dataFileAsserts(): iterable
{
yield from $this->gatherAssertTypes(__DIR__.'/data/macros.php');
}

/**
* @dataProvider dataFileAsserts
*/
public function testFileAsserts(
string $assertType,
string $file,
...$args
): void {
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [__DIR__.'/../../extension.neon'];
}
}
16 changes: 16 additions & 0 deletions tests/Type/data/macros.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Macros;

use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use function PHPStan\Testing\assertVariableCertainty;
use PHPStan\TrinaryLogic;

try {
Request::validate([]);
} catch (ValidationException $e) {
$foo = 'foo';
}

assertVariableCertainty(TrinaryLogic::createMaybe(), $foo);

0 comments on commit 3b7e6ea

Please sign in to comment.