<commit_before><?php

namespace WireMock\SerdeGen;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\ContextFactory;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\String_;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
use WireMock\Serde\SerdeClassDefinition;
use WireMock\Serde\PropNaming\ConstantPropertyNamingStrategy;
use WireMock\Serde\PropNaming\ReferencingPropertyNamingStrategy;
use WireMock\Serde\SerdeClassDiscriminationInfo;
use WireMock\Serde\SerdeProp;
use WireMock\Serde\SerializationException;
use WireMock\Serde\Type\SerdeType;
use WireMock\Serde\Type\SerdeTypeArray;
use WireMock\Serde\Type\SerdeTypeAssocArray;
use WireMock\Serde\Type\SerdeTypeClass;
use WireMock\Serde\Type\SerdeTypeNull;
use WireMock\Serde\Type\SerdeTypePrimitive;
use WireMock\Serde\Type\SerdeTypeTypedArray;
use WireMock\Serde\Type\SerdeTypeUnion;
use WireMock\Serde\Type\SerdeTypeUntypedArray;
use WireMock\SerdeGen\Tag\SerdeCatchAllTag;
use WireMock\SerdeGen\Tag\SerdeDiscriminateTypeTag;
use WireMock\SerdeGen\Tag\SerdeNamedByTag;
use WireMock\SerdeGen\Tag\SerdeNameTag;
use WireMock\SerdeGen\Tag\SerdePossibleNamesTag;
use WireMock\SerdeGen\Tag\SerdePossibleSubtypeTag;
use WireMock\SerdeGen\Tag\SerdeUnwrappedTag;

class SerdeTypeParser
{
    /** @var PartialSerdeTypeLookup */
    private $partialSerdeTypeLookup;
    /** @var DocBlockFactory */
    private $docBlockFactory;

    /**
     * @param PartialSerdeTypeLookup $partialSerdeTypeLookup
     */
    public function __construct(PartialSerdeTypeLookup $partialSerdeTypeLookup)
    {
        $this->partialSerdeTypeLookup = $partialSerdeTypeLookup;
        $this->docBlockFactory = DocBlockFactory::createInstance([
            'serde-name' => SerdeNameTag::class,
            'serde-unwrapped' => SerdeUnwrappedTag::class,
            'serde-named-by' => SerdeNamedByTag::class,
            'serde-possible-names' => SerdePossibleNamesTag::class,
            'serde-discriminate-type' => SerdeDiscriminateTypeTag::class,
            'serde-possible-subtype' => SerdePossibleSubtypeTag::class,
        ]);
    }

    /**
     * @param $context Context|null
     * @throws SerializationException|ReflectionException
     */
    public function parseTypeString(string $type, Context $context = null): SerdeType
    {
        $typeResolver = new TypeResolver();
        $resolvedType = $typeResolver->resolve($type, $context);

        return $this->resolveTypeToSerdeType($resolvedType, true);
    }

    /**
     * Translate from a phpDocumentor Type to a wiremock-php SerdeType
     * @throws SerializationException
     * @throws ReflectionException
     */
    private function resolveTypeToSerdeType(Type $type, bool $forceRootType = false): SerdeType
    {
        $isRootType = $forceRootType || $this->partialSerdeTypeLookup->isRootType($type->__toString());
        if ($type instanceof Array_) {
            $typeString = $type->__toString();
            if ($typeString === 'array') {
                return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                    'array',
                    new SerdeTypeUntypedArray(),
                    $isRootType
                );
            }
            // TODO: Handle circular references involving arrays
            // When array of a type is a root type, the type of the array's elements is still considered a root type
            $valueSerdeType = $this->resolveTypeToSerdeType($type->getValueType(), $isRootType);
            if (substr($typeString, -2) === '[]') {
                return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                    $typeString,
                    new SerdeTypeTypedArray($valueSerdeType),
                    $isRootType
                );
            }
            $keySerdeType = $this->resolveTypeToSerdeType($type->getKeyType(), $isRootType);
            if (!($keySerdeType instanceof SerdeTypePrimitive)) {
                throw new SerializationException(
                    'Expected associative array to have primitive key type, but found' .
                        $keySerdeType->displayName()
                );
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $typeString,
                new SerdeTypeAssocArray($keySerdeType, $valueSerdeType),
                $isRootType
            );
        } elseif ($type instanceof Boolean) {
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                'bool',
                new SerdeTypePrimitive('bool'),
                $isRootType
            );
        } elseif ($type instanceof Compound) {
            // TODO: Handle circular references involving unions
            $primitives = [];
            $nonPrimitive = null;
            foreach ($type->getIterator() as $subtype) {
                // Subtypes in a union are considered root types if the union is a root type
                $serdeSubtype = $this->resolveTypeToSerdeType($subtype, $isRootType);
                if ($serdeSubtype instanceof SerdeTypePrimitive) {
                    $primitives[] = $serdeSubtype;
                } elseif (($serdeSubtype instanceof SerdeTypeClass) || ($serdeSubtype instanceof SerdeTypeArray)) {
                    if ($nonPrimitive !== null) {
                        throw new SerializationException("Serde of union types with more than one non-primitive type are not supported: $type");
                    } else {
                        $nonPrimitive = $serdeSubtype;
                    }
                } else {
                    throw new SerializationException("Serde of union types only supported for primitives, classes, and arrays: $type");
                }
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $type->__toString(),
                new SerdeTypeUnion($primitives, $nonPrimitive),
                $isRootType
            );
        } elseif ($type instanceof Float_) {
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                'float',
                new SerdeTypePrimitive('float'),
                $isRootType
            );
        } elseif ($type instanceof Integer) {
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                'int',
                new SerdeTypePrimitive('int'),
                $isRootType
            );
        } elseif ($type instanceof Null_) {
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                'null',
                new SerdeTypeNull(),
                $isRootType
            );
        } elseif ($type instanceof Nullable) {
            // TODO: Handle circular references involving nullables
            // The inner type of nullable types are root types if the nullable type is a root type
            $innerType = $this->resolveTypeToSerdeType($type->getActualType(), $isRootType);
            if ($innerType instanceof SerdeTypePrimitive) {
                $unionType = new SerdeTypeUnion([$innerType, new SerdeTypeNull()], null);
            } elseif ($innerType instanceof SerdeTypeClass || $innerType instanceof SerdeTypeArray) {
                $unionType = new SerdeTypeUnion([new SerdeTypeNull()], $innerType);
            } else {
                throw new SerializationException('Unexpected nullable type: ' . $innerType->displayName());
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $type->__toString(),
                $unionType,
                $isRootType
            );
        } elseif ($type instanceof Object_) {
            $fqsen = $type->getFqsen();
            if ($fqsen === null) {
                throw new SerializationException('Unsupported use of type "object"');
            }

            if (!$this->partialSerdeTypeLookup->contains($fqsen)) {
                // Add a SerdeTypeClass with a placeholder class definition, to break cycles
                $placeholderClassDef = new SerdeClassDefinition(null, [], []);
                $serdeType = new SerdeTypeClass($fqsen, $placeholderClassDef);
                $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded($fqsen, $serdeType, $isRootType);

                // Create the actual class definition
                $classDefinition = $this->createClassDefinition($fqsen, $isRootType);

                // Overwrite the placeholder class definition on the existing SerdeTypeClass reference
                // Because this property is private, we do so via reflection
                $refProp = new ReflectionProperty($serdeType, 'classDefinition');
                $refProp->setAccessible(true);
                $refProp->setValue($serdeType, $classDefinition);
            }
            return $this->partialSerdeTypeLookup->getSerdeType($fqsen);
        } elseif ($type instanceof String_) {
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                'string',
                new SerdeTypePrimitive('string'),
                $isRootType
            );
        } else {
            throw new SerializationException('Unexpected type ' . get_class($type) . ": $type");
        }
    }

    /**
     * @throws SerializationException|ReflectionException
     */
    private function createClassDefinition(string $classType, bool $isRootType): SerdeClassDefinition
    {
        $refClass = new ReflectionClass($classType);
        $contextFactory = new ContextFactory();
        $context = $contextFactory->createFromReflector($refClass);<commit_msg>Refactor: commonise primitive parsing<commit_after><?php

namespace WireMock\SerdeGen;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\ContextFactory;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\String_;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
use WireMock\Serde\SerdeClassDefinition;
use WireMock\Serde\PropNaming\ConstantPropertyNamingStrategy;
use WireMock\Serde\PropNaming\ReferencingPropertyNamingStrategy;
use WireMock\Serde\SerdeClassDiscriminationInfo;
use WireMock\Serde\SerdeProp;
use WireMock\Serde\SerializationException;
use WireMock\Serde\Type\SerdeType;
use WireMock\Serde\Type\SerdeTypeArray;
use WireMock\Serde\Type\SerdeTypeAssocArray;
use WireMock\Serde\Type\SerdeTypeClass;
use WireMock\Serde\Type\SerdeTypeNull;
use WireMock\Serde\Type\SerdeTypePrimitive;
use WireMock\Serde\Type\SerdeTypeTypedArray;
use WireMock\Serde\Type\SerdeTypeUnion;
use WireMock\Serde\Type\SerdeTypeUntypedArray;
use WireMock\SerdeGen\Tag\SerdeCatchAllTag;
use WireMock\SerdeGen\Tag\SerdeDiscriminateTypeTag;
use WireMock\SerdeGen\Tag\SerdeNamedByTag;
use WireMock\SerdeGen\Tag\SerdeNameTag;
use WireMock\SerdeGen\Tag\SerdePossibleNamesTag;
use WireMock\SerdeGen\Tag\SerdePossibleSubtypeTag;
use WireMock\SerdeGen\Tag\SerdeUnwrappedTag;

class SerdeTypeParser
{
    /** @var PartialSerdeTypeLookup */
    private $partialSerdeTypeLookup;
    /** @var DocBlockFactory */
    private $docBlockFactory;

    /**
     * @param PartialSerdeTypeLookup $partialSerdeTypeLookup
     */
    public function __construct(PartialSerdeTypeLookup $partialSerdeTypeLookup)
    {
        $this->partialSerdeTypeLookup = $partialSerdeTypeLookup;
        $this->docBlockFactory = DocBlockFactory::createInstance([
            'serde-name' => SerdeNameTag::class,
            'serde-unwrapped' => SerdeUnwrappedTag::class,
            'serde-named-by' => SerdeNamedByTag::class,
            'serde-possible-names' => SerdePossibleNamesTag::class,
            'serde-discriminate-type' => SerdeDiscriminateTypeTag::class,
            'serde-possible-subtype' => SerdePossibleSubtypeTag::class,
        ]);
    }

    /**
     * @param $context Context|null
     * @throws SerializationException|ReflectionException
     */
    public function parseTypeString(string $type, Context $context = null): SerdeType
    {
        $typeResolver = new TypeResolver();
        $resolvedType = $typeResolver->resolve($type, $context);

        return $this->resolveTypeToSerdeType($resolvedType, true);
    }

    /**
     * Translate from a phpDocumentor Type to a wiremock-php SerdeType
     * @throws SerializationException
     * @throws ReflectionException
     */
    private function resolveTypeToSerdeType(Type $type, bool $forceRootType = false): SerdeType
    {
        $isRootType = $forceRootType || $this->partialSerdeTypeLookup->isRootType($type->__toString());
        if ($type instanceof Array_) {
            $typeString = $type->__toString();
            if ($typeString === 'array') {
                return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                    'array',
                    new SerdeTypeUntypedArray(),
                    $isRootType
                );
            }
            // TODO: Handle circular references involving arrays
            // When array of a type is a root type, the type of the array's elements is still considered a root type
            $valueSerdeType = $this->resolveTypeToSerdeType($type->getValueType(), $isRootType);
            if (substr($typeString, -2) === '[]') {
                return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                    $typeString,
                    new SerdeTypeTypedArray($valueSerdeType),
                    $isRootType
                );
            }
            $keySerdeType = $this->resolveTypeToSerdeType($type->getKeyType(), $isRootType);
            if (!($keySerdeType instanceof SerdeTypePrimitive)) {
                throw new SerializationException(
                    'Expected associative array to have primitive key type, but found' .
                        $keySerdeType->displayName()
                );
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $typeString,
                new SerdeTypeAssocArray($keySerdeType, $valueSerdeType),
                $isRootType
            );
        } elseif ($type instanceof Compound) {
            // TODO: Handle circular references involving unions
            $primitives = [];
            $nonPrimitive = null;
            foreach ($type->getIterator() as $subtype) {
                // Subtypes in a union are considered root types if the union is a root type
                $serdeSubtype = $this->resolveTypeToSerdeType($subtype, $isRootType);
                if ($serdeSubtype instanceof SerdeTypePrimitive) {
                    $primitives[] = $serdeSubtype;
                } elseif (($serdeSubtype instanceof SerdeTypeClass) || ($serdeSubtype instanceof SerdeTypeArray)) {
                    if ($nonPrimitive !== null) {
                        throw new SerializationException("Serde of union types with more than one non-primitive type are not supported: $type");
                    } else {
                        $nonPrimitive = $serdeSubtype;
                    }
                } else {
                    throw new SerializationException("Serde of union types only supported for primitives, classes, and arrays: $type");
                }
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $type->__toString(),
                new SerdeTypeUnion($primitives, $nonPrimitive),
                $isRootType
            );
        } elseif ($type instanceof Nullable) {
            // TODO: Handle circular references involving nullables
            // The inner type of nullable types are root types if the nullable type is a root type
            $innerType = $this->resolveTypeToSerdeType($type->getActualType(), $isRootType);
            if ($innerType instanceof SerdeTypePrimitive) {
                $unionType = new SerdeTypeUnion([$innerType, new SerdeTypeNull()], null);
            } elseif ($innerType instanceof SerdeTypeClass || $innerType instanceof SerdeTypeArray) {
                $unionType = new SerdeTypeUnion([new SerdeTypeNull()], $innerType);
            } else {
                throw new SerializationException('Unexpected nullable type: ' . $innerType->displayName());
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $type->__toString(),
                $unionType,
                $isRootType
            );
        } elseif ($type instanceof Object_) {
            $fqsen = $type->getFqsen();
            if ($fqsen === null) {
                throw new SerializationException('Unsupported use of type "object"');
            }

            if (!$this->partialSerdeTypeLookup->contains($fqsen)) {
                // Add a SerdeTypeClass with a placeholder class definition, to break cycles
                $placeholderClassDef = new SerdeClassDefinition(null, [], []);
                $serdeType = new SerdeTypeClass($fqsen, $placeholderClassDef);
                $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded($fqsen, $serdeType, $isRootType);

                // Create the actual class definition
                $classDefinition = $this->createClassDefinition($fqsen, $isRootType);

                // Overwrite the placeholder class definition on the existing SerdeTypeClass reference
                // Because this property is private, we do so via reflection
                $refProp = new ReflectionProperty($serdeType, 'classDefinition');
                $refProp->setAccessible(true);
                $refProp->setValue($serdeType, $classDefinition);
            }
            return $this->partialSerdeTypeLookup->getSerdeType($fqsen);
        } elseif ($type instanceof Null_) {
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                'null',
                new SerdeTypeNull(),
                $isRootType
            );
        } else {
            // $type is a primitive, or unsupported
            if ($type instanceof Boolean) {
                $primitiveType = 'bool';
            } elseif ($type instanceof Float_) {
                $primitiveType = 'float';
            } elseif ($type instanceof Integer) {
                $primitiveType = 'int';
            } elseif ($type instanceof String_) {
                $primitiveType = 'string';
            } else {
                throw new SerializationException('Unexpected type ' . get_class($type) . ": $type");
            }
            return $this->partialSerdeTypeLookup->addSerdeTypeIfNeeded(
                $primitiveType,
                new SerdeTypePrimitive($primitiveType),
                $isRootType
            );
        }
    }

    /**
     * @throws SerializationException|ReflectionException
     */
    private function createClassDefinition(string $classType, bool $isRootType): SerdeClassDefinition
    {
        $refClass = new ReflectionClass($classType);
        $contextFactory = new ContextFactory();
        $context = $contextFactory->createFromReflector($refClass);
{'fraction_non_alphanumeric': 0.09589993050729674, 'fraction_numerical': 9.927529038022436e-05, 'mean_word_length': 2.442166410387835, 'pattern_counts': {'":': 2, '<': 5, '<?xml version=': 0, '>': 111, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6601549', 'n_tokens_mistral': 5355, 'n_tokens_neox': 5231, 'n_words': 1107}