diff --git a/composer.json b/composer.json index 4626f630..5f77e5a7 100644 --- a/composer.json +++ b/composer.json @@ -75,5 +75,12 @@ "pstan": [ "./vendor/bin/phpstan analyse --memory-limit=1G" ] + }, + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } } } diff --git a/extension.neon b/extension.neon new file mode 100644 index 00000000..d6957fe6 --- /dev/null +++ b/extension.neon @@ -0,0 +1,5 @@ +services: + - + class: Saloon\PHPStan\MacroMethodsClassReflectionExtension + tags: + - phpstan.broker.methodsClassReflectionExtension diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 2d8200a1..aabeb1fc 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -19,6 +19,9 @@ parameters: ignoreErrors: - "#^Unsafe usage of new static\\(\\)\\.$#" + excludePaths: + - '*/src/PHPStan/*' + # Rules treatPhpDocTypesAsCertain: false diff --git a/src/PHPStan/Macro.php b/src/PHPStan/Macro.php new file mode 100644 index 00000000..684a1f63 --- /dev/null +++ b/src/PHPStan/Macro.php @@ -0,0 +1,106 @@ +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(); + } +} diff --git a/src/PHPStan/MacroMethodsClassReflectionExtension.php b/src/PHPStan/MacroMethodsClassReflectionExtension.php new file mode 100644 index 00000000..0fea6b59 --- /dev/null +++ b/src/PHPStan/MacroMethodsClassReflectionExtension.php @@ -0,0 +1,147 @@ + */ + private array $methods = []; + + /** @var array> */ + 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]; + } +} diff --git a/src/Traits/Macroable.php b/src/Traits/Macroable.php index bb49bb3d..55f10498 100644 --- a/src/Traits/Macroable.php +++ b/src/Traits/Macroable.php @@ -25,6 +25,8 @@ trait Macroable /** * Create a macro + * + * @param-closure-this static $macro */ public static function macro(string $name, object|callable $macro): void {