Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,12 @@
"pstan": [
"./vendor/bin/phpstan analyse --memory-limit=1G"
]
},
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
}
}
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
-
class: Saloon\PHPStan\MacroMethodsClassReflectionExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
3 changes: 3 additions & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ parameters:
ignoreErrors:
- "#^Unsafe usage of new static\\(\\)\\.$#"

excludePaths:
- '*/src/PHPStan/*'

# Rules

treatPhpDocTypesAsCertain: false
106 changes: 106 additions & 0 deletions src/PHPStan/Macro.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace Saloon\PHPStan;

use PHPStan\TrinaryLogic;
use PHPStan\Type\ClosureType;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Reflection\ClassMemberReflection;

final class Macro implements MethodReflection
{
/**
* The is static.
*/
private bool $isStatic = false;

public function __construct(private ClassReflection $classReflection, private string $methodName, private ClosureType $closureType)
{
}

public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}

public function isPrivate(): bool
{
return false;
}

public function isPublic(): bool
{
return true;
}

public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function isStatic(): bool
{
return $this->isStatic;
}

/**
* Set the is static value.
*/
public function setIsStatic(bool $isStatic): void
{
$this->isStatic = $isStatic;
}

public function getDocComment(): string|null
{
return null;
}

public function getName(): string
{
return $this->methodName;
}

public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function getPrototype(): ClassMemberReflection
{
return $this;
}

/** @inheritDoc */
public function getVariants(): array
{
return [
new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->closureType->getParameters(), $this->closureType->isVariadic(), $this->closureType->getReturnType()),
];
}

public function getDeprecatedDescription(): string|null
{
return null;
}

public function getThrowType(): null
{
return null;
}

public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}
147 changes: 147 additions & 0 deletions src/PHPStan/MacroMethodsClassReflectionExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

declare(strict_types=1);

namespace Saloon\PHPStan;

use Closure;
use function explode;
use function in_array;
use function is_array;
use function get_class;
use function is_string;
use function array_keys;
use ReflectionException;
use function is_callable;
use function str_contains;
use Saloon\Traits\Macroable;
use function array_key_exists;
use PHPStan\Type\ClosureTypeFactory;
use PHPStan\ShouldNotHappenException;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;

/**
* Many thanks to Larastan for building this excellent extension.
*
* @see https://github.com/larastan/larastan/blob/3.x/src/Methods/MacroMethodsClassReflectionExtension.php
*/
class MacroMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
/** @var array<string, MethodReflection> */
private array $methods = [];

/** @var array<string, array<string, bool>> */
private array $traitCache = [];

public function __construct(private ReflectionProvider $reflectionProvider, private ClosureTypeFactory $closureTypeFactory)
{
}

/**
* @throws ReflectionException
* @throws ShouldNotHappenException
* @throws MissingMethodFromReflectionException
*/
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
/** @var class-string[] $classNames */
$classNames = [];
$found = false;
$macroTraitProperty = null;

if ($this->hasIndirectTraitUse($classReflection, Macroable::class)) {
$classNames = [$classReflection->getName()];
$macroTraitProperty = 'macros';
}

if ($classNames !== [] && $macroTraitProperty) {
foreach ($classNames as $className) {
$macroClassReflection = $this->reflectionProvider->getClass($className);

if (! $macroClassReflection->getNativeReflection()->hasProperty($macroTraitProperty)) {
continue;
}

$refProperty = $macroClassReflection->getNativeReflection()->getProperty($macroTraitProperty);

$found = array_key_exists($methodName, $refProperty->getValue());

if (! $found) {
continue;
}

$macroDefinition = $refProperty->getValue()[$methodName];

if (is_string($macroDefinition)) {
if (str_contains($macroDefinition, '::')) {
$macroDefinition = explode('::', $macroDefinition, 2);
$macroClassName = $macroDefinition[0];
if (! $this->reflectionProvider->hasClass($macroClassName) || ! $this->reflectionProvider->getClass($macroClassName)->hasNativeMethod($macroDefinition[1])) {
throw new ShouldNotHappenException('Class ' . $macroClassName . ' does not exist');
}

$methodReflection = $this->reflectionProvider->getClass($macroClassName)->getNativeMethod($macroDefinition[1]);
} elseif (is_callable($macroDefinition)) {
$methodReflection = new Macro(
$macroClassReflection,
$methodName,
$this->closureTypeFactory->fromClosureObject(Closure::fromCallable($macroDefinition)),
);
} else {
throw new ShouldNotHappenException('Function ' . $macroDefinition . ' does not exist');
}
} elseif (is_array($macroDefinition)) {
if (is_string($macroDefinition[0])) {
$macroClassName = $macroDefinition[0];
} else {
$macroClassName = get_class($macroDefinition[0]);
}

if ($macroClassName === false || ! $this->reflectionProvider->hasClass($macroClassName) || ! $this->reflectionProvider->getClass($macroClassName)->hasNativeMethod($macroDefinition[1])) {
throw new ShouldNotHappenException('Class ' . $macroClassName . ' does not exist');
}

$methodReflection = $this->reflectionProvider->getClass($macroClassName)->getNativeMethod($macroDefinition[1]);
} else {
$methodReflection = new Macro(
$macroClassReflection,
$methodName,
$this->closureTypeFactory->fromClosureObject($macroDefinition),
);

$methodReflection->setIsStatic(true);
}

$this->methods[$classReflection->getName() . '-' . $methodName] = $methodReflection;

break;
}
}

return $found;
}

public function getMethod(
ClassReflection $classReflection,
string $methodName,
): MethodReflection {
return $this->methods[$classReflection->getName() . '-' . $methodName];
}

private function hasIndirectTraitUse(ClassReflection $class, string $traitName): bool
{
$className = $class->getName();

if (array_key_exists($className, $this->traitCache) && array_key_exists($traitName, $this->traitCache[$className])) {
return $this->traitCache[$className][$traitName];
}

$this->traitCache[$className][$traitName] = in_array($traitName, array_keys($class->getTraits(true)), true);

return $this->traitCache[$className][$traitName];
}
}
2 changes: 2 additions & 0 deletions src/Traits/Macroable.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ trait Macroable

/**
* Create a macro
*
* @param-closure-this static $macro
*/
public static function macro(string $name, object|callable $macro): void
{
Expand Down