Skip to content

Commit

Permalink
Added enum case type, invokable type inference, method calls extensio…
Browse files Browse the repository at this point in the history
…ns for exception inference (#627)

* implemented enum case

* implemented invokable inference

* extensions for exceptions

* Fix styling

* removed dummy code

---------

Co-authored-by: romalytvynenko <romalytvynenko@users.noreply.github.com>
  • Loading branch information
romalytvynenko and romalytvynenko authored Nov 15, 2024
1 parent e8c2b8f commit 859708d
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 11 deletions.
17 changes: 17 additions & 0 deletions src/Infer/Extensions/ExtensionsBroker.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ public function getMethodReturnType($event)
return null;
}

public function getMethodCallExceptions($event)
{
$extensions = array_filter($this->extensions, function ($e) use ($event) {
return $e instanceof MethodCallExceptionsExtension
&& $e->shouldHandle($event->getInstance());
});

$exceptions = [];
foreach ($extensions as $extension) {
if ($extensionExceptions = $extension->getMethodCallExceptions($event)) {
$exceptions = array_merge($exceptions, $extensionExceptions);
}
}

return $exceptions;
}

public function getStaticMethodReturnType($event)
{
$extensions = array_filter($this->extensions, function ($e) use ($event) {
Expand Down
17 changes: 17 additions & 0 deletions src/Infer/Extensions/MethodCallExceptionsExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Dedoc\Scramble\Infer\Extensions;

use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;

interface MethodCallExceptionsExtension extends InferExtension
{
public function shouldHandle(ObjectType $type): bool;

/**
* @return array<Type>
*/
public function getMethodCallExceptions(MethodCallEvent $event): array;
}
36 changes: 28 additions & 8 deletions src/Infer/Scope/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Dedoc\Scramble\Infer\Scope;

use Dedoc\Scramble\Infer\Definition\ClassDefinition;
use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
use Dedoc\Scramble\Infer\Extensions\ExtensionsBroker;
use Dedoc\Scramble\Infer\Services\FileNameResolver;
use Dedoc\Scramble\Infer\SimpleTypeGetters\BooleanNotTypeGetter;
use Dedoc\Scramble\Infer\SimpleTypeGetters\CastTypeGetter;
Expand All @@ -14,6 +16,7 @@
use Dedoc\Scramble\Support\Type\CallableStringType;
use Dedoc\Scramble\Support\Type\KeyedArrayType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Reference\AbstractReferenceType;
use Dedoc\Scramble\Support\Type\Reference\CallableCallReferenceType;
use Dedoc\Scramble\Support\Type\Reference\MethodCallReferenceType;
use Dedoc\Scramble\Support\Type\Reference\NewCallReferenceType;
Expand Down Expand Up @@ -115,27 +118,44 @@ public function getType(Node $node): Type
}

$calleeType = $this->getType($node->var);
if ($calleeType instanceof TemplateType) {

$event = $calleeType instanceof ObjectType
? new MethodCallEvent($calleeType, $node->name->name, $this, $this->getArgsTypes($node->args))
: null;

$type = ($event ? app(ExtensionsBroker::class)->getMethodReturnType($event) : null)
?: new MethodCallReferenceType($calleeType, $node->name->name, $this->getArgsTypes($node->args));

$exceptions = $event ? app(ExtensionsBroker::class)->getMethodCallExceptions($event) : [];

if (
$calleeType instanceof TemplateType
&& $type instanceof AbstractReferenceType
&& ! $exceptions
) {
// @todo
// if ($calleeType->is instanceof ObjectType) {
// $calleeType = $calleeType->is;
// }
return $this->setType($node, new UnknownType("Cannot infer type of method [{$node->name->name}] call on template type: not supported yet."));
}

$referenceType = new MethodCallReferenceType($calleeType, $node->name->name, $this->getArgsTypes($node->args));

/*
* When inside a constructor, we want to add a side effect to the constructor definition, so we can track
* how the properties are being set.
*/
if (
$this->functionDefinition()?->type->name === '__construct'
) {
$this->functionDefinition()->sideEffects[] = $referenceType;
if ($this->functionDefinition()?->type->name === '__construct' && $type instanceof AbstractReferenceType) {
$this->functionDefinition()->sideEffects[] = $type;
}

if ($this->functionDefinition()) {
$this->functionDefinition()->type->exceptions = array_merge(
$this->functionDefinition()->type->exceptions,
$exceptions,
);
}

return $this->setType($node, $referenceType);
return $this->setType($node, $type);
}

if ($node instanceof Node\Expr\StaticCall) {
Expand Down
5 changes: 5 additions & 0 deletions src/Infer/Services/ConstFetchTypeGetter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Dedoc\Scramble\Infer\Services;

use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Support\Type\EnumCaseType;
use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
use Dedoc\Scramble\Support\Type\TypeHelper;
use Dedoc\Scramble\Support\Type\UnknownType;
Expand All @@ -19,6 +20,10 @@ public function __invoke(Scope $scope, string $className, string $constName)
$constantReflection = new \ReflectionClassConstant($className, $constName);
$constantValue = $constantReflection->getValue();

if ($constantReflection->isEnumCase()) {
return new EnumCaseType($className, $constName);
}

$type = TypeHelper::createTypeFromValue($constantValue);

if ($type) {
Expand Down
16 changes: 13 additions & 3 deletions src/Infer/Services/ReferenceTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,10 @@ private function resolveUnknownClass(string $className): ?ClassDefinition

private function resolveCallableCallReferenceType(Scope $scope, CallableCallReferenceType $type)
{
if ($type->callee instanceof CallableStringType) {
$callee = $this->resolve($scope, $type->callee);
$callee = $callee instanceof TemplateType ? $callee->is : $callee;

if ($callee instanceof CallableStringType) {
$analyzedType = clone $type;

$analyzedType->arguments = array_map(
Expand All @@ -382,7 +385,7 @@ private function resolveCallableCallReferenceType(Scope $scope, CallableCallRefe
);

$returnType = Context::getInstance()->extensionsBroker->getFunctionReturnType(new FunctionCallEvent(
name: $analyzedType->callee->name,
name: $callee->name,
scope: $scope,
arguments: $analyzedType->arguments,
));
Expand All @@ -392,7 +395,14 @@ private function resolveCallableCallReferenceType(Scope $scope, CallableCallRefe
}
}

$calleeType = $type->callee instanceof CallableStringType
if ($callee instanceof ObjectType) {
return $this->resolve(
$scope,
new MethodCallReferenceType($callee, '__invoke', $type->arguments),
);
}

$calleeType = $callee instanceof CallableStringType
? $this->index->getFunctionDefinition($type->callee->name)
: $this->resolve($scope, $type->callee);

Expand Down
1 change: 1 addition & 0 deletions src/Support/OperationExtensions/ResponseExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo)

$responses = collect($returnTypes)
->merge(optional($routeInfo->getMethodType())->exceptions ?: [])
// ->dd()
->map($this->openApiTransformer->toResponse(...))
->filter()
->unique(fn ($response) => ($response instanceof Response ? $response->code : 'ref').':'.json_encode($response->toArray()))
Expand Down
23 changes: 23 additions & 0 deletions src/Support/Type/EnumCaseType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Dedoc\Scramble\Support\Type;

class EnumCaseType extends ObjectType
{
public function __construct(
string $name,
public string $caseName,
) {
parent::__construct($name);
}

public function isSame(Type $type)
{
return $type instanceof static && $type->toString() === $this->toString();
}

public function toString(): string
{
return "{$this->name}::{$this->caseName}";
}
}
7 changes: 7 additions & 0 deletions tests/Infer/ClassConstFetchTypesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@
['$var::class', 'string'],
['(new SomeType)::class', 'string(SomeType)'],
['SomeType::class', 'string(SomeType)'],
['Enum_ClassConstFetchTypesTest::FOO', 'Enum_ClassConstFetchTypesTest::FOO'],
]);

enum Enum_ClassConstFetchTypesTest: string
{
case FOO = 'foo';
case BAR = 'bar';
}
7 changes: 7 additions & 0 deletions tests/Infer/ReferenceResolutionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Dedoc\Scramble\Support\Type\Generic;
use Dedoc\Scramble\Support\Type\UnknownType;
use Dedoc\Scramble\Tests\Infer\stubs\InvokableFoo;

it('supports creating an object without constructor', function () {
$type = analyzeFile(<<<'EOD'
Expand Down Expand Up @@ -236,3 +237,9 @@ public function car()
return 1;
}
}

it('resolves invokable call from parent class', function () {
$type = analyzeClass(InvokableFoo::class)->getExpressionType('(new Dedoc\Scramble\Tests\Infer\stubs\InvokableFoo)("foo")');

expect($type->toString())->toBe('string(foo)');
});
11 changes: 11 additions & 0 deletions tests/Infer/stubs/InvokableFoo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Dedoc\Scramble\Tests\Infer\stubs;

class InvokableFoo
{
public function __invoke($bar)
{
return $bar;
}
}

0 comments on commit 859708d

Please sign in to comment.