Skip to content

Commit

Permalink
Improving extensions system (#467)
Browse files Browse the repository at this point in the history
* improved index builders

* string type roll back

* implemented setter calls analysis

* added more stuff

* upd formatting

* remove unused var
  • Loading branch information
romalytvynenko committed Jul 25, 2024
1 parent 57bc523 commit 9e947b3
Show file tree
Hide file tree
Showing 16 changed files with 455 additions and 31 deletions.
6 changes: 6 additions & 0 deletions src/Infer/Analyzer/ClassAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/*
Expand Down
33 changes: 33 additions & 0 deletions src/Infer/Extensions/IndexBuildingBroker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Dedoc\Scramble\Infer\Extensions;

use Dedoc\Scramble\Support\IndexBuilders\Bag;
use Dedoc\Scramble\Support\IndexBuilders\IndexBuilder;

class IndexBuildingBroker
{
public function __construct(
public readonly array $indexBuilders = [],
) {}

public function getIndex(string $builderClassName): Bag
{
foreach ($this->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);
}
}
}
}
23 changes: 18 additions & 5 deletions src/Infer/Scope/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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];
Expand Down
33 changes: 33 additions & 0 deletions src/Infer/Services/ConstFetchTypeGetter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Dedoc\Scramble\Infer\Services;

use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
use Dedoc\Scramble\Support\Type\TypeHelper;
use Dedoc\Scramble\Support\Type\UnknownType;

class ConstFetchTypeGetter
{
public function __invoke(Scope $scope, string $className, string $constName)
{
if ($constName === 'class') {
return new LiteralStringType($className);
}

try {
$constantReflection = new \ReflectionClassConstant($className, $constName);
$constantValue = $constantReflection->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.');
}
}
123 changes: 101 additions & 22 deletions src/Infer/Services/ReferenceTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -197,15 +203,36 @@ 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()
// (#Doctrine\DBAL\Schema\Table).listTableDetails()
// (#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,
);

Expand All @@ -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;
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 ?? [],
Expand All @@ -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;
}
}
Loading

0 comments on commit 9e947b3

Please sign in to comment.