From 9e947b3010da26e5d0c80abefdd8a03bf046f022 Mon Sep 17 00:00:00 2001 From: romalytvynenko Date: Thu, 25 Jul 2024 13:34:58 +0300 Subject: [PATCH] Improving extensions system (#467) * improved index builders * string type roll back * implemented setter calls analysis * added more stuff * upd formatting * remove unused var --- src/Infer/Analyzer/ClassAnalyzer.php | 6 + src/Infer/Extensions/IndexBuildingBroker.php | 33 +++++ src/Infer/Scope/Scope.php | 23 +++- src/Infer/Services/ConstFetchTypeGetter.php | 33 +++++ src/Infer/Services/ReferenceTypeResolver.php | 123 ++++++++++++++---- src/Infer/Services/TemplateTypesSolver.php | 106 +++++++++++++++ .../ClassConstFetchTypeGetter.php | 20 +++ src/ScrambleServiceProvider.php | 17 +++ src/Support/IndexBuilders/IndexBuilder.php | 11 ++ .../RequestParametersBuilder.php | 6 +- .../RulesExtractor/RulesToParameter.php | 2 +- src/Support/RouteInfo.php | 4 + .../Reference/ConstFetchReferenceType.php | 23 ++++ .../SideEffects/SelfTemplateDefinition.php | 36 +++++ tests/Infer/Analyzer/ClassAnalyzerTest.php | 9 ++ tests/Infer/stubs/ChildParentSetterCalls.php | 34 +++++ 16 files changed, 455 insertions(+), 31 deletions(-) create mode 100644 src/Infer/Extensions/IndexBuildingBroker.php create mode 100644 src/Infer/Services/ConstFetchTypeGetter.php create mode 100644 src/Infer/Services/TemplateTypesSolver.php create mode 100644 src/Support/IndexBuilders/IndexBuilder.php create mode 100644 src/Support/Type/Reference/ConstFetchReferenceType.php create mode 100644 tests/Infer/stubs/ChildParentSetterCalls.php diff --git a/src/Infer/Analyzer/ClassAnalyzer.php b/src/Infer/Analyzer/ClassAnalyzer.php index 05a9306e..d5b1ebe9 100644 --- a/src/Infer/Analyzer/ClassAnalyzer.php +++ b/src/Infer/Analyzer/ClassAnalyzer.php @@ -44,8 +44,14 @@ public function analyze(string $name): ClassDefinition $classReflection = new ReflectionClass($name); $parentDefinition = null; + if ($classReflection->getParentClass() && $this->shouldAnalyzeParentClass($classReflection->getParentClass())) { $parentDefinition = $this->analyze($parentName = $classReflection->getParentClass()->name); + } elseif ($classReflection->getParentClass() && ! $this->shouldAnalyzeParentClass($classReflection->getParentClass())) { + // @todo: Here we still want to fire the event, so we can add some details to the definition. + $parentDefinition = new ClassDefinition($parentName = $classReflection->getParentClass()->name); + + Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($parentDefinition->name, $parentDefinition)); } /* diff --git a/src/Infer/Extensions/IndexBuildingBroker.php b/src/Infer/Extensions/IndexBuildingBroker.php new file mode 100644 index 00000000..fa175e51 --- /dev/null +++ b/src/Infer/Extensions/IndexBuildingBroker.php @@ -0,0 +1,33 @@ +indexBuilders as $indexBuilder) { + if (is_a($indexBuilder, $builderClassName)) { + return $indexBuilder->bag; + } + } + + return new Bag; + } + + public function handleEvent($event) + { + foreach ($this->indexBuilders as $indexBuilder) { + if ($indexBuilder instanceof IndexBuilder) { + $indexBuilder->handleEvent($event); + } + } + } +} diff --git a/src/Infer/Scope/Scope.php b/src/Infer/Scope/Scope.php index ef9cd5d6..882a6a63 100644 --- a/src/Infer/Scope/Scope.php +++ b/src/Infer/Scope/Scope.php @@ -124,10 +124,19 @@ public function getType(Node $node): Type return $this->setType($node, new UnknownType("Cannot infer type of method [{$node->name->name}] call on template type: not supported yet.")); } - return $this->setType( - $node, - new MethodCallReferenceType($calleeType, $node->name->name, $this->getArgsTypes($node->args)), - ); + $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; + } + + return $this->setType($node, $referenceType); } if ($node instanceof Node\Expr\StaticCall) { @@ -193,12 +202,16 @@ public function getType(Node $node): Type return $type; } - private function getArgsTypes(array $args) + // @todo: Move to some helper, Scope should be passed as a dependency. + public function getArgsTypes(array $args) { return collect($args) ->filter(fn ($arg) => $arg instanceof Node\Arg) ->mapWithKeys(function (Node\Arg $arg, $index) { $type = $this->getType($arg->value); + if ($parsedPhpDoc = $arg->getAttribute('parsedPhpDoc')) { + $type->setAttribute('docNode', $parsedPhpDoc); + } if (! $arg->unpack) { return [$arg->name ? $arg->name->name : $index => $type]; diff --git a/src/Infer/Services/ConstFetchTypeGetter.php b/src/Infer/Services/ConstFetchTypeGetter.php new file mode 100644 index 00000000..e14d6169 --- /dev/null +++ b/src/Infer/Services/ConstFetchTypeGetter.php @@ -0,0 +1,33 @@ +getValue(); + + $type = TypeHelper::createTypeFromValue($constantValue); + + if ($type) { + return $type; + } + } catch (\ReflectionException $e) { + return new UnknownType('Cannot get const value'); + } + + return new UnknownType('ConstFetchTypeGetter is not yet implemented fully for non-class const fetches.'); + } +} diff --git a/src/Infer/Services/ReferenceTypeResolver.php b/src/Infer/Services/ReferenceTypeResolver.php index 0765bc0d..99150c02 100644 --- a/src/Infer/Services/ReferenceTypeResolver.php +++ b/src/Infer/Services/ReferenceTypeResolver.php @@ -7,6 +7,7 @@ use Dedoc\Scramble\Infer\Definition\ClassDefinition; use Dedoc\Scramble\Infer\Definition\ClassPropertyDefinition; use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition; +use Dedoc\Scramble\Infer\Extensions\Event\ClassDefinitionCreatedEvent; use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent; use Dedoc\Scramble\Infer\Extensions\Event\StaticMethodCallEvent; use Dedoc\Scramble\Infer\Scope\Index; @@ -17,6 +18,7 @@ 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\ConstFetchReferenceType; use Dedoc\Scramble\Support\Type\Reference\Dependency\ClassDependency; use Dedoc\Scramble\Support\Type\Reference\Dependency\FunctionDependency; use Dedoc\Scramble\Support\Type\Reference\Dependency\MethodDependency; @@ -163,6 +165,10 @@ private function checkDependencies(AbstractReferenceType $type) private function doResolve(Type $t, Type $type, Scope $scope) { $resolver = function () use ($t, $scope) { + if ($t instanceof ConstFetchReferenceType) { + return $this->resolveConstFetchReferenceType($scope, $t); + } + if ($t instanceof MethodCallReferenceType) { return $this->resolveMethodCallReferenceType($scope, $t); } @@ -197,6 +203,28 @@ private function doResolve(Type $t, Type $type, Scope $scope) return $this->resolve($scope, $resolved); } + private function resolveConstFetchReferenceType(Scope $scope, ConstFetchReferenceType $type) + { + $analyzedType = clone $type; + + if ($type->callee instanceof StaticReference) { + $contextualCalleeName = match ($type->callee->keyword) { + StaticReference::SELF => $scope->context->functionDefinition?->definingClassName, + StaticReference::STATIC => $scope->context->classDefinition?->name, + StaticReference::PARENT => $scope->context->classDefinition?->parentFqn, + }; + + // This can only happen if any of static reserved keyword used in non-class context – hence considering not possible for now. + if (! $contextualCalleeName) { + return new UnknownType("Cannot properly analyze [{$type->toString()}] reference type as static keyword used in non-class context, or current class scope has no parent."); + } + + $analyzedType->callee = $contextualCalleeName; + } + + return (new ConstFetchTypeGetter)($scope, $analyzedType->callee, $analyzedType->constName); + } + private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenceType $type) { // (#self).listTableDetails() @@ -204,8 +232,7 @@ private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenc // (#TName).listTableDetails() $type->arguments = array_map( - // @todo: fix resolving arguments when deep arg is reference - fn ($t) => $t instanceof AbstractReferenceType ? $this->resolve($scope, $t) : $t, + fn ($t) => $this->resolve($scope, $t), $type->arguments, ); @@ -219,18 +246,24 @@ private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenc throw new \LogicException('Should not happen.'); } + $event = null; + // Attempting extensions broker before potentially giving up on type inference if (($calleeType instanceof TemplateType || $calleeType instanceof ObjectType)) { $unwrappedType = $calleeType instanceof TemplateType && $calleeType->is ? $calleeType->is : $calleeType; - if ($unwrappedType instanceof ObjectType && $returnType = Context::getInstance()->extensionsBroker->getMethodReturnType(new MethodCallEvent( - instance: $unwrappedType, - name: $type->methodName, - scope: $scope, - arguments: $type->arguments, - ))) { + if ($unwrappedType instanceof ObjectType) { + $event = new MethodCallEvent( + instance: $unwrappedType, + name: $type->methodName, + scope: $scope, + arguments: $type->arguments, + ); + } + + if ($event && $returnType = Context::getInstance()->extensionsBroker->getMethodReturnType($event)) { return $returnType; } } @@ -257,7 +290,7 @@ private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenc return new UnknownType("Cannot get a method type [$type->methodName] on type [$name]"); } - return $this->getFunctionCallResult($methodDefinition, $type->arguments, $calleeType); + return $this->getFunctionCallResult($methodDefinition, $type->arguments, $calleeType, $event); } private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethodCallReferenceType $type) @@ -314,7 +347,9 @@ private function resolveUnknownClassResolver(string $className): ?ClassDefinitio $reflection = new \ReflectionClass($className); if (Str::contains($reflection->getFileName(), '/vendor/')) { - return null; + Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($className, new ClassDefinition($className))); + + return $this->index->getClassDefinition($className); } return (new ClassAnalyzer($this->index))->analyze($className); @@ -396,12 +431,14 @@ private function resolveNewCallReferenceType(Scope $scope, NewCallReferenceType ->merge($propertyDefaultTemplateTypes) ->merge($inferredConstructorParamTemplates); - return new Generic( + $type = new Generic( $classDefinition->name, collect($classDefinition->templateTypes) ->map(fn (TemplateType $t) => $inferredTemplates->get($t->name, new UnknownType)) ->toArray(), ); + + return $this->getMethodCallsSideEffectIntroducedTypesInConstructor($type, $scope, $classDefinition, $constructorDefinition); } private function resolvePropertyFetchReferenceType(Scope $scope, PropertyFetchReferenceType $type) @@ -449,6 +486,7 @@ private function getFunctionCallResult( array $arguments, /* When this is a handling for method call */ ObjectType|SelfType|null $calledOnType = null, + ?MethodCallEvent $event = null, ) { $returnType = $callee->type->getReturnType(); $isSelf = false; @@ -514,18 +552,9 @@ private function getFunctionCallResult( $sideEffect instanceof SelfTemplateDefinition && $isSelf && $returnType instanceof Generic + && $event ) { - $templateType = $sideEffect->type instanceof TemplateType - ? collect($inferredTemplates)->get($sideEffect->type->name, new UnknownType) - : $sideEffect->type; - - if (! isset($templateNameToIndexMap[$sideEffect->definedTemplate])) { - throw new \LogicException('Should not happen'); - } - - $templateIndex = $templateNameToIndexMap[$sideEffect->definedTemplate]; - - $returnType->templateTypes[$templateIndex] = $templateType; + $sideEffect->apply($returnType, $event); } } @@ -623,6 +652,10 @@ private function getParentConstructCallsTypes(ClassDefinition $classDefinition, $parentClassDefinition = $this->index->getClassDefinition($classDefinition->parentFqn); + if (! $parentClassDefinition) { + return collect(); + } + $templateArgs = collect($this->resolveTypesTemplatesFromArguments( $parentClassDefinition->templateTypes, $parentClassDefinition->getMethodDefinition('__construct')?->type->arguments ?? [], @@ -633,4 +666,50 @@ private function getParentConstructCallsTypes(ClassDefinition $classDefinition, ->getParentConstructCallsTypes($parentClassDefinition, $parentClassDefinition->getMethodDefinition('__construct')) ->merge($templateArgs); } + + private function getMethodCallsSideEffectIntroducedTypesInConstructor(Generic $type, Scope $scope, ClassDefinition $classDefinition, ?FunctionLikeDefinition $constructorDefinition): Type + { + if (! $constructorDefinition) { + return $type; + } + + $mappo = new \WeakMap; + foreach ($constructorDefinition->sideEffects as $se) { + if (! $se instanceof MethodCallReferenceType) { + continue; + } + + if ((! $se->callee instanceof SelfType) && ($mappo->offsetExists($se->callee) && ! $mappo->offsetGet($se->callee) instanceof SelfType)) { + continue; + } + + // at this point we know that this is a method call on a self type + $resultingType = $this->resolveMethodCallReferenceType($scope, $se); + + // $resultingType will be Self type if $this is returned, and we're in context of fluent setter + + $mappo->offsetSet($se, $resultingType); + + $methodDefinition = ($methodDependency = collect($se->dependencies())->first(fn ($d) => $d instanceof MethodDependency)) + ? $this->index->getClassDefinition($methodDependency->class)?->getMethodDefinition($methodDependency->name) + : null; + + if (! $methodDefinition) { + continue; + } + + if (! $type instanceof ObjectType) { + continue; + } + + $type = $this->getFunctionCallResult($methodDefinition, $se->arguments, $type, new MethodCallEvent( + instance: $type, + name: $se->methodName, + scope: $scope, + arguments: $se->arguments, + )); + } + + return $type; + } } diff --git a/src/Infer/Services/TemplateTypesSolver.php b/src/Infer/Services/TemplateTypesSolver.php new file mode 100644 index 00000000..deb9a0bb --- /dev/null +++ b/src/Infer/Services/TemplateTypesSolver.php @@ -0,0 +1,106 @@ +templateTypes)->mapWithKeys(fn ($t, $index) => [ + $t->name => $type->templateTypes[$index] ?? new UnknownType, + ])->toArray(); + } + + public function getFunctionContextTemplates(FunctionLikeDefinition $functionLikeDefinition, array $arguments) + { + return collect($this->resolveTypesTemplatesFromArguments( + $functionLikeDefinition->type->templates, + $functionLikeDefinition->type->arguments, + $this->prepareArguments($functionLikeDefinition, $arguments), + ))->mapWithKeys(fn ($searchReplace) => [$searchReplace[0]->name => $searchReplace[1]])->toArray(); + } + + /** + * Prepares the actual arguments list with which a function is going to be executed, taking into consideration + * arguments defaults. + * + * @param ?FunctionLikeDefinition $callee + * @param array $realArguments The list of arguments a function has been called with. + * @return array The actual list of arguments where not passed arguments replaced with default values. + */ + private function prepareArguments(?FunctionLikeDefinition $callee, array $realArguments) + { + if (! $callee) { + return $realArguments; + } + + return collect($callee->type->arguments) + ->keys() + ->map(function (string $name, int $index) use ($callee, $realArguments) { + return $realArguments[$name] ?? $realArguments[$index] ?? $callee->argumentsDefaults[$name] ?? null; + }) + ->filter() + ->values() + ->toArray(); + } + + private function resolveTypesTemplatesFromArguments($templates, $templatedArguments, $realArguments) + { + return array_values(array_filter(array_map(function (TemplateType $template) use ($templatedArguments, $realArguments) { + $argumentIndexName = null; + $index = 0; + foreach ($templatedArguments as $name => $type) { + if ($type === $template) { + $argumentIndexName = [$index, $name]; + break; + } + $index++; + } + if (! $argumentIndexName) { + return null; + } + + $foundCorrespondingTemplateType = $realArguments[$argumentIndexName[1]] + ?? $realArguments[$argumentIndexName[0]] + ?? null; + + if (! $foundCorrespondingTemplateType) { + $foundCorrespondingTemplateType = new UnknownType; + // throw new \LogicException("Cannot infer type of template $template->name from arguments."); + } + + return [ + $template, + $foundCorrespondingTemplateType, + ]; + }, $templates))); + } + + /** + * For a given generic type, defined a template type by the template type name. + */ + public function defineTemplateTypes(?ClassDefinition $classDefinition, Generic $type, string $definedTemplate, Type $definedType) + { + $templateNameToIndexMap = array_flip(array_map(fn ($t) => $t->name, $classDefinition->templateTypes ?? [])); + + if (! isset($templateNameToIndexMap[$definedTemplate])) { + throw new \LogicException('Should not happen'); + } + + $templateIndex = $templateNameToIndexMap[$definedTemplate]; + + $type->templateTypes[$templateIndex] = $definedType; + } +} diff --git a/src/Infer/SimpleTypeGetters/ClassConstFetchTypeGetter.php b/src/Infer/SimpleTypeGetters/ClassConstFetchTypeGetter.php index fe140023..4edfe62c 100644 --- a/src/Infer/SimpleTypeGetters/ClassConstFetchTypeGetter.php +++ b/src/Infer/SimpleTypeGetters/ClassConstFetchTypeGetter.php @@ -5,7 +5,9 @@ use Dedoc\Scramble\Infer\Scope\Scope; use Dedoc\Scramble\Support\Type\Literal\LiteralStringType; use Dedoc\Scramble\Support\Type\ObjectType; +use Dedoc\Scramble\Support\Type\Reference\ConstFetchReferenceType; use Dedoc\Scramble\Support\Type\Reference\NewCallReferenceType; +use Dedoc\Scramble\Support\Type\Reference\StaticReference; use Dedoc\Scramble\Support\Type\StringType; use Dedoc\Scramble\Support\Type\Type; use Dedoc\Scramble\Support\Type\UnknownType; @@ -25,7 +27,25 @@ public function __invoke(Node\Expr\ClassConstFetch $node, Scope $scope): Type if ($type instanceof ObjectType || $type instanceof NewCallReferenceType) { return new LiteralStringType($type->name); } + } + + if ( + $node->class instanceof Node\Name + && $node->name instanceof Node\Identifier + ) { + $className = in_array($node->class->toString(), StaticReference::KEYWORDS) + ? new StaticReference($node->class->toString()) + : $node->class->toString(); + return new ConstFetchReferenceType( + $className, + $node->name->toString(), + ); + } + + // In case we're here, it means that we were unable to infer the type from the const fetch. So we rollback to the + // string type. + if ($node->name->toString() === 'class') { return new StringType; } diff --git a/src/ScrambleServiceProvider.php b/src/ScrambleServiceProvider.php index 93997a85..e1ad59e8 100644 --- a/src/ScrambleServiceProvider.php +++ b/src/ScrambleServiceProvider.php @@ -8,6 +8,7 @@ use Dedoc\Scramble\Extensions\OperationExtension; use Dedoc\Scramble\Extensions\TypeToSchemaExtension; use Dedoc\Scramble\Infer\Extensions\ExtensionsBroker; +use Dedoc\Scramble\Infer\Extensions\IndexBuildingBroker; use Dedoc\Scramble\Infer\Extensions\InferExtension; use Dedoc\Scramble\Infer\Scope\Index; use Dedoc\Scramble\Infer\Services\FileParser; @@ -18,6 +19,7 @@ use Dedoc\Scramble\Support\ExceptionToResponseExtensions\ValidationExceptionToResponseExtension; use Dedoc\Scramble\Support\Generator\Components; use Dedoc\Scramble\Support\Generator\TypeTransformer; +use Dedoc\Scramble\Support\IndexBuilders\IndexBuilder; use Dedoc\Scramble\Support\InferExtensions\AbortHelpersExceptionInfer; use Dedoc\Scramble\Support\InferExtensions\JsonResourceCallsTypeInfer; use Dedoc\Scramble\Support\InferExtensions\JsonResourceCreationInfer; @@ -137,6 +139,21 @@ public function configurePackage(Package $package): void ], $operationExtensions); }); + $this->app->when(IndexBuildingBroker::class) + ->needs('$indexBuilders') + ->give(function () { + $extensions = array_merge(config('scramble.extensions', []), Scramble::$extensions); + + $indexBuilders = array_values(array_filter( + $extensions, + fn ($e) => is_a($e, IndexBuilder::class, true), + )); + + return array_map(function ($class) { + return app($class); + }, $indexBuilders); + }); + $this->app->singleton(ServerFactory::class); $this->app->singleton(TypeTransformer::class, function () { diff --git a/src/Support/IndexBuilders/IndexBuilder.php b/src/Support/IndexBuilders/IndexBuilder.php new file mode 100644 index 00000000..56a017f2 --- /dev/null +++ b/src/Support/IndexBuilders/IndexBuilder.php @@ -0,0 +1,11 @@ +schema->type->default($default[0]); } - if ($format = $this->docNode->getTagsByName('@format')[0]->value->value ?? null) { + if ($format = array_values($this->docNode->getTagsByName('@format'))[0]->value->value ?? null) { $parameter->schema->type->format($format); } diff --git a/src/Support/RouteInfo.php b/src/Support/RouteInfo.php index a25d43fa..404d3b15 100644 --- a/src/Support/RouteInfo.php +++ b/src/Support/RouteInfo.php @@ -30,12 +30,15 @@ class RouteInfo public readonly Bag $requestParametersFromCalls; + public readonly Infer\Extensions\IndexBuildingBroker $indexBuildingBroker; + public function __construct(Route $route, FileParser $fileParser, Infer $infer) { $this->route = $route; $this->parser = $fileParser; $this->infer = $infer; $this->requestParametersFromCalls = new Bag; + $this->indexBuildingBroker = app(Infer\Extensions\IndexBuildingBroker::class); } public function isClassBased(): bool @@ -118,6 +121,7 @@ public function getMethodType(): ?FunctionType */ $this->methodType = $def->getMethodDefinition($this->methodName(), indexBuilders: [ new RequestParametersBuilder($this->requestParametersFromCalls), + ...$this->indexBuildingBroker->indexBuilders, ])->type; } diff --git a/src/Support/Type/Reference/ConstFetchReferenceType.php b/src/Support/Type/Reference/ConstFetchReferenceType.php new file mode 100644 index 00000000..6d4f15f6 --- /dev/null +++ b/src/Support/Type/Reference/ConstFetchReferenceType.php @@ -0,0 +1,23 @@ +callee) ? $this->callee : $this->callee->toString(); + + return "(#{$callee})::{$this->constName}"; + } + + public function dependencies(): array + { + return []; + } +} diff --git a/src/Support/Type/SideEffects/SelfTemplateDefinition.php b/src/Support/Type/SideEffects/SelfTemplateDefinition.php index bfe3ab89..bcbf17ee 100644 --- a/src/Support/Type/SideEffects/SelfTemplateDefinition.php +++ b/src/Support/Type/SideEffects/SelfTemplateDefinition.php @@ -2,7 +2,12 @@ namespace Dedoc\Scramble\Support\Type\SideEffects; +use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent; +use Dedoc\Scramble\Infer\Services\TemplateTypesSolver; +use Dedoc\Scramble\Support\Type\Generic; +use Dedoc\Scramble\Support\Type\TemplateType; use Dedoc\Scramble\Support\Type\Type; +use Dedoc\Scramble\Support\Type\UnknownType; class SelfTemplateDefinition { @@ -10,4 +15,35 @@ public function __construct( public string $definedTemplate, public Type $type, ) {} + + public function apply(Generic $type, MethodCallEvent $event) + { + $templateDefinitions = $this->getAllDefinedTemplates($type, $event); + + $templateType = $this->type instanceof TemplateType + ? collect($templateDefinitions)->get($this->type->name, new UnknownType) + : $this->type; + + (new TemplateTypesSolver)->defineTemplateTypes( + $event->getDefinition(), + $type, + $this->definedTemplate, + $templateType, + ); + } + + /** + * Returns the map of all template names to the values. This includes template names of a class, and template names + * of a given method. + */ + public function getAllDefinedTemplates(Generic $type, MethodCallEvent $event): array + { + $classDefinition = $event->getDefinition(); + + $classDefinedTemplates = (new TemplateTypesSolver)->getClassContextTemplates($type, $classDefinition); + + $methodDefinedTemplates = (new TemplateTypesSolver)->getFunctionContextTemplates($classDefinition->getMethodDefinition($event->name), $event->arguments); + + return array_merge($classDefinedTemplates, $methodDefinedTemplates); + } } diff --git a/tests/Infer/Analyzer/ClassAnalyzerTest.php b/tests/Infer/Analyzer/ClassAnalyzerTest.php index 5ab3d77d..8c82df35 100644 --- a/tests/Infer/Analyzer/ClassAnalyzerTest.php +++ b/tests/Infer/Analyzer/ClassAnalyzerTest.php @@ -8,6 +8,7 @@ use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver; use Dedoc\Scramble\Tests\Infer\stubs\Bar; use Dedoc\Scramble\Tests\Infer\stubs\Child; +use Dedoc\Scramble\Tests\Infer\stubs\ChildParentSetterCalls; use Dedoc\Scramble\Tests\Infer\stubs\ChildPromotion; use Dedoc\Scramble\Tests\Infer\stubs\DeepChild; use Dedoc\Scramble\Tests\Infer\stubs\Foo; @@ -95,3 +96,11 @@ expect($type->toString())->toBe('Dedoc\Scramble\Tests\Infer\stubs\ChildPromotion'); }); + +it('analyzes call to parent setter methods in child constructor', function () { + $this->classAnalyzer->analyze(ChildParentSetterCalls::class); + + $type = getStatementType('new Dedoc\Scramble\Tests\Infer\stubs\ChildParentSetterCalls("some", "wow")'); + + expect($type->toString())->toBe('Dedoc\Scramble\Tests\Infer\stubs\ChildParentSetterCalls'); +}); diff --git a/tests/Infer/stubs/ChildParentSetterCalls.php b/tests/Infer/stubs/ChildParentSetterCalls.php new file mode 100644 index 00000000..e0df4c8f --- /dev/null +++ b/tests/Infer/stubs/ChildParentSetterCalls.php @@ -0,0 +1,34 @@ +foo = $foo; + + return $this; + } + + public function setWow(string $wow) + { + $this->wow = $wow; + + return $this; + } +} + +class ChildParentSetterCalls extends _ParentChildParentSetterCalls +{ + public function __construct(public string $bar, string $wow) + { + parent::__construct('constructor call', $wow); + + $this + ->setFoo('from ChildParentSetterCalls constructor') + ->setWow('from ChildParentSetterCalls wow'); + } +}