Skip to content

Commit

Permalink
Merge branch 'main' into feature/shallow-analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
romalytvynenko committed Dec 15, 2024
2 parents 3dac064 + e51e93f commit 4ca243c
Show file tree
Hide file tree
Showing 38 changed files with 753 additions and 126 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
os: [ubuntu-latest]
php: [8.1, 8.2]
laravel: [11.*, 10.*]
phpdoc-parser: [1.*, 2.*]
stability: [prefer-lowest, prefer-stable]
include:
- laravel: 10.*
Expand All @@ -25,7 +26,7 @@ jobs:
- laravel: 11.*
php: 8.1

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
name: P${{ matrix.php }} - L${{ matrix.laravel }} - PHPDoc-Parser${{ matrix.phpdoc-parser }} - ${{ matrix.stability }} - ${{ matrix.os }}

steps:
- name: Checkout code
Expand All @@ -46,7 +47,7 @@ jobs:
- name: Install dependencies
run: |
composer config --no-plugins allow-plugins.pestphp/pest-plugin true
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.phpdoc-parser }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"illuminate/contracts": "^10.0|^11.0",
"myclabs/deep-copy": "^1.12",
"nikic/php-parser": "^5.0",
"phpstan/phpdoc-parser": "^1.33.0",
"phpstan/phpdoc-parser": "^1.0|^2.0",
"spatie/laravel-package-tools": "^1.9.2"
},
"require-dev": {
Expand Down
1 change: 1 addition & 0 deletions dictionaries/classMap.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* Do not change! This file is generated via scripts/generate.php.
*/
Expand Down
1 change: 1 addition & 0 deletions src/Infer/Analyzer/ClassAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public function analyze(string $name): ClassDefinition
returnType: new UnknownType,
),
definingClassName: $name,
isStatic: $reflectionMethod->isStatic(),
);
}

Expand Down
30 changes: 30 additions & 0 deletions src/Infer/Definition/ClassDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Dedoc\Scramble\Infer\Analyzer\MethodAnalyzer;
use Dedoc\Scramble\Infer\Reflector\ClassReflector;
use Dedoc\Scramble\Infer\Scope\GlobalScope;
use Dedoc\Scramble\Infer\Scope\Index;
use Dedoc\Scramble\Infer\Scope\NodeTypesResolver;
use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Infer\Scope\ScopeContext;
Expand Down Expand Up @@ -48,6 +49,35 @@ public function hasMethodDefinition(string $name): bool
return array_key_exists($name, $this->methods);
}

public function getMethodDefinitionWithoutAnalysis(string $name)
{
if (! array_key_exists($name, $this->methods)) {
return null;
}

return $this->methods[$name];
}

public function getMethodDefiningClassName(string $name, Index $index)
{
$lastLookedUpClassName = $this->name;
while ($lastLookedUpClassDefinition = $index->getClassDefinition($lastLookedUpClassName)) {
if ($methodDefinition = $lastLookedUpClassDefinition->getMethodDefinitionWithoutAnalysis($name)) {
return $methodDefinition->definingClassName;
}

if ($lastLookedUpClassDefinition->parentFqn) {
$lastLookedUpClassName = $lastLookedUpClassDefinition->parentFqn;

continue;
}

break;
}

return $lastLookedUpClassName;
}

