diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 975365a341..2cf5a9bbe1 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -92,6 +92,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\OffsetAccessType; +use PHPStan\Type\PropertyOfType; use PHPStan\Type\ResourceType; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; @@ -760,7 +761,15 @@ static function (string $variance): TemplateTypeVariance { } return new ErrorType(); - } elseif ($mainTypeName === 'int-mask-of') { + } elseif ($mainTypeName === 'property-of') { + if (count($genericTypes) === 1) { // property-of + $type = new PropertyOfType($genericTypes[0]); + + return $type->isResolvable() ? $type->resolve() : $type; + } + + return new ErrorType(); + } elseif ($mainTypeName === 'int-mask-of') { if (count($genericTypes) === 1) { // int-mask-of $maskType = $this->expandIntMaskToType($genericTypes[0]); if ($maskType !== null) { diff --git a/src/Type/PropertyOfType.php b/src/Type/PropertyOfType.php new file mode 100644 index 0000000000..17aec6e97a --- /dev/null +++ b/src/Type/PropertyOfType.php @@ -0,0 +1,112 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('property-of<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + + $classReflections = $this->type->getObjectClassReflections(); + $classReflection = $classReflections[0] ?? null; + + if ($classReflection !== null) { + + $propertiesReflection = $classReflection->getNativeReflection()->getProperties(); + + // get the names of the properties + // and build a union type from them + $propertyNames = array_map( + fn($property) => new ConstantStringType($property->getName()), + $propertiesReflection + ); + + return new UnionType($propertyNames); + + } + + return new MixedType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('property-of'), [$this->type->toPhpDocNode()]); + } + +} \ No newline at end of file diff --git a/tests/PHPStan/Analyser/nsrt/property-of.php b/tests/PHPStan/Analyser/nsrt/property-of.php new file mode 100644 index 0000000000..7777495cff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/property-of.php @@ -0,0 +1,38 @@ + $property + */ + public static function fromObject(string $property): void + { + assertType("'age'|'name'", $property); + } + + /** + * @param property-of $property + */ + public static function fromStatic(string $property): void + { + assertType("'age'|'name'", $property); + } + + + /** + * @param property-of $property + */ + public static function fromClass(string $property): void + { + assertType("'age'|'name'", $property); + } + +}