diff --git a/src/Type/Php/ReflectionClassGetConstantsDynamicReturnTypeExtension.php b/src/Type/Php/ReflectionClassGetConstantsDynamicReturnTypeExtension.php new file mode 100644 index 00000000000..c6d1d7321ed --- /dev/null +++ b/src/Type/Php/ReflectionClassGetConstantsDynamicReturnTypeExtension.php @@ -0,0 +1,219 @@ +getName() === 'getConstant' + || $methodReflection->getName() === 'getConstants'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $calledOnType = $scope->getType($methodCall->var); + $reflectionType = $calledOnType->getTemplateType(ReflectionClass::class, 'T'); + + $classReflections = $reflectionType->getObjectClassReflections(); + if (count($classReflections) === 0) { + return null; + } + + if (!$this->isInvariantOrFinalClass($calledOnType, $classReflections)) { + return null; + } + + if ($methodReflection->getName() === 'getConstant') { + return $this->resolveGetConstant($methodCall, $scope, $classReflections); + } + + $filterType = count($methodCall->getArgs()) >= 1 + ? $scope->getType($methodCall->getArgs()[0]->value) + : null; + + return $this->resolveGetConstants($scope, $classReflections, $filterType); + } + + /** + * @param non-empty-list $classReflections + */ + private function isInvariantOrFinalClass(Type $calledOnType, array $classReflections): bool + { + $hasNonFinalClass = false; + foreach ($classReflections as $classReflection) { + if (!$classReflection->isFinal() && !$classReflection->isEnum()) { + $hasNonFinalClass = true; + break; + } + } + + if (!$hasNonFinalClass) { + return true; + } + + foreach ($calledOnType->getObjectClassReflections() as $reflectionClassReflection) { + if ($reflectionClassReflection->getName() !== 'ReflectionClass') { + return false; + } + $variance = $reflectionClassReflection->getCallSiteVarianceMap()->getVariance('T'); + if ($variance !== null && $variance->covariant()) { + return false; + } + } + + return true; + } + + /** + * @param non-empty-list $classReflections + */ + private function resolveGetConstant(MethodCall $methodCall, Scope $scope, array $classReflections): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return null; + } + + $nameType = $scope->getType($methodCall->getArgs()[0]->value); + $constantNames = $nameType->getConstantStrings(); + + if (count($constantNames) > 0) { + $types = []; + foreach ($classReflections as $classReflection) { + foreach ($constantNames as $constantName) { + $name = $constantName->getValue(); + if ($name === '') { + continue; + } + if ($classReflection->hasConstant($name)) { + $types[] = $this->getConstantType($scope, $classReflection, $name); + } else { + $types[] = new ConstantBooleanType(false); + } + } + } + + if (count($types) === 0) { + return null; + } + + return TypeCombinator::union(...$types); + } + + $allConstantTypes = []; + foreach ($classReflections as $classReflection) { + foreach ($this->getConstantNames($classReflection) as $name) { + $allConstantTypes[] = $this->getConstantType($scope, $classReflection, $name); + } + } + + if (count($allConstantTypes) === 0) { + return new ConstantBooleanType(false); + } + + $allConstantTypes[] = new ConstantBooleanType(false); + + return TypeCombinator::union(...$allConstantTypes); + } + + /** + * @param non-empty-string $name + */ + private function getConstantType(Scope $scope, ClassReflection $classReflection, string $name): Type + { + return $scope->getType(new ClassConstFetch( + new FullyQualified($classReflection->getName()), + new Identifier($name), + )); + } + + /** + * @return list + */ + private function getConstantNames(ClassReflection $classReflection, ?int $filter = null): array + { + $names = []; + foreach ($classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) { + if ($filter !== null && ($reflectionConstant->getModifiers() & $filter) === 0) { + continue; + } + + $name = $reflectionConstant->getName(); + if ($name === '') { + continue; + } + + $names[] = $name; + } + + return $names; + } + + /** + * @param non-empty-list $classReflections + */ + private function resolveGetConstants(Scope $scope, array $classReflections, ?Type $filterType): Type + { + if ($filterType === null) { + return $this->buildConstantsArray($scope, $classReflections, null, false); + } + + $filterScalars = $filterType->getConstantScalarValues(); + if (count($filterScalars) === 0) { + return $this->buildConstantsArray($scope, $classReflections, null, true); + } + + $types = []; + foreach ($filterScalars as $filter) { + $types[] = $this->buildConstantsArray($scope, $classReflections, (int) $filter, false); + } + + return TypeCombinator::union(...$types); + } + + /** + * @param non-empty-list $classReflections + */ + private function buildConstantsArray(Scope $scope, array $classReflections, ?int $filter, bool $optional): Type + { + $types = []; + foreach ($classReflections as $classReflection) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($this->getConstantNames($classReflection, $filter) as $name) { + $builder->setOffsetValueType( + new ConstantStringType($name), + $this->getConstantType($scope, $classReflection, $name), + $optional, + ); + } + $types[] = $builder->getArray(); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/reflection-class-get-constants.php b/tests/PHPStan/Analyser/nsrt/reflection-class-get-constants.php new file mode 100644 index 00000000000..def17e05eb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/reflection-class-get-constants.php @@ -0,0 +1,265 @@ += 8.1 + +namespace ReflectionClassGetConstants; + +use ReflectionClass; +use function PHPStan\Testing\assertType; + +class Foo +{ + public const A = 1; + public const B = 'hello'; + protected const C = 3.14; + private const D = true; +} + +class Bar +{ + public const X = 'x'; +} + +final class FinalClass +{ + public const ONE = 1; + public const TWO = 2; +} + +enum SimpleEnum +{ + case Hearts; + case Diamonds; +} + +enum BackedEnum: string +{ + case Active = 'active'; + case Inactive = 'inactive'; +} + +enum MixedEnum: int +{ + const SOME_CONST = 42; + case One = 1; + case Two = 2; +} + +interface HasConstants +{ + public const IFACE_CONST = 'iface'; +} + +class ParentClass +{ + public const PARENT_CONST = 'parent'; +} + +class ChildClass extends ParentClass +{ + public const CHILD_CONST = 'child'; +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantKnown(ReflectionClass $ref): void +{ + assertType('1', $ref->getConstant('A')); + assertType("'hello'", $ref->getConstant('B')); + assertType('3.14', $ref->getConstant('C')); + assertType('true', $ref->getConstant('D')); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantNonExistent(ReflectionClass $ref): void +{ + assertType('false', $ref->getConstant('nonExistent')); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantDynamic(ReflectionClass $ref, string $name): void +{ + assertType("1|3.14|'hello'|bool", $ref->getConstant($name)); +} + +function testGetConstantUnknownClass(ReflectionClass $ref): void +{ + assertType('mixed', $ref->getConstant('A')); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstants(ReflectionClass $ref): void +{ + assertType("array{A: 1, B: 'hello', C: 3.14, D: true}", $ref->getConstants()); +} + +function testGetConstantsUnknownClass(ReflectionClass $ref): void +{ + assertType('array', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsFinalClass(ReflectionClass $ref): void +{ + assertType('array{ONE: 1, TWO: 2}', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsSimple(ReflectionClass $ref): void +{ + assertType("array{X: 'x'}", $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsEnum(ReflectionClass $ref): void +{ + assertType('array{Hearts: ReflectionClassGetConstants\SimpleEnum::Hearts, Diamonds: ReflectionClassGetConstants\SimpleEnum::Diamonds}', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsBackedEnum(ReflectionClass $ref): void +{ + assertType('array{Active: ReflectionClassGetConstants\BackedEnum::Active, Inactive: ReflectionClassGetConstants\BackedEnum::Inactive}', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsEnumWithConst(ReflectionClass $ref): void +{ + assertType('array{SOME_CONST: 42, One: ReflectionClassGetConstants\MixedEnum::One, Two: ReflectionClassGetConstants\MixedEnum::Two}', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantEnumCase(ReflectionClass $ref): void +{ + assertType('ReflectionClassGetConstants\SimpleEnum::Hearts', $ref->getConstant('Hearts')); + assertType('false', $ref->getConstant('nonExistent')); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsInterface(ReflectionClass $ref): void +{ + assertType("array{IFACE_CONST: 'iface'}", $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsInheritance(ReflectionClass $ref): void +{ + assertType("array{CHILD_CONST: 'child', PARENT_CONST: 'parent'}", $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantInherited(ReflectionClass $ref): void +{ + assertType("'parent'", $ref->getConstant('PARENT_CONST')); + assertType("'child'", $ref->getConstant('CHILD_CONST')); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsWithFilter(ReflectionClass $ref): void +{ + assertType("array{A: 1, B: 'hello'}", $ref->getConstants(\ReflectionClassConstant::IS_PUBLIC)); + assertType('array{C: 3.14}', $ref->getConstants(\ReflectionClassConstant::IS_PROTECTED)); + assertType('array{D: true}', $ref->getConstants(\ReflectionClassConstant::IS_PRIVATE)); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsWithDynamicFilter(ReflectionClass $ref, int $filter): void +{ + assertType("array{A?: 1, B?: 'hello', C?: 3.14, D?: true}", $ref->getConstants($filter)); +} + +/** + * @param ReflectionClass $ref + * @param \ReflectionClassConstant::IS_PUBLIC|\ReflectionClassConstant::IS_PROTECTED $filter + */ +function testGetConstantsWithMultipleConstantFilters(ReflectionClass $ref, int $filter): void +{ + assertType("array{A: 1, B: 'hello'}|array{C: 3.14}", $ref->getConstants($filter)); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantCovariant(ReflectionClass $ref): void +{ + assertType('mixed', $ref->getConstant('A')); + assertType('mixed', $ref->getConstant('nonExistent')); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantCovariantDynamic(ReflectionClass $ref, string $name): void +{ + assertType('mixed', $ref->getConstant($name)); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantsCovariant(ReflectionClass $ref): void +{ + assertType('array', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantCovariantFinalClass(ReflectionClass $ref): void +{ + assertType('1', $ref->getConstant('ONE')); + assertType('false', $ref->getConstant('nonExistent')); + assertType('array{ONE: 1, TWO: 2}', $ref->getConstants()); +} + +/** + * @param ReflectionClass $ref + */ +function testGetConstantCovariantEnum(ReflectionClass $ref): void +{ + assertType('ReflectionClassGetConstants\SimpleEnum::Hearts', $ref->getConstant('Hearts')); + assertType('array{Hearts: ReflectionClassGetConstants\SimpleEnum::Hearts, Diamonds: ReflectionClassGetConstants\SimpleEnum::Diamonds}', $ref->getConstants()); +} + +function testGetConstantDirectInstantiation(): void +{ + $ref = new ReflectionClass(Foo::class); + assertType('1', $ref->getConstant('A')); + assertType("'hello'", $ref->getConstant('B')); + assertType('false', $ref->getConstant('nonExistent')); + assertType("array{A: 1, B: 'hello', C: 3.14, D: true}", $ref->getConstants()); +} + +function testGetConstantDirectInstantiationFinalClass(): void +{ + $ref = new ReflectionClass(FinalClass::class); + assertType('1', $ref->getConstant('ONE')); + assertType('array{ONE: 1, TWO: 2}', $ref->getConstants()); +}