public function getMethodDefinition(string $name, Scope $scope = new GlobalScope, array $indexBuilders = [])
{
if (! array_key_exists($name, $this->methods)) {
Expand Down
1 change: 1 addition & 0 deletions src/Infer/Definition/FunctionLikeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(
public array $sideEffects = [],
public array $argumentsDefaults = [],
public ?string $definingClassName = null,
public bool $isStatic = false,
) {}

public function isFullyAnalyzed(): bool
Expand Down
1 change: 1 addition & 0 deletions src/Infer/Extensions/Event/MethodCallEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function __construct(
public readonly string $name,
public readonly Scope $scope,
public readonly array $arguments,
public readonly ?string $methodDefiningClassName,
) {}

public function getDefinition()
Expand Down
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;
}
1 change: 1 addition & 0 deletions src/Infer/Handler/FunctionLikeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function enter(FunctionLike $node, Scope $scope)
$scope->context->setFunctionDefinition($fnDefinition = new FunctionLikeDefinition(
type: $fnType = new FunctionType($node->name->name ?? 'anonymous'),
sideEffects: [],
isStatic: $node instanceof Node\Stmt\ClassMethod ? $node->isStatic() : false,
));
$fnDefinition->isFullyAnalyzed = true;

Expand Down
5 changes: 4 additions & 1 deletion src/Infer/Reflector/MethodReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public function getMethodCode(): string

public function getReflection(): ReflectionMethod
{
return new ReflectionMethod($this->className, $this->name);
/**
* \ReflectionMethod could've been used here, but for `\Closure::__invoke` it fails when constructed manually
*/
return (new \ReflectionClass($this->className))->getMethod($this->name);
}

/**
Expand Down
31 changes: 22 additions & 9 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 Down Expand Up @@ -64,11 +66,7 @@ public function getType(Node $node): Type
}

if ($node instanceof Node\Expr\Match_) {
return Union::wrap(
collect($node->arms)
->map(fn (Node\MatchArm $arm) => $this->getType($arm->body))
->toArray()
);
return Union::wrap(array_map(fn (Node\MatchArm $arm) => $this->getType($arm->body), $node->arms));
}

if ($node instanceof Node\Expr\ClassConstFetch) {
Expand Down Expand Up @@ -115,7 +113,17 @@ 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), $calleeType->name)
: null;

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

if (
$calleeType instanceof TemplateType
&& ! $exceptions
) {
// @todo
// if ($calleeType->is instanceof ObjectType) {
// $calleeType = $calleeType->is;
Expand All @@ -129,12 +137,17 @@ public function getType(Node $node): Type
* 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'
) {
if ($this->functionDefinition()?->type->name === '__construct') {
$this->functionDefinition()->sideEffects[] = $referenceType;
}

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

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

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
45 changes: 41 additions & 4 deletions src/Infer/Services/ReferenceTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,14 @@ private function resolveMethodCallReferenceType(Scope $scope, MethodCallReferenc
: $calleeType;

if ($unwrappedType instanceof ObjectType) {
$classDefinition = $this->index->getClassDefinition($unwrappedType->name);

$event = new MethodCallEvent(
instance: $unwrappedType,
name: $type->methodName,
scope: $scope,
arguments: $type->arguments,
methodDefiningClassName: $classDefinition ? $classDefinition->getMethodDefiningClassName($type->methodName, $scope->index) : $unwrappedType->name,
);
}

Expand Down Expand Up @@ -314,12 +317,16 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod
$type->arguments,
);

$calleeName = $type->callee;
$contextualClassName = $this->resolveClassName($scope, $type->callee);
if (! $contextualClassName) {
return new UnknownType;
}
$type->callee = $contextualClassName;

$isStaticCall = ! in_array($calleeName, StaticReference::KEYWORDS)
|| (in_array($calleeName, StaticReference::KEYWORDS) && $scope->context->functionDefinition?->isStatic);

// Assuming callee here can be only string of known name. Reality is more complex than
// that, but it is fine for now.

Expand All @@ -329,7 +336,7 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod
$this->resolveUnknownClass($type->callee);

// Attempting extensions broker before potentially giving up on type inference
if ($returnType = Context::getInstance()->extensionsBroker->getStaticMethodReturnType(new StaticMethodCallEvent(
if ($isStaticCall && $returnType = Context::getInstance()->extensionsBroker->getStaticMethodReturnType(new StaticMethodCallEvent(
callee: $type->callee,
name: $type->methodName,
scope: $scope,
Expand All @@ -338,6 +345,25 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod
return $returnType;
}

// Attempting extensions broker before potentially giving up on type inference
if (! $isStaticCall && $scope->context->classDefinition) {
$definingMethodName = ($definingClass = $scope->index->getClassDefinition($contextualClassName))
? $definingClass->getMethodDefiningClassName($type->methodName, $scope->index)
: $contextualClassName;

$returnType = Context::getInstance()->extensionsBroker->getMethodReturnType($e = new MethodCallEvent(
instance: $i = new ObjectType($scope->context->classDefinition->name),
name: $type->methodName,
scope: $scope,
arguments: $type->arguments,
methodDefiningClassName: $definingMethodName,
));

if ($returnType) {
return $returnType;
}
}

if (! array_key_exists($type->callee, $this->index->classesDefinitions)) {
return new UnknownType;
}
Expand Down Expand Up @@ -372,7 +398,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 +411,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 +421,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 Expand Up @@ -737,6 +773,7 @@ private function getMethodCallsSideEffectIntroducedTypesInConstructor(Generic $t
name: $se->methodName,
scope: $scope,
arguments: $se->arguments,
methodDefiningClassName: $type->name,
));
}

Expand Down
5 changes: 3 additions & 2 deletions src/PhpDoc/AbstractPhpDocTypeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

namespace Dedoc\Scramble\PhpDoc;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;

class AbstractPhpDocTypeVisitor implements PhpDocTypeVisitor
{
public function enter(TypeNode $type): void {}
public function enter(TypeNode|Node $type): void {}

public function leave(TypeNode $type): void {}
public function leave(TypeNode|Node $type): void {}
}
3 changes: 2 additions & 1 deletion src/PhpDoc/ResolveFqnPhpDocTypeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Dedoc\Scramble\PhpDoc;

use Dedoc\Scramble\Infer\Services\FileNameResolver;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;

Expand All @@ -15,7 +16,7 @@ public function __construct(FileNameResolver $nameResolver)
$this->nameResolver = $nameResolver;
}

public function enter(TypeNode $type): void
public function enter(TypeNode|Node $type): void
{
if ($type instanceof IdentifierTypeNode) {
$type->name = ($this->nameResolver)($type->name);
Expand Down
Loading

0 comments on commit 4ca243c

Please sign in to comment.