Skip to content

Commit

Permalink
Fixed return type inference from models when in JSON API resource con…
Browse files Browse the repository at this point in the history
…text (#516)

* removed old model analysis approach

* moved stuff from old extension to simpler new one

* resolving unknown classes before firing the events

* fix enums being not correctly indexed and transformed

* Fix styling

* remove json infer extension

* Fix styling

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Sep 4, 2024
1 parent 224be1b commit 8eb2672
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 344 deletions.
12 changes: 11 additions & 1 deletion src/Infer/Definition/ClassDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Dedoc\Scramble\Infer\Definition;

use Dedoc\Scramble\Infer\Analyzer\MethodAnalyzer;
use Dedoc\Scramble\Infer\Reflector\ClassReflector;
use Dedoc\Scramble\Infer\Scope\GlobalScope;
use Dedoc\Scramble\Infer\Scope\NodeTypesResolver;
use Dedoc\Scramble\Infer\Scope\Scope;
Expand Down Expand Up @@ -66,7 +67,11 @@ public function getMethodDefinition(string $name, Scope $scope = new GlobalScope
$scope->index,
new NodeTypesResolver,
new ScopeContext($this, $methodDefinition),
new FileNameResolver(tap(new NameContext(new Throwing), fn (NameContext $nc) => $nc->startNamespace())),
new FileNameResolver(
class_exists($this->name)
? ClassReflector::make($this->name)->getNameContext()
: tap(new NameContext(new Throwing), fn (NameContext $nc) => $nc->startNamespace()),
),
);

(new ReferenceTypeResolver($scope->index))
Expand All @@ -88,6 +93,11 @@ public function getPropertyDefinition($name)
return $this->properties[$name] ?? null;
}

public function hasPropertyDefinition(string $name): bool
{
return array_key_exists($name, $this->properties);
}

public function getMethodCallType(string $name, ?ObjectType $calledOn = null)
{
$methodDefinition = $this->methods[$name] ?? null;
Expand Down
8 changes: 0 additions & 8 deletions src/Infer/Scope/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Dedoc\Scramble\Infer\Definition\ClassDefinition;
use Dedoc\Scramble\Infer\Services\FileNameResolver;
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
use Dedoc\Scramble\Infer\SimpleTypeGetters\BooleanNotTypeGetter;
use Dedoc\Scramble\Infer\SimpleTypeGetters\CastTypeGetter;
use Dedoc\Scramble\Infer\SimpleTypeGetters\ClassConstFetchTypeGetter;
Expand Down Expand Up @@ -323,11 +322,4 @@ private function getVariableType(Node\Expr\Variable $node)

return $type;
}

public function getMethodCallType(Type $calledOn, string $methodName, array $arguments = []): Type {}

public function getPropertyFetchType(Type $calledOn, string $propertyName): Type
{
return (new ReferenceTypeResolver($this->index))->resolve($this, new PropertyFetchReferenceType($calledOn, $propertyName));
}
}
26 changes: 17 additions & 9 deletions src/Infer/Services/ReferenceTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenc
throw new \LogicException('Should not happen.');
}

/*
* Doing a deep dive into the dependent class, if it has not been analyzed.
*/
if ($calleeType instanceof ObjectType) {
$this->resolveUnknownClass($calleeType->name);
}

$event = null;

// Attempting extensions broker before potentially giving up on type inference
Expand Down Expand Up @@ -280,7 +287,6 @@ private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenc
if (
($calleeType instanceof ObjectType)
&& ! array_key_exists($calleeType->name, $this->index->classesDefinitions)
&& ! $this->resolveUnknownClassResolver($calleeType->name)
) {
return new UnknownType;
}
Expand Down Expand Up @@ -315,6 +321,11 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod
// Assuming callee here can be only string of known name. Reality is more complex than
// that, but it is fine for now.

/*
* Doing a deep dive into the dependent class, if it has not been analyzed.
*/
$this->resolveUnknownClass($type->callee);

// Attempting extensions broker before potentially giving up on type inference
if ($returnType = Context::getInstance()->extensionsBroker->getStaticMethodReturnType(new StaticMethodCallEvent(
callee: $type->callee,
Expand All @@ -325,10 +336,7 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod
return $returnType;
}

if (
! array_key_exists($type->callee, $this->index->classesDefinitions)
&& ! $this->resolveUnknownClassResolver($type->callee)
) {
if (! array_key_exists($type->callee, $this->index->classesDefinitions)) {
return new UnknownType;
}

Expand All @@ -342,7 +350,7 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod
return $this->getFunctionCallResult($methodDefinition, $type->arguments);
}

private function resolveUnknownClassResolver(string $className): ?ClassDefinition
private function resolveUnknownClass(string $className): ?ClassDefinition
{
try {
$reflection = new \ReflectionClass($className);
Expand Down Expand Up @@ -419,7 +427,7 @@ private function resolveNewCallReferenceType(Scope $scope, NewCallReferenceType

if (
! array_key_exists($type->name, $this->index->classesDefinitions)
&& ! $this->resolveUnknownClassResolver($type->name)
&& ! $this->resolveUnknownClass($type->name)
) {
/*
* Usually in this case we want to return UnknownType. But we certainly know that using `new` will produce
Expand Down Expand Up @@ -469,7 +477,7 @@ private function resolvePropertyFetchReferenceType(Scope $scope, PropertyFetchRe
if (
($objectType instanceof ObjectType)
&& ! array_key_exists($objectType->name, $this->index->classesDefinitions)
&& ! $this->resolveUnknownClassResolver($objectType->name)
&& ! $this->resolveUnknownClass($objectType->name)
) {
// Class is not indexed, and we simply cannot get an info from it.
return $type;
Expand Down Expand Up @@ -499,7 +507,7 @@ private function resolvePropertyFetchReferenceType(Scope $scope, PropertyFetchRe
return new UnknownType("Cannot get property [$type->propertyName] type on [$name]");
}

return $objectType->getPropertyType($type->propertyName);
return $objectType->getPropertyType($type->propertyName, $scope);
}

private function getFunctionCallResult(
Expand Down
2 changes: 0 additions & 2 deletions src/ScrambleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
use Dedoc\Scramble\Support\InferExtensions\JsonResourceCallsTypeInfer;
use Dedoc\Scramble\Support\InferExtensions\JsonResourceCreationInfer;
use Dedoc\Scramble\Support\InferExtensions\JsonResourceExtension;
use Dedoc\Scramble\Support\InferExtensions\JsonResourceTypeInfer;
use Dedoc\Scramble\Support\InferExtensions\JsonResponseMethodReturnTypeExtension;
use Dedoc\Scramble\Support\InferExtensions\ModelExtension;
use Dedoc\Scramble\Support\InferExtensions\PossibleExceptionInfer;
Expand Down Expand Up @@ -105,7 +104,6 @@ public function configurePackage(Package $package): void

new JsonResourceCallsTypeInfer,
new JsonResourceCreationInfer,
new JsonResourceTypeInfer,
new ValidatorTypeInfer,
new ResourceCollectionTypeInfer,
new ResponseFactoryTypeInfer,
Expand Down
4 changes: 2 additions & 2 deletions src/Support/Generator/TypeTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,13 @@ public function transform(Type $type)

if ($stringLiterals->count()) {
$items[] = (new StringType)->enum(
$stringLiterals->map->value->unique()->toArray()
$stringLiterals->map->value->unique()->values()->toArray()
);
}

if ($integerLiterals->count()) {
$items[] = (new IntegerType)->enum(
$integerLiterals->map->value->unique()->toArray()
$integerLiterals->map->value->unique()->values()->toArray()
);
}

Expand Down
70 changes: 70 additions & 0 deletions src/Support/Helpers/JsonResourceHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Dedoc\Scramble\Support\Helpers;

use Dedoc\Scramble\Infer\Definition\ClassDefinition;
use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Infer\Services\FileNameResolver;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\UnknownType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class JsonResourceHelper
{
public static $jsonResourcesModelTypesCache = [];

/**
* @internal
*/
public static function modelType(ClassDefinition $jsonClass, Scope $scope): ?Type
{
if ($cachedModelType = static::$jsonResourcesModelTypesCache[$jsonClass->name] ?? null) {
return $cachedModelType;
}

$modelClass = static::getModelName(
$jsonClass->name,
new \ReflectionClass($jsonClass->name),
$scope->nameResolver,
);

$modelType = new UnknownType("Cannot resolve [$modelClass] model type.");
if ($modelClass && is_a($modelClass, Model::class, true)) {
$modelType = new ObjectType($modelClass);
}

static::$jsonResourcesModelTypesCache[$jsonClass->name] = $modelType;

return $modelType;
}

private static function getModelName(string $jsonResourceClassName, \ReflectionClass $reflectionClass, FileNameResolver $getFqName)
{
$phpDoc = $reflectionClass->getDocComment() ?: '';

$mixinOrPropertyLine = Str::of($phpDoc)
->explode("\n")
->first(fn ($str) => Str::is(['*@property*$resource', '*@mixin*'], $str));

if ($mixinOrPropertyLine) {
$modelName = Str::replace(['@property', '$resource', '@mixin', ' ', '*'], '', $mixinOrPropertyLine);

$modelClass = $getFqName($modelName);

if (class_exists($modelClass)) {
return $modelClass;
}
}

$modelName = (string) Str::of(Str::of($jsonResourceClassName)->explode('\\')->last())->replace('Resource', '')->singular();

$modelClass = 'App\\Models\\'.$modelName;
if (! class_exists($modelClass)) {
return null;
}

return $modelClass;
}
}
88 changes: 81 additions & 7 deletions src/Support/InferExtensions/JsonResourceExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,33 @@
namespace Dedoc\Scramble\Support\InferExtensions;

use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
use Dedoc\Scramble\Infer\Extensions\Event\PropertyFetchEvent;
use Dedoc\Scramble\Infer\Extensions\Event\StaticMethodCallEvent;
use Dedoc\Scramble\Infer\Extensions\MethodReturnTypeExtension;
use Dedoc\Scramble\Infer\Extensions\PropertyTypeExtension;
use Dedoc\Scramble\Infer\Extensions\StaticMethodReturnTypeExtension;
use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
use Dedoc\Scramble\Support\Helpers\JsonResourceHelper;
use Dedoc\Scramble\Support\Type\ArrayType;
use Dedoc\Scramble\Support\Type\BooleanType;
use Dedoc\Scramble\Support\Type\FunctionType;
use Dedoc\Scramble\Support\Type\Generic;
use Dedoc\Scramble\Support\Type\IntegerType;
use Dedoc\Scramble\Support\Type\Literal\LiteralBooleanType;
use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Reference\MethodCallReferenceType;
use Dedoc\Scramble\Support\Type\Reference\PropertyFetchReferenceType;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\Union;
use Dedoc\Scramble\Support\Type\UnknownType;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\MergeValue;
use Illuminate\Http\Resources\MissingValue;

class JsonResourceExtension implements MethodReturnTypeExtension, StaticMethodReturnTypeExtension
class JsonResourceExtension implements MethodReturnTypeExtension, PropertyTypeExtension, StaticMethodReturnTypeExtension
{
public function shouldHandle(ObjectType|string $type): bool
{
Expand All @@ -33,10 +45,46 @@ public function getMethodReturnType(MethodCallEvent $event): ?Type
return match ($event->name) {
// @todo This should work automatically as toArray calls must be proxied to parents.
'toArray' => ($event->getInstance()->name === JsonResource::class || ($event->getDefinition() && ! $event->getDefinition()->hasMethodDefinition('toArray')))
? $this->getToArrayReturn($event->getInstance()->name, $event->arguments, $event->scope)
? $this->getModelMethodReturn($event->getInstance()->name, 'toArray', $event->arguments, $event->scope)
: null,
'response', 'toResponse' => new Generic(JsonResponse::class, [$event->getInstance(), new LiteralIntegerType(200), new ArrayType]),
default => null,

'whenLoaded' => count($event->arguments) === 1
? Union::wrap([
// Relationship type which does not really matter
new UnknownType('Skipped real relationship type extracting'),
new ObjectType(MissingValue::class),
])
: Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

'when' => Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

'merge' => new Generic(MergeValue::class, [
new LiteralBooleanType(true),
$this->value($event->getArg('value', 0)),
]),

'mergeWhen' => new Generic(MergeValue::class, [
new BooleanType,
$this->value($event->getArg('value', 1)),
]),

'whenCounted' => count($event->arguments) === 1
? Union::wrap([new IntegerType, new ObjectType(MissingValue::class)])
: Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

default => ! $event->getDefinition() || $event->getDefinition()->hasMethodDefinition($event->name)
? null
: $this->proxyMethodCallToModel($event),
};
}

Expand All @@ -48,6 +96,22 @@ public function getStaticMethodReturnType(StaticMethodCallEvent $event): ?Type
};
}

public function getPropertyType(PropertyFetchEvent $event): ?Type
{
return match ($event->name) {
'resource' => JsonResourceHelper::modelType($event->getDefinition(), $event->scope),
default => ! $event->getDefinition() || $event->getDefinition()->hasPropertyDefinition($event->name)
? null
: ReferenceTypeResolver::getInstance()->resolve(
$event->scope,
new PropertyFetchReferenceType(
JsonResourceHelper::modelType($event->getDefinition(), $event->scope),
$event->name,
),
),
};
}

/**
* Note: In fact, this is not a static call to the JsonResource. This is how type inference system treats it for
* now, when analyzing parent::toArray() call. `parent::` becomes `JsonResource::`. So this should be fixed in
Expand All @@ -61,16 +125,26 @@ private function handleToArrayStaticCall(StaticMethodCallEvent $event): ?Type
return null;
}

return $this->getToArrayReturn($contextClassName, $event->arguments, $event->scope);
return $this->getModelMethodReturn($contextClassName, 'toArray', $event->arguments, $event->scope);
}

private function getToArrayReturn(string $resourceClassName, array $arguments, Scope $scope)
private function proxyMethodCallToModel(MethodCallEvent $event)
{
$modelType = JsonResourceTypeInfer::modelType($scope->index->getClassDefinition($resourceClassName), $scope);
return $this->getModelMethodReturn($event->getInstance()->name, $event->name, $event->arguments, $event->scope);
}

private function getModelMethodReturn(string $resourceClassName, string $methodName, array $arguments, Scope $scope)
{
$modelType = JsonResourceHelper::modelType($scope->index->getClassDefinition($resourceClassName), $scope);

return ReferenceTypeResolver::getInstance()->resolve(
$scope,
new MethodCallReferenceType($modelType, 'toArray', arguments: $arguments),
new MethodCallReferenceType($modelType, $methodName, arguments: $arguments),
);
}

private function value(Type $type)
{
return $type instanceof FunctionType ? $type->getReturnType() : $type;
}
}
Loading

0 comments on commit 8eb2672

Please sign in to comment.