Skip to content

Instantly share code, notes, and snippets.

@voku
Created December 13, 2020 00:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save voku/80722c9310c21719fbcfbfeb565cc6f9 to your computer and use it in GitHub Desktop.
Save voku/80722c9310c21719fbcfbfeb565cc6f9 to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
namespace vdmg\App\scripts\githooks\StandardVdmg\PHPStanHelper;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension;
use PHPStan\Type\ArrayType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
final class PropertyArrayDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension {
/**
* @var \PHPStan\Reflection\ReflectionProvider
*/
private $reflectionProvider;
/**
* @var string[]
*/
private $universalObjectCratesClasses;
/**
* @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider
* @param string[] $universalObjectCratesClasses
*/
public function __construct(
\PHPStan\Reflection\ReflectionProvider $reflectionProvider,
array $universalObjectCratesClasses
) {
$this->reflectionProvider = $reflectionProvider;
$this->universalObjectCratesClasses = $universalObjectCratesClasses;
}
/**
* @param \PHPStan\Reflection\FunctionReflection $functionReflection
*
* @return bool
*/
public function isFunctionSupported(FunctionReflection $functionReflection): bool {
return \strtolower($functionReflection->getName()) === 'propertyarray';
}
/**
* @param \PHPStan\Reflection\FunctionReflection $functionReflection
* @param \PhpParser\Node\Expr\FuncCall $functionCall
* @param \PHPStan\Analyser\Scope $scope
*
* @return \PHPStan\Type\Type
*/
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
if (count($functionCall->args) <= 1) {
/** @noinspection PhpParamsInspection */
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}
$objectArrayArg = $functionCall->args[0]->value ?? null;
$propertyResultArrayValueArg = $functionCall->args[1]->value ?? null;
$propertyResultArrayKeyArg = $functionCall->args[2]->value ?? null;
if ($objectArrayArg !== null && $propertyResultArrayValueArg !== null) {
$objectArrayArgType = $scope->getType($objectArrayArg);
$objectArrayKeyType = $objectArrayArgType->getIterableKeyType();
$objectArrayType = $objectArrayArgType->getIterableValueType();
$classNames = $objectArrayType->getReferencedClasses();
$classPropertyTypes = $this->getClassProperties($classNames);
$propertyResultArrayValues = $this->getPropertiesTypes(
$classNames,
$scope,
$propertyResultArrayValueArg,
$classPropertyTypes
);
if ($propertyResultArrayKeyArg !== null) {
$propertyResultArrayKeys = $this->getPropertiesTypes(
$classNames,
$scope,
$propertyResultArrayKeyArg,
$classPropertyTypes
);
$returnTypes = [];
foreach ($propertyResultArrayValues as $propertyResultArrayValue) {
foreach ($propertyResultArrayKeys as $propertyResultArrayKey) {
$returnTypes[] = new ArrayType($propertyResultArrayKey, $propertyResultArrayValue);
}
}
return TypeCombinator::union(...$returnTypes);
}
$returnTypes = [];
foreach ($propertyResultArrayValues as $propertyResultArrayValue) {
$returnTypes[] = new ArrayType($objectArrayKeyType, $propertyResultArrayValue);
}
return TypeCombinator::union(...$returnTypes);
}
// fallback
/** @noinspection PhpParamsInspection */
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}
/**
* @param string[] $classNames
* @param \PHPStan\Analyser\Scope $scope
* @param \PhpParser\Node\Expr $propertyResultArrayValueArg
* @param \PHPStan\Type\Type[][] $classPropertyTypes
*
* @return \PHPStan\Type\Type[]
* @phpstan-param array<class-string, array<string, \PHPStan\Type\Type>> $classPropertyTypes
*/
private function getPropertiesTypes(
array $classNames,
Scope $scope,
\PhpParser\Node\Expr $propertyResultArrayValueArg,
array $classPropertyTypes
): array {
// init
$propertyResultArrayValues = [];
foreach ($classNames as $className) {
$propertyResultArrayValueNameArgType = $scope->getType($propertyResultArrayValueArg);
$propertyResultArrayValueNames = \PHPStan\Type\TypeUtils::getConstantStrings($propertyResultArrayValueNameArgType);
foreach ($propertyResultArrayValueNames as $propertyResultArrayValueName) {
$propertyResultArrayValueName = $propertyResultArrayValueName->getValue();
if (isset($classPropertyTypes[$className][$propertyResultArrayValueName])) {
$propertyResultArrayValues[] = $classPropertyTypes[$className][$propertyResultArrayValueName];
} else {
$propertyResultArrayValues[] = new NullType();
}
}
}
return $propertyResultArrayValues;
}
/**
* @param string[] $classNames
*
* @return array
* @phpstan-return array<class-string, array<string, \PHPStan\Type\Type>>
*/
private function getClassProperties(array $classNames): array {
// init
$classPropertyTypes = [];
foreach ($classNames as $className) {
$classReflection = $this->reflectionProvider->getClass($className);
/** @noinspection NotOptimalIfConditionsInspection */
if (
!$classReflection->getNativeReflection()->isUserDefined()
||
UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate(
$this->reflectionProvider,
$this->universalObjectCratesClasses,
$classReflection
)
) {
continue;
}
do {
foreach ($classReflection->getNativeReflection()->getProperties() as $nativeProperty) {
$declaringClass = $this->reflectionProvider->getClass($nativeProperty->getDeclaringClass()->getName());
$declaringClassProperty = $declaringClass->getNativeProperty($nativeProperty->getName());
$declaringClassPropertyName = $nativeProperty->getName();
if ($nativeProperty->isPrivate()) {
$declaringClassPropertyName = sprintf(
"\0%s\0%s",
$declaringClass->getName(),
$declaringClassPropertyName
);
} elseif ($nativeProperty->isProtected()) {
$declaringClassPropertyName = sprintf(
"\0*\0%s",
$declaringClassPropertyName
);
}
$classPropertyTypes[$className][$declaringClassPropertyName] = $declaringClassProperty->getReadableType();
}
$classReflection = $classReflection->getParentClass();
} while ($classReflection !== false);
}
return $classPropertyTypes;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment