From 5d856f1a3b714e602ce11ea0b8cd4324854f4e85 Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Mon, 3 Nov 2025 14:07:24 +0300 Subject: [PATCH] Refactor Stringify to a composable final class --- infection.json5.dist | 6 +- src/Type/Internal/Reference.php | 27 +++ src/Type/Visitor/Stringify.php | 302 ++++++++++++++++---------------- src/Type/stringify.php | 12 +- tests/Type/StringifyTest.php | 64 +++++-- 5 files changed, 230 insertions(+), 181 deletions(-) create mode 100644 src/Type/Internal/Reference.php diff --git a/infection.json5.dist b/infection.json5.dist index fa6f508..40f3f2c 100644 --- a/infection.json5.dist +++ b/infection.json5.dist @@ -15,6 +15,10 @@ "minCoveredMsi": 100, "mutators": { "@default": true, - "ProtectedVisibility": false + "PublicVisibility": { + "ignore": [ + "Typhoon\\Type\\Visitor\\Stringify" + ] + } }, } diff --git a/src/Type/Internal/Reference.php b/src/Type/Internal/Reference.php new file mode 100644 index 0000000..cb3eaaf --- /dev/null +++ b/src/Type/Internal/Reference.php @@ -0,0 +1,27 @@ +object; + } +} diff --git a/src/Type/Visitor/Stringify.php b/src/Type/Visitor/Stringify.php index 59a84c1..dcbea65 100644 --- a/src/Type/Visitor/Stringify.php +++ b/src/Type/Visitor/Stringify.php @@ -24,6 +24,7 @@ use Typhoon\Type\FloatRangeT; use Typhoon\Type\FloatT; use Typhoon\Type\FloatValueT; +use Typhoon\Type\Internal\Reference; use Typhoon\Type\IntersectionT; use Typhoon\Type\IntRangeT; use Typhoon\Type\IntT; @@ -75,8 +76,44 @@ * @api * @implements Visitor */ -abstract class Stringify implements Visitor +final readonly class Stringify implements Visitor { + /** + * @var Reference>|\WeakReference> + */ + private Reference|\WeakReference $next; + + /** + * @param null|Visitor|\WeakReference> $next + */ + public function __construct(null|Visitor|\WeakReference $next = null) + { + $this->next = $next instanceof \WeakReference ? $next : new Reference($next); + } + + /** + * @return non-empty-string + */ + public function safe(Type $type): string + { + return $type->accept($this->next->get() ?? $this); + } + + /** + * @return non-empty-string + */ + public function unsafe(Type $type): string + { + $string = $this->safe($type); + + if ($string[0] === '(') { + /** @phpstan-ignore return.type */ + return substr($string, 1, -1); + } + + return $string; + } + #[\Override] public function neverT(NeverT $type): string { @@ -168,7 +205,7 @@ public function positiveIntT(PositiveIntT $type): string #[\Override] public function bitmaskT(BitmaskT $type): string { - return \sprintf('int-mask-of<%s>', $this->stringifyUnwrap($type->intType)); + return \sprintf('int-mask-of<%s>', $this->unsafe($type->intType)); } #[\Override] @@ -241,7 +278,7 @@ public function stringValueT(StringValueT $type): string #[\Override] public function classT(ClassT $type): string { - return \sprintf('class-string<%s>', $this->stringifyUnwrap($type->objectType)); + return \sprintf('class-string<%s>', $this->unsafe($type->objectType)); } #[\Override] @@ -265,9 +302,9 @@ public function scalarT(ScalarT $type): string #[\Override] public function listT(ListT $type): string { - $value = $this->stringifyUnwrap($type->valueType); + $value = $this->unsafe($type->valueType); - $elements = implode(', ', array_map($this->stringifyUnwrap(...), $type->elementTypes)); + $elements = implode(', ', array_map($this->unsafe(...), $type->elementTypes)); if ($value === 'never') { return \sprintf('list{%s}', $elements); @@ -275,13 +312,13 @@ public function listT(ListT $type): string $name = $type->isNonEmpty ? 'non-empty-list' : 'list'; - $unsealed = $value === 'mixed' ? '' : \sprintf('<%s>', $value); + $unsealed = \sprintf('<%s>', $value); if ($elements === '') { return $name . $unsealed; } - return \sprintf('%s{%s, ...%s}', $name, $elements, $unsealed); + return \sprintf('%s{%s, ...%s}', $name, $elements, $unsealed === '' ? '' : $unsealed); } #[\Override] @@ -293,7 +330,7 @@ public function arrayBareT(ArrayBareT $type): string #[\Override] public function arrayT(ArrayT $type): string { - $value = $this->stringifyUnwrap($type->valueType); + $value = $this->unsafe($type->valueType); $elements = implode(', ', array_map($this->arrayElement(...), $type->elements)); @@ -303,10 +340,10 @@ public function arrayT(ArrayT $type): string $name = $type->isNonEmpty ? 'non-empty-array' : 'array'; - $key = $this->stringifyUnwrap($type->keyType); + $key = $this->unsafe($type->keyType); $unsealed = match ($key) { - 'array-key', 'int|string', 'string|int' => $value === 'mixed' ? '' : \sprintf('<%s>', $value), + 'array-key', 'int|string', 'string|int' => \sprintf('<%s>', $value), default => \sprintf('<%s, %s>', $key, $value), }; @@ -314,20 +351,7 @@ public function arrayT(ArrayT $type): string return $name . $unsealed; } - return \sprintf('%s{%s, ...%s}', $name, $elements, $unsealed); - } - - /** - * @return non-empty-string - */ - protected function arrayElement(ArrayElement $element): string - { - return \sprintf( - '%s%s: %s', - \is_int($element->key) ? $element->key : $this->stringValueT(new StringValueT($element->key)), - $element->isOptional ? '?' : '', - $this->stringifyUnwrap($element->type), - ); + return \sprintf('%s{%s, ...%s}', $name, $elements, $unsealed === '' ? '' : $unsealed); } #[\Override] @@ -339,7 +363,7 @@ public function objectBareT(ObjectBareT $type): string #[\Override] public function namedObjectT(NamedObjectT $type): string { - return $this->constructor($type->class, $type->templateArguments); + return $type->class . $this->templateArguments($type->templateArguments); } #[\Override] @@ -352,53 +376,26 @@ public function objectT(ObjectT $type): string fn(NamedObjectT $inherited): string => ':' . $this->namedObjectT($inherited), $type->supertypes, )), - $type->properties === [] ? '' : \sprintf('{%s}', implode(', ', array_map($this->property(...), $type->properties))), - ); - } - - /** - * @return non-empty-string - */ - protected function property(Property $property): string - { - return \sprintf( - '%s%s: %s', - $property->name, - $property->isOptional ? '?' : '', - $this->stringifyUnwrap($property->type), + \sprintf('{%s}', implode(', ', array_map($this->property(...), $type->properties))), ); } #[\Override] public function selfT(SelfT $type): string { - return $this->constructor('self', $type->templateArguments); + return 'self' . $this->templateArguments($type->templateArguments); } #[\Override] public function parentT(ParentT $type): string { - return $this->constructor('parent', $type->templateArguments); + return 'parent' . $this->templateArguments($type->templateArguments); } #[\Override] public function staticT(StaticT $type): string { - return $this->constructor('static', $type->templateArguments); - } - - /** - * @param non-empty-string $name - * @param list $templateArguments - * @return non-empty-string - */ - protected function constructor(string $name, array $templateArguments): string - { - if ($templateArguments === []) { - return $name; - } - - return \sprintf('%s<%s>', $name, implode(', ', array_map($this->stringifyUnwrap(...), $templateArguments))); + return 'static' . $this->templateArguments($type->templateArguments); } #[\Override] @@ -410,14 +407,10 @@ public function iterableBareT(IterableBareT $type): string #[\Override] public function iterableT(IterableT $type): string { - $key = $this->stringifyUnwrap($type->keyType); - $value = $this->stringifyUnwrap($type->valueType); + $key = $this->unsafe($type->keyType); + $value = $this->unsafe($type->valueType); if ($key === 'mixed') { - if ($value === 'mixed') { - return 'iterable'; - } - return \sprintf('iterable<%s>', $value); } @@ -431,73 +424,21 @@ public function callableBareT(CallableBareT $type): string } #[\Override] - public function callableT(CallableT $type): string + public function callableT(CallableT|ClosureT $type): string { - return $this->callables($type); - } - - #[\Override] - public function closureT(ClosureT $type): string - { - return $this->callables($type); - } - - /** - * @return non-empty-string - */ - protected function callables(CallableT|ClosureT $type): string - { - $prefix = $type instanceof CallableT ? 'callable' : 'Closure'; - - $string = \sprintf( + return \sprintf( '(%s%s(%s): %s)', - $prefix, + $type instanceof CallableT ? 'callable' : 'Closure', $this->templates($type->templates), implode(', ', array_map($this->parameter(...), $type->parameters)), - $this->stringify($type->returnType), + $this->safe($type->returnType), ); - - if ($string === "({$prefix}(): mixed)") { - return $prefix; - } - - return $string; } - /** - * @return non-empty-string - */ - protected function parameter(Parameter $parameter): string + #[\Override] + public function closureT(ClosureT $type): string { - $string = $this->stringify($parameter->type); - - if ($parameter->name !== null) { - $string .= ' '; - } - - if ($parameter->isPassedByReference) { - $string .= '&'; - - // todo $parameter->outType - } - - if ($parameter->isVariadic) { - $string .= '...'; - } - - if ($parameter->name !== null) { - $string .= '$' . $parameter->name; - } - - if ($parameter->hasDefault) { - $string .= '='; - - if ($parameter->defaultType !== null) { - $string .= $this->stringify($parameter->defaultType); - } - } - - return $string; + return $this->callableT($type); } #[\Override] @@ -509,13 +450,13 @@ public function resourceT(ResourceT $type): string #[\Override] public function intersectionT(IntersectionT $type): string { - return \sprintf('%s', implode('&', array_map($this->stringify(...), $type->types))); + return \sprintf('(%s)', implode('&', array_map($this->safe(...), $type->types))); } #[\Override] public function unionT(UnionT $type): string { - return \sprintf('(%s)', implode('|', array_map($this->stringify(...), $type->types))); + return \sprintf('(%s)', implode('|', array_map($this->safe(...), $type->types))); } #[\Override] @@ -545,19 +486,19 @@ public function classConstantMaskT(ClassConstantMaskT $type): string #[\Override] public function keyOfT(KeyOfT $type): string { - return \sprintf('key-of<%s>', $this->stringifyUnwrap($type->arrayType)); + return \sprintf('key-of<%s>', $this->unsafe($type->arrayType)); } #[\Override] public function valueOfT(ValueOfT $type): string { - return \sprintf('value-of<%s>', $this->stringifyUnwrap($type->arrayType)); + return \sprintf('value-of<%s>', $this->unsafe($type->arrayType)); } #[\Override] public function offsetT(OffsetT $type): string { - return \sprintf('%s[%s]', $this->stringify($type->arrayType), $this->stringifyUnwrap($type->keyType)); + return \sprintf('%s[%s]', $this->safe($type->arrayType), $this->unsafe($type->keyType)); } #[\Override] @@ -565,8 +506,8 @@ public function isSubtypeT(IsSubtypeT $type): string { return \sprintf( '(%s is %s)', - $this->stringify($type->leftType), - $this->stringify($type->rightType), + $this->safe($type->leftType), + $this->safe($type->rightType), ); } @@ -575,16 +516,16 @@ public function ternaryT(TernaryT $type): string { return \sprintf( '(%s ? %s : %s)', - $this->stringify($type->conditionType), - $this->stringify($type->thenType), - $this->stringify($type->elseType), + $this->safe($type->conditionType), + $this->safe($type->thenType), + $this->safe($type->elseType), ); } #[\Override] public function aliasT(AliasT $type): string { - return $this->constructor(\sprintf('%s@%s', $type->class, $type->name), $type->templateArguments); + return \sprintf('%s@%s%s', $type->class, $type->name, $this->templateArguments($type->templateArguments)); } #[\Override] @@ -593,10 +534,34 @@ public function templateT(TemplateT $type): string return $type->name; } + #[\Override] + public function mixedT(MixedT $type): string + { + return 'mixed'; + } + + #[\Override] + public function untypedT(UntypedT $type): mixed + { + return 'untyped-mixed'; + } + + /** + * @param list $templateArguments + */ + public function templateArguments(array $templateArguments): string + { + if ($templateArguments === []) { + return ''; + } + + return \sprintf('<%s>', implode(', ', array_map($this->unsafe(...), $templateArguments))); + } + /** * @param list