|
58 | 58 | use Traversable; |
59 | 59 | use function array_key_exists; |
60 | 60 | use function array_map; |
| 61 | +use function array_merge; |
61 | 62 | use function array_values; |
62 | 63 | use function count; |
63 | 64 | use function get_class; |
@@ -124,6 +125,14 @@ class ObjectType implements TypeWithClassName, SubtractableType |
124 | 125 |
|
125 | 126 | private ?string $cachedDescription = null; |
126 | 127 |
|
| 128 | + /** |
| 129 | + * The class name as the reflection spells it - the properly cased name, or the |
| 130 | + * readable `class@anonymous...` form. Resolving it takes two ReflectionProvider |
| 131 | + * round-trips and both the type-only and the value level need it, the latter on |
| 132 | + * every operation RecursionGuard guards. |
| 133 | + */ |
| 134 | + private ?string $cachedPreciseName = null; |
| 135 | + |
127 | 136 | /** |
128 | 137 | * The reflection resolved on demand by getClassReflection(), kept apart from the one |
129 | 138 | * handed to the constructor. The constructor's reflection is part of the type's value |
@@ -742,13 +751,21 @@ private function checkSubclassAcceptability(string $thatClass): AcceptsResult |
742 | 751 |
|
743 | 752 | public function describe(VerbosityLevel $level): string |
744 | 753 | { |
| 754 | + if ($this->cachedPreciseName !== null && ($level->isValue() || $level->isTypeOnly())) { |
| 755 | + return $this->cachedPreciseName; |
| 756 | + } |
| 757 | + |
745 | 758 | $preciseNameCallback = function (): string { |
| 759 | + if ($this->cachedPreciseName !== null) { |
| 760 | + return $this->cachedPreciseName; |
| 761 | + } |
| 762 | + |
746 | 763 | $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); |
747 | 764 | if (!$reflectionProvider->hasClass($this->className)) { |
748 | | - return $this->className; |
| 765 | + return $this->cachedPreciseName = $this->className; |
749 | 766 | } |
750 | 767 |
|
751 | | - return $reflectionProvider->getClassName($this->className); |
| 768 | + return $this->cachedPreciseName = $reflectionProvider->getClassName($this->className); |
752 | 769 | }; |
753 | 770 |
|
754 | 771 | $preciseWithSubtracted = fn (): string => $this->className . $this->describeSubtractedType($this->subtractedType, $level); |
@@ -1756,11 +1773,24 @@ public function isInstanceOf(string $className): TrinaryLogic |
1756 | 1773 |
|
1757 | 1774 | public function subtract(Type $type): Type |
1758 | 1775 | { |
1759 | | - if ($this->subtractedType !== null) { |
1760 | | - $type = TypeCombinator::union($this->subtractedType, $type); |
| 1776 | + if ($this->subtractedType === null) { |
| 1777 | + return $this->changeSubtractedType($type); |
| 1778 | + } |
| 1779 | + |
| 1780 | + // A sealed hierarchy rebuilds its subtraction from the flattened parts |
| 1781 | + // below, so normalising the union of the old and the new subtracted type |
| 1782 | + // first is wasted work - and quadratic when a removal peels the subtypes |
| 1783 | + // off one at a time, as removing a whole enum from its own type does. |
| 1784 | + // Only the path that keeps the subtracted type as it is needs the union. |
| 1785 | + $matched = $this->matchAllowedSubTypes(array_merge( |
| 1786 | + TypeUtils::flattenTypes($this->subtractedType), |
| 1787 | + TypeUtils::flattenTypes($type), |
| 1788 | + )); |
| 1789 | + if ($matched !== null) { |
| 1790 | + return $matched; |
1761 | 1791 | } |
1762 | 1792 |
|
1763 | | - return $this->changeSubtractedType($type); |
| 1793 | + return $this->changeSubtractedType(TypeCombinator::union($this->subtractedType, $type)); |
1764 | 1794 | } |
1765 | 1795 |
|
1766 | 1796 | public function getTypeWithoutSubtractedType(): Type |
@@ -1788,55 +1818,70 @@ public function withoutFinalByKeywordOverride(): Type |
1788 | 1818 | public function changeSubtractedType(?Type $subtractedType): Type |
1789 | 1819 | { |
1790 | 1820 | if ($subtractedType !== null) { |
1791 | | - $classReflection = $this->getClassReflection(); |
1792 | | - $allowedSubTypes = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null; |
1793 | | - if ($allowedSubTypes !== null) { |
1794 | | - $preciseVerbosity = VerbosityLevel::precise(); |
1795 | | - |
1796 | | - $originalAllowedSubTypes = $allowedSubTypes; |
1797 | | - $subtractedSubTypes = []; |
1798 | | - |
1799 | | - $subtractedTypes = TypeUtils::flattenTypes($subtractedType); |
1800 | | - foreach ($subtractedTypes as $subType) { |
1801 | | - foreach ($allowedSubTypes as $key => $allowedSubType) { |
1802 | | - if ($subType->equals($allowedSubType)) { |
1803 | | - $description = $allowedSubType->describe($preciseVerbosity); |
1804 | | - $subtractedSubTypes[$description] = $subType; |
1805 | | - unset($allowedSubTypes[$key]); |
1806 | | - continue 2; |
1807 | | - } |
1808 | | - } |
| 1821 | + $matched = $this->matchAllowedSubTypes(TypeUtils::flattenTypes($subtractedType)); |
| 1822 | + if ($matched !== null) { |
| 1823 | + return $matched; |
| 1824 | + } |
| 1825 | + } |
1809 | 1826 |
|
1810 | | - return new self($this->className, $subtractedType); |
1811 | | - } |
| 1827 | + if ($this->subtractedType === null && $subtractedType === null) { |
| 1828 | + return $this; |
| 1829 | + } |
1812 | 1830 |
|
1813 | | - if (count($allowedSubTypes) === 1) { |
1814 | | - return array_values($allowedSubTypes)[0]; |
1815 | | - } |
| 1831 | + return new self($this->className, $subtractedType); |
| 1832 | + } |
1816 | 1833 |
|
1817 | | - $subtractedSubTypes = array_values($subtractedSubTypes); |
1818 | | - $subtractedSubTypesCount = count($subtractedSubTypes); |
1819 | | - if ($subtractedSubTypesCount === count($originalAllowedSubTypes)) { |
1820 | | - return new NeverType(); |
1821 | | - } |
| 1834 | + /** |
| 1835 | + * Rebuilds the type from the subtracted members of its sealed hierarchy. |
| 1836 | + * |
| 1837 | + * Returns null when the class has no allowed subtypes, or when a subtracted |
| 1838 | + * type is not one of them - the caller then keeps the subtracted type whole. |
| 1839 | + * |
| 1840 | + * @param array<Type> $subtractedTypes flattened, may repeat |
| 1841 | + */ |
| 1842 | + private function matchAllowedSubTypes(array $subtractedTypes): ?Type |
| 1843 | + { |
| 1844 | + $classReflection = $this->getClassReflection(); |
| 1845 | + $allowedSubTypes = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null; |
| 1846 | + if ($allowedSubTypes === null) { |
| 1847 | + return null; |
| 1848 | + } |
1822 | 1849 |
|
1823 | | - if ($subtractedSubTypesCount === 0) { |
1824 | | - return new self($this->className); |
1825 | | - } |
| 1850 | + $allowedSubTypesCount = count($allowedSubTypes); |
| 1851 | + $subtractedSubTypes = []; |
1826 | 1852 |
|
1827 | | - if ($subtractedSubTypesCount === 1) { |
1828 | | - return new self($this->className, $subtractedSubTypes[0]); |
| 1853 | + foreach ($subtractedTypes as $subType) { |
| 1854 | + foreach ($allowedSubTypes as $key => $allowedSubType) { |
| 1855 | + if ($subType->equals($allowedSubType)) { |
| 1856 | + // An allowed subtype is dropped as it matches, so no two matches |
| 1857 | + // can be the same one and the matches need no keying. |
| 1858 | + $subtractedSubTypes[] = $subType; |
| 1859 | + unset($allowedSubTypes[$key]); |
| 1860 | + continue 2; |
1829 | 1861 | } |
1830 | | - |
1831 | | - return new self($this->className, new UnionType($subtractedSubTypes)); |
1832 | 1862 | } |
| 1863 | + |
| 1864 | + return null; |
1833 | 1865 | } |
1834 | 1866 |
|
1835 | | - if ($this->subtractedType === null && $subtractedType === null) { |
1836 | | - return $this; |
| 1867 | + if (count($allowedSubTypes) === 1) { |
| 1868 | + return array_values($allowedSubTypes)[0]; |
1837 | 1869 | } |
1838 | 1870 |
|
1839 | | - return new self($this->className, $subtractedType); |
| 1871 | + $subtractedSubTypesCount = count($subtractedSubTypes); |
| 1872 | + if ($subtractedSubTypesCount === $allowedSubTypesCount) { |
| 1873 | + return new NeverType(); |
| 1874 | + } |
| 1875 | + |
| 1876 | + if ($subtractedSubTypesCount === 0) { |
| 1877 | + return new self($this->className); |
| 1878 | + } |
| 1879 | + |
| 1880 | + if ($subtractedSubTypesCount === 1) { |
| 1881 | + return new self($this->className, $subtractedSubTypes[0]); |
| 1882 | + } |
| 1883 | + |
| 1884 | + return new self($this->className, new UnionType($subtractedSubTypes)); |
1840 | 1885 | } |
1841 | 1886 |
|
1842 | 1887 | public function getSubtractedType(): ?Type |
|
0 commit comments