diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 277d417c..181f2111 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -308,6 +308,9 @@ + + $reflectionMethod->getReturnType() + DocBlockGenerator::fromArray($value) ParameterGenerator::fromArray($parameter) @@ -346,6 +349,9 @@ + + $reflectionParameter->getType() + $array['name'] $value @@ -373,6 +379,9 @@ + + $reflectionProperty->getType() + DocBlockGenerator::fromArray($value) @@ -491,19 +500,29 @@ - + + allowsNull - getName - getName - getParentClass getTypes getTypes + + $type + + + $atomicType !== 'null' + $atomicType->type !== 'mixed' && $atomicType !== 'null' + + + $type instanceof ReflectionNamedType + - - - $types - + + + getName + getName + getParentClass + @@ -1029,9 +1048,6 @@ - - new TagManager() - testConstructorWithOptions testCreatingTagFromReflection @@ -1048,9 +1064,6 @@ - - new TagManager() - testConstructorWithOptions testCreatingTagFromReflection @@ -1066,9 +1079,6 @@ - - new TagManager() - testConstructorWithOptions testCreatingTagFromReflection @@ -1085,9 +1095,6 @@ - - new TagManager() - testConstructorWithOptions testCreatingTagFromReflection @@ -1105,9 +1112,6 @@ - - new TagManager() - testConstructorWithOptions testCreatingTagFromReflection @@ -1125,9 +1129,6 @@ - - new TagManager() - testConstructorWithOptions testCreatingTagFromReflection @@ -1145,9 +1146,6 @@ - - new TagManager() - testCreatingTagFromReflection testNameIsCorrect @@ -1162,9 +1160,6 @@ - - new TagManager() - testCreatingTagFromReflection testNameIsCorrect @@ -1191,13 +1186,6 @@ null - - - new TagManager() - setVariableName - setVariableName - - setDatatype @@ -1413,13 +1401,6 @@ - - CompositeType::fromString($typeString) - - - CompositeType::fromString($typeString) - fullyQualifiedName - iterable @@ -1466,9 +1447,6 @@ setMethods - - new PrototypeClassFactory() - testAddAndGetPrototype testFallBackToGeneric @@ -1872,12 +1850,6 @@ - - new DocBlockScanner($docComment) - new DocBlockScanner($docComment) - new DocBlockScanner($docComment) - new DocBlockScanner($docComment) - testDocBlockScannerDescriptions testDocBlockScannerParsesTagsWithNoValuesProperly diff --git a/psalm.xml b/psalm.xml index 8c2df273..2501d42c 100644 --- a/psalm.xml +++ b/psalm.xml @@ -29,34 +29,17 @@ + + - - - - - - - - - - - - - - + + - - - - - - - diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index 14b8ed0a..014d2b19 100644 --- a/src/Generator/TypeGenerator.php +++ b/src/Generator/TypeGenerator.php @@ -1,5 +1,7 @@ assertCanBeStandaloneNullable(); + } + } + /** * @internal * * @psalm-pure */ public static function fromReflectionType( - ?ReflectionType $type, - ?ReflectionClass $currentClass + ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null $type, + ?ReflectionClass $currentClass ): ?self { if (null === $type) { return null; } - // Having to go through `fromTypeString` leads to interesting invalid types as "acceptable", but that's neither - // a security issue, nor a major problem, since {@see ReflectionType} should itself produce valid/usable strings - return self::fromTypeString(self::reflectionTypeToString($type, $currentClass)); - } - - /** @psalm-pure */ - private static function reflectionTypeToString(ReflectionType $type, ?ReflectionClass $currentClass): string - { - assert( - $type instanceof ReflectionNamedType - || $type instanceof ReflectionUnionType - || $type instanceof ReflectionIntersectionType - ); + if ($type instanceof ReflectionUnionType) { + return new self( + new UnionType(array_map( + static fn( + ReflectionIntersectionType|ReflectionNamedType $type + ): IntersectionType|AtomicType => $type instanceof ReflectionNamedType + ? AtomicType::fromReflectionNamedTypeAndClass($type, $currentClass) + : self::fromIntersectionType($type, $currentClass), + $type->getTypes() + )), + false + ); + } - if ($type instanceof ReflectionNamedType) { - return self::reflectionNamedTypeToString($type, $currentClass); + if ($type instanceof ReflectionIntersectionType) { + return new self(self::fromIntersectionType($type, $currentClass), false); } + + $atomicType = AtomicType::fromReflectionNamedTypeAndClass($type, $currentClass); - return implode( - $type instanceof ReflectionIntersectionType - ? CompositeType::INTERSECTION_SEPARATOR - : CompositeType::UNION_SEPARATOR, - array_map( - static function (ReflectionType $type) use ($currentClass): string { - $typeString = self::reflectionTypeToString($type, $currentClass); - - return $type instanceof ReflectionIntersectionType - ? sprintf('(%s)', $typeString) - : $typeString; - }, - $type->getTypes() - ) + return new self( + $atomicType, + $atomicType->type !== 'mixed' && $atomicType !== 'null' && $type->allowsNull() ); } /** @psalm-pure */ - private static function reflectionNamedTypeToString( - ReflectionNamedType $type, - ?ReflectionClass $currentClass - ): string { - $lowerCaseName = strtolower($type->getName()); - - if ('mixed' === $lowerCaseName || 'null' === $lowerCaseName) { - // `mixed` and `null` are implicitly nullable, therefore we need to skip adding nullability markers to it - return $lowerCaseName; - } - - $nullabilityMarker = $type->allowsNull() - ? self::NULL_MARKER - : ''; - - if ('self' === $lowerCaseName && $currentClass) { - return $nullabilityMarker . $currentClass->getName(); - } - - if ('parent' === $lowerCaseName && $currentClass && $parentClass = $currentClass->getParentClass()) { - return $nullabilityMarker . $parentClass->getName(); - } - - return $nullabilityMarker . $type->getName(); + private static function fromIntersectionType( + ReflectionIntersectionType $intersectionType, + ?ReflectionClass $currentClass + ): IntersectionType { + return new IntersectionType(array_map( + static fn( + ReflectionNamedType $type + ): AtomicType => AtomicType::fromReflectionNamedTypeAndClass($type, $currentClass), + $intersectionType->getTypes() + )); } /** @@ -120,7 +97,7 @@ public static function fromTypeString(string $type): self ! str_contains($trimmedNullable, CompositeType::INTERSECTION_SEPARATOR) && ! str_contains($trimmedNullable, CompositeType::UNION_SEPARATOR) ) { - return new self(AtomicType::fromString($trimmedNullable), $nullable); + return new self(CompositeType::fromString($trimmedNullable), $nullable); } if ($nullable) { @@ -133,13 +110,6 @@ public static function fromTypeString(string $type): self return new self(CompositeType::fromString($trimmedNullable)); } - private function __construct(private readonly UnionType|IntersectionType|AtomicType $type, private readonly bool $nullable = false) - { - if ($nullable && $type instanceof AtomicType) { - $type->assertCanBeStandaloneNullable(); - } - } - /** * {@inheritDoc} * @@ -160,9 +130,9 @@ public function equals(TypeGenerator $otherType): bool } /** - * @return string the cleaned type string. Please note that this value is not suitable for code generation, - * since the returned value does not include any root namespace prefixes, when applicable, - * and therefore the values cannot be used as FQCN in generated code. + * @return non-empty-string the cleaned type string. Note that this value is not suitable for code generation, + * since the returned value does not include any root namespace prefixes, when applicable, + * and therefore the values cannot be used as FQCN in generated code. */ public function __toString(): string { diff --git a/src/Generator/TypeGenerator/AtomicType.php b/src/Generator/TypeGenerator/AtomicType.php index bc90d9c1..73d4c2aa 100644 --- a/src/Generator/TypeGenerator/AtomicType.php +++ b/src/Generator/TypeGenerator/AtomicType.php @@ -4,6 +4,9 @@ use Laminas\Code\Generator\Exception\InvalidArgumentException; +use ReflectionClass; +use ReflectionNamedType; + use function array_key_exists; use function assert; use function implode; @@ -113,10 +116,26 @@ public static function fromString(string $type): self return new self($trimmedType, 0); } - /** @psalm-pure */ - public static function null(): self - { - return new self('null', self::BUILT_IN_TYPES_PRECEDENCE['null']); + /** + * @psalm-pure + * @throws InvalidArgumentException + */ + public static function fromReflectionNamedTypeAndClass( + ReflectionNamedType $type, + ?ReflectionClass $currentClass + ): self { + $name = $type->getName(); + $lowerCaseName = strtolower($name); + + if ('self' === $lowerCaseName && $currentClass) { + return new self($currentClass->getName(), 0); + } + + if ('parent' === $lowerCaseName && $currentClass && $parentClass = $currentClass->getParentClass()) { + return new self($parentClass->getName(), 0); + } + + return self::fromString($name); } /** @psalm-return non-empty-string */ @@ -173,11 +192,8 @@ public function assertCanUnionWith(self|IntersectionType $other): void } } - /** - * @psalm-param non-empty-array $others - * @throws InvalidArgumentException - */ - public function assertCanIntersectWith(array $others): void + /** @throws InvalidArgumentException */ + public function assertCanIntersectWith(AtomicType $other): void { if (array_key_exists($this->type, self::BUILT_IN_TYPES_PRECEDENCE)) { throw new InvalidArgumentException(sprintf( @@ -186,14 +202,12 @@ public function assertCanIntersectWith(array $others): void )); } - foreach ($others as $other) { - if ($other->type === $this->type) { - throw new InvalidArgumentException(sprintf( - 'Type "%s" cannot be composed in an intersection with the same type "%s"', - $this->type, - $other->type - )); - } + if ($other->type === $this->type) { + throw new InvalidArgumentException(sprintf( + 'Type "%s" cannot be composed in an intersection with the same type "%s"', + $this->type, + $other->type + )); } } diff --git a/src/Generator/TypeGenerator/IntersectionType.php b/src/Generator/TypeGenerator/IntersectionType.php index f625d9f6..52e9094e 100644 --- a/src/Generator/TypeGenerator/IntersectionType.php +++ b/src/Generator/TypeGenerator/IntersectionType.php @@ -37,9 +37,9 @@ public function __construct(array $types) ); foreach ($types as $index => $atomicType) { - $otherTypes = array_diff_key($types, array_flip([$index])); - - $atomicType->assertCanIntersectWith($otherTypes); + foreach (array_diff_key($types, array_flip([$index])) as $otherType) { + $atomicType->assertCanIntersectWith($otherType); + } } $this->types = $types; diff --git a/test/Generator/Cases/BackedCasesTest.php b/test/Generator/Cases/BackedCasesTest.php index 373d07fa..c373cb49 100644 --- a/test/Generator/Cases/BackedCasesTest.php +++ b/test/Generator/Cases/BackedCasesTest.php @@ -15,7 +15,6 @@ public function testProvidingInvalidTypeThrowsException(): void '"bool" is not a valid type for Enums, only "int" and "string" types are allowed.' ); - /** @psalm-suppress InternalMethod, InternalClass */ BackedCases::fromCasesWithType([], 'bool'); } } diff --git a/test/Generator/TypeGenerator/IntersectionTypeTest.php b/test/Generator/TypeGenerator/IntersectionTypeTest.php index 34cd8aa1..598d39d3 100644 --- a/test/Generator/TypeGenerator/IntersectionTypeTest.php +++ b/test/Generator/TypeGenerator/IntersectionTypeTest.php @@ -75,16 +75,20 @@ public function testWillRejectInvalidIntersections(array $types): void public static function invalidIntersectionsExamples(): array { return [ - [ - 'same type makes no sense' => [ + 'same type makes no sense' => [ + [ AtomicType::fromString('A'), AtomicType::fromString('A'), ], - 'same type makes no sense, even with different namespace qualifier' => [ + ], + 'same type makes no sense, even with different namespace qualifier' => [ + [ AtomicType::fromString('A'), AtomicType::fromString('\A'), ], - 'duplicate type in long chain of types' => [ + ], + 'duplicate type in long chain of types' => [ + [ AtomicType::fromString('A'), AtomicType::fromString('B'), AtomicType::fromString('C'), @@ -92,7 +96,9 @@ public static function invalidIntersectionsExamples(): array AtomicType::fromString('A'), AtomicType::fromString('E'), ], - 'native types cannot intersect with other types' => [ + ], + 'native types cannot intersect with other types' => [ + [ AtomicType::fromString('A'), AtomicType::fromString('bool'), ], diff --git a/test/Generator/TypeGenerator/UnionTypeTest.php b/test/Generator/TypeGenerator/UnionTypeTest.php index 3dfd1870..a54bf29a 100644 --- a/test/Generator/TypeGenerator/UnionTypeTest.php +++ b/test/Generator/TypeGenerator/UnionTypeTest.php @@ -16,8 +16,8 @@ class UnionTypeTest extends TestCase /** * @dataProvider sortingExamples * - * @param AtomicType|IntersectionType $types - * @param non-empty-string $expected + * @param non-empty-list $types + * @param non-empty-string $expected */ public function testTypeSorting(array $types, string $expected): void { @@ -108,7 +108,7 @@ public function testWillRejectInvalidUnions(array $types): void new UnionType($types); } - /** @return non-empty-array}> */ + /** @return non-empty-array}> */ public static function invalidUnionsExamples(): array { return [