From 49dd055a584144a194a7a806ae34a7ac0f95bcb1 Mon Sep 17 00:00:00 2001 From: glb_adm Date: Mon, 6 Jul 2026 22:20:16 +0000 Subject: [PATCH] =?UTF-8?q?build:=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F?= =?UTF-8?q?=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0=20=D0=B2=20git=20?= =?UTF-8?q?=E2=80=94=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20PHP-=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20lib/Vendor=20(438=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2,=20=D0=B8=D0=B7=20=D0=BF=D0=B8=D0=BD=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B5=D0=BB=D0=B8=D0=B7=D0=B0=20v22.0.12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Теперь master f7_talk = ПОЛНЫЙ деплой-артефакт: built js (наша сборка) + lib/Vendor (серверные PHP-deps) + патч Listener + version 22.0.12.1. Готов к брендингу dev-f7 и подписи dev-appstore. Апстрим-подписи (appinfo/signature.json) в репо нет — переподпишет dev-appstore при публикации. --- lib/Vendor/CuyZ/Valinor/Cache/ChainCache.php | 136 ++++ .../Exception/CacheDirectoryNotWritable.php | 19 + .../CompiledPhpCacheFileNotWritten.php | 19 + .../CorruptedCompiledPhpCacheFile.php | 19 + .../Exception/InvalidSignatureToWarmup.php | 21 + .../CuyZ/Valinor/Cache/FileSystemCache.php | 223 ++++++ .../CuyZ/Valinor/Cache/FileWatchingCache.php | 159 ++++ .../CuyZ/Valinor/Cache/KeySanitizerCache.php | 113 +++ .../CuyZ/Valinor/Cache/RuntimeCache.php | 91 +++ .../Warmup/RecursiveCacheWarmupService.php | 109 +++ lib/Vendor/CuyZ/Valinor/Cache/WarmupCache.php | 16 + lib/Vendor/CuyZ/Valinor/Compiler/Compiler.php | 60 ++ .../Compiler/Library/NewAttributeNode.php | 53 ++ .../Compiler/Library/TypeAcceptNode.php | 24 + .../Compiler/Native/AnonymousClassNode.php | 105 +++ .../Compiler/Native/ArrayKeyAccessNode.php | 26 + .../Valinor/Compiler/Native/ArrayNode.php | 36 + .../Valinor/Compiler/Native/AssignNode.php | 25 + .../CuyZ/Valinor/Compiler/Native/CallNode.php | 39 + .../CuyZ/Valinor/Compiler/Native/CastNode.php | 29 + .../Valinor/Compiler/Native/ClassNode.php | 33 + .../Valinor/Compiler/Native/CloneNode.php | 23 + .../Valinor/Compiler/Native/ClosureNode.php | 53 ++ .../Compiler/Native/ComplianceNode.php | 118 +++ .../Valinor/Compiler/Native/DifferentNode.php | 25 + .../Valinor/Compiler/Native/EqualsNode.php | 25 + .../Compiler/Native/ExpressionNode.php | 19 + .../Valinor/Compiler/Native/ForEachNode.php | 35 + .../Compiler/Native/FunctionCallNode.php | 25 + .../Compiler/Native/FunctionNameNode.php | 32 + .../Compiler/Native/GreaterOrEqualsToNode.php | 25 + .../Compiler/Native/GreaterThanNode.php | 25 + .../CuyZ/Valinor/Compiler/Native/IfNode.php | 31 + .../Compiler/Native/InstanceOfNode.php | 28 + .../Compiler/Native/LessOrEqualsToNode.php | 25 + .../Valinor/Compiler/Native/LessThanNode.php | 25 + .../Compiler/Native/LogicalAndNode.php | 38 + .../Valinor/Compiler/Native/LogicalOrNode.php | 38 + .../Valinor/Compiler/Native/MatchNode.php | 65 ++ .../Compiler/Native/MethodCallNode.php | 27 + .../Valinor/Compiler/Native/MethodNode.php | 109 +++ .../Valinor/Compiler/Native/NegateNode.php | 19 + .../Valinor/Compiler/Native/NewClassNode.php | 40 + .../Native/ParameterDeclarationNode.php | 23 + .../Valinor/Compiler/Native/PhpFileNode.php | 27 + .../Native/PropertyDeclarationNode.php | 23 + .../Valinor/Compiler/Native/PropertyNode.php | 19 + .../Valinor/Compiler/Native/ReturnNode.php | 22 + .../Compiler/Native/ShortClosureNode.php | 46 ++ .../Compiler/Native/StaticAccessNode.php | 25 + .../Compiler/Native/StaticMethodCallNode.php | 27 + .../Valinor/Compiler/Native/TernaryNode.php | 28 + .../Valinor/Compiler/Native/ThrowNode.php | 23 + .../Valinor/Compiler/Native/ValueNode.php | 49 ++ .../Compiler/Native/VariableAccessNode.php | 25 + .../Valinor/Compiler/Native/VariableNode.php | 19 + .../CuyZ/Valinor/Compiler/Native/WrapNode.php | 22 + .../Valinor/Compiler/Native/YieldNode.php | 26 + lib/Vendor/CuyZ/Valinor/Compiler/Node.php | 203 +++++ .../Definition/AttributeDefinition.php | 20 + .../CuyZ/Valinor/Definition/Attributes.php | 81 ++ .../Valinor/Definition/ClassDefinition.php | 22 + .../Exception/ClassTypeAliasesDuplication.php | 26 + .../Exception/ExtendTagTypeError.php | 25 + .../Exception/InvalidExtendTagClassName.php | 27 + .../Exception/InvalidExtendTagType.php | 27 + .../Exception/InvalidTypeAliasImportClass.php | 20 + .../InvalidTypeAliasImportClassType.php | 21 + .../Exception/SeveralExtendTagsFound.php | 23 + .../Exception/UnknownTypeAliasImport.php | 23 + .../Valinor/Definition/FunctionDefinition.php | 27 + .../Valinor/Definition/FunctionObject.php | 20 + .../Valinor/Definition/FunctionsContainer.php | 60 ++ .../Valinor/Definition/MethodDefinition.php | 23 + .../CuyZ/Valinor/Definition/Methods.php | 60 ++ .../Definition/ParameterDefinition.php | 24 + .../CuyZ/Valinor/Definition/Parameters.php | 68 ++ .../CuyZ/Valinor/Definition/Properties.php | 50 ++ .../Valinor/Definition/PropertyDefinition.php | 24 + .../Repository/AttributesRepository.php | 23 + .../Cache/CacheClassDefinitionRepository.php | 38 + .../CacheFunctionDefinitionRepository.php | 40 + .../Cache/Compiler/AttributesCompiler.php | 79 ++ .../Compiler/ClassDefinitionCompiler.php | 74 ++ .../Exception/TypeCannotBeCompiled.php | 22 + .../Compiler/FunctionDefinitionCompiler.php | 60 ++ .../Compiler/MethodDefinitionCompiler.php | 54 ++ .../Compiler/ParameterDefinitionCompiler.php | 48 ++ .../Compiler/PropertyDefinitionCompiler.php | 39 + .../Cache/Compiler/TypeCompiler.php | 193 +++++ .../Repository/ClassDefinitionRepository.php | 14 + .../FunctionDefinitionRepository.php | 13 + .../ReflectionAttributesRepository.php | 74 ++ .../ReflectionClassDefinitionRepository.php | 177 +++++ ...ReflectionFunctionDefinitionRepository.php | 102 +++ .../ReflectionMethodDefinitionBuilder.php | 63 ++ .../ReflectionParameterDefinitionBuilder.php | 74 ++ .../ReflectionPropertyDefinitionBuilder.php | 64 ++ .../ClassImportedTypeAliasResolver.php | 111 +++ .../ClassLocalTypeAliasResolver.php | 89 +++ .../TypeResolver/ClassParentTypeResolver.php | 86 ++ .../TypeResolver/ClassTemplatesResolver.php | 76 ++ .../FunctionReturnTypeResolver.php | 42 + .../TypeResolver/ParameterTypeResolver.php | 86 ++ .../TypeResolver/PropertyTypeResolver.php | 42 + .../TypeResolver/ReflectionTypeResolver.php | 86 ++ lib/Vendor/CuyZ/Valinor/Library/Container.php | 276 +++++++ lib/Vendor/CuyZ/Valinor/Library/Settings.php | 138 ++++ .../CuyZ/Valinor/Mapper/ArgumentsMapper.php | 16 + .../Valinor/Mapper/ArgumentsMapperError.php | 42 + .../Exception/InvalidMappingTypeSignature.php | 21 + .../TypeErrorDuringArgumentsMapping.php | 22 + .../Exception/TypeErrorDuringMapping.php | 22 + .../CuyZ/Valinor/Mapper/MappingError.php | 14 + .../CuyZ/Valinor/Mapper/Object/Argument.php | 101 +++ .../CuyZ/Valinor/Mapper/Object/Arguments.php | 86 ++ .../Valinor/Mapper/Object/ArgumentsValues.php | 142 ++++ .../Valinor/Mapper/Object/Constructor.php | 45 ++ .../Object/DateTimeFormatConstructor.php | 63 ++ .../Mapper/Object/DynamicConstructor.php | 51 ++ .../Exception/CannotFindObjectBuilder.php | 68 ++ .../Exception/CannotInstantiateObject.php | 20 + .../Exception/CannotParseToDateTime.php | 41 + .../InvalidConstructorClassTypeParameter.php | 21 + ...nstructorMethodWithAttributeReturnType.php | 27 + .../InvalidConstructorReturnType.php | 24 + .../Mapper/Object/Exception/InvalidSource.php | 44 ++ .../MissingConstructorClassTypeParameter.php | 20 + .../Exception/ObjectBuildersCollision.php | 20 + .../Exception/PermissiveTypeNotAllowed.php | 21 + .../Factory/CacheObjectBuilderFactory.php | 36 + .../ConstructorObjectBuilderFactory.php | 197 +++++ .../Factory/DateTimeObjectBuilderFactory.php | 62 ++ .../DateTimeZoneObjectBuilderFactory.php | 78 ++ .../Object/Factory/ObjectBuilderFactory.php | 17 + .../ReflectionObjectBuilderFactory.php | 17 + .../Factory/SortingObjectBuilderFactory.php | 109 +++ .../StrictTypesObjectBuilderFactory.php | 47 ++ .../Mapper/Object/FunctionObjectBuilder.php | 74 ++ .../Valinor/Mapper/Object/MethodArguments.php | 43 + .../Mapper/Object/MethodObjectBuilder.php | 43 + .../Object/NativeConstructorObjectBuilder.php | 39 + .../Mapper/Object/NativeEnumObjectBuilder.php | 51 ++ .../Valinor/Mapper/Object/ObjectBuilder.php | 21 + .../Mapper/Object/ReflectionObjectBuilder.php | 42 + .../Exception/FileExtensionNotHandled.php | 19 + .../Mapper/Source/Exception/InvalidJson.php | 26 + .../Mapper/Source/Exception/InvalidSource.php | 13 + .../Mapper/Source/Exception/InvalidYaml.php | 24 + .../Source/Exception/SourceNotIterable.php | 24 + .../Source/Exception/UnableToReadFile.php | 19 + .../Exception/YamlExtensionNotEnabled.php | 24 + .../CuyZ/Valinor/Mapper/Source/FileSource.php | 58 ++ .../Mapper/Source/IdentifiableSource.php | 11 + .../CuyZ/Valinor/Mapper/Source/JsonSource.php | 55 ++ .../Mapper/Source/Modifier/CamelCaseKeys.php | 68 ++ .../Mapper/Source/Modifier/Mapping.php | 44 ++ .../Mapper/Source/Modifier/PathMapping.php | 94 +++ .../CuyZ/Valinor/Mapper/Source/Source.php | 80 ++ .../CuyZ/Valinor/Mapper/Source/YamlSource.php | 62 ++ .../Mapper/Tree/Builder/ArrayNodeBuilder.php | 92 +++ .../Tree/Builder/InterfaceNodeBuilder.php | 144 ++++ .../Mapper/Tree/Builder/ListNodeBuilder.php | 93 +++ .../Mapper/Tree/Builder/MixedNodeBuilder.php | 24 + .../Mapper/Tree/Builder/NodeBuilder.php | 11 + .../Mapper/Tree/Builder/NullNodeBuilder.php | 29 + .../Tree/Builder/ObjectImplementations.php | 151 ++++ .../Mapper/Tree/Builder/ObjectNodeBuilder.php | 155 ++++ .../Mapper/Tree/Builder/RootNodeBuilder.php | 47 ++ .../Mapper/Tree/Builder/ScalarNodeBuilder.php | 32 + .../Tree/Builder/ShapedArrayNodeBuilder.php | 100 +++ .../Valinor/Mapper/Tree/Builder/TreeNode.php | 184 +++++ .../Mapper/Tree/Builder/TypeNodeBuilder.php | 75 ++ .../Builder/UndefinedObjectNodeBuilder.php | 26 + .../Mapper/Tree/Builder/UnionNodeBuilder.php | 108 +++ .../Tree/Builder/ValueAlteringNodeBuilder.php | 47 ++ .../Tree/Exception/CannotInferFinalClass.php | 21 + .../Exception/CannotMapToPermissiveType.php | 23 + .../Exception/CannotResolveObjectType.php | 19 + .../Exception/CannotResolveTypeFromUnion.php | 56 ++ .../Exception/CircularDependencyDetected.php | 20 + .../InterfaceHasBothConstructorAndInfer.php | 22 + .../Exception/InvalidAbstractObjectName.php | 19 + .../Tree/Exception/InvalidIterableKeyType.php | 23 + .../Mapper/Tree/Exception/InvalidListKey.php | 40 + .../Exception/InvalidNodeHasNoMappedValue.php | 19 + .../Tree/Exception/InvalidNodeValue.php | 44 ++ .../InvalidResolvedImplementationValue.php | 22 + .../Tree/Exception/InvalidTraversableKey.php | 42 + .../Tree/Exception/MissingNodeValue.php | 40 + ...issingObjectImplementationRegistration.php | 20 + .../ObjectImplementationCallbackError.php | 26 + .../ObjectImplementationNotRegistered.php | 28 + .../ResolvedImplementationIsNotAccepted.php | 20 + .../Mapper/Tree/Exception/SourceIsNotNull.php | 26 + .../Tree/Exception/SourceMustBeIterable.php | 50 ++ .../Exception/SourceValueWasNotFilled.php | 19 + .../TooManyResolvedTypesFromUnion.php | 50 ++ .../Tree/Exception/UnexpectedKeysInSource.php | 56 ++ .../Tree/Exception/UnresolvableShellType.php | 17 + .../Mapper/Tree/Message/DefaultMessage.php | 105 +++ .../Mapper/Tree/Message/ErrorMessage.php | 10 + .../Formatter/AggregateMessageFormatter.php | 28 + .../Formatter/CallbackMessageFormatter.php | 47 ++ .../Formatter/LocaleMessageFormatter.php | 18 + .../Message/Formatter/MessageFormatter.php | 13 + .../Message/Formatter/MessageMapFormatter.php | 105 +++ .../Formatter/TranslationMessageFormatter.php | 88 +++ .../Valinor/Mapper/Tree/Message/HasCode.php | 16 + .../Mapper/Tree/Message/HasParameters.php | 46 ++ .../Valinor/Mapper/Tree/Message/Message.php | 11 + .../Mapper/Tree/Message/MessageBuilder.php | 189 +++++ .../Valinor/Mapper/Tree/Message/Messages.php | 134 ++++ .../Mapper/Tree/Message/NodeMessage.php | 161 ++++ .../Mapper/Tree/Message/UserlandError.php | 30 + lib/Vendor/CuyZ/Valinor/Mapper/Tree/Node.php | 136 ++++ .../Valinor/Mapper/Tree/NodeTraverser.php | 44 ++ lib/Vendor/CuyZ/Valinor/Mapper/Tree/Shell.php | 201 +++++ lib/Vendor/CuyZ/Valinor/Mapper/TreeMapper.php | 24 + .../Valinor/Mapper/TypeArgumentsMapper.php | 71 ++ .../CuyZ/Valinor/Mapper/TypeTreeMapper.php | 53 ++ .../Valinor/Mapper/TypeTreeMapperError.php | 38 + lib/Vendor/CuyZ/Valinor/MapperBuilder.php | 609 ++++++++++++++ .../Valinor/Normalizer/ArrayNormalizer.php | 43 + .../CuyZ/Valinor/Normalizer/AsTransformer.php | 55 ++ ...cularReferenceFoundDuringNormalization.php | 21 + .../KeyTransformerHasTooManyParameters.php | 20 + .../KeyTransformerParameterInvalidType.php | 20 + ...TransformerHasInvalidCallableParameter.php | 22 + .../Exception/TransformerHasNoParameter.php | 21 + .../TransformerHasTooManyParameters.php | 21 + .../Exception/TypeUnhandledByNormalizer.php | 23 + lib/Vendor/CuyZ/Valinor/Normalizer/Format.php | 98 +++ .../CannotFormatInvalidTypeToJson.php | 21 + .../Normalizer/Formatter/JsonFormatter.php | 138 ++++ .../Valinor/Normalizer/JsonNormalizer.php | 159 ++++ .../CuyZ/Valinor/Normalizer/Normalizer.php | 25 + .../Valinor/Normalizer/StreamNormalizer.php | 28 + .../Transformer/CacheTransformer.php | 162 ++++ .../Compiler/TransformerDefinition.php | 100 +++ .../Compiler/TransformerDefinitionBuilder.php | 127 +++ .../Compiler/TransformerRootNode.php | 73 ++ .../Compiler/TypeFormatter/ClassFormatter.php | 212 +++++ .../TypeFormatter/DateTimeFormatter.php | 26 + .../TypeFormatter/DateTimeZoneFormatter.php | 24 + .../Compiler/TypeFormatter/EnumFormatter.php | 32 + .../TypeFormatter/InterfaceFormatter.php | 36 + .../Compiler/TypeFormatter/MixedFormatter.php | 90 +++ .../Compiler/TypeFormatter/NullFormatter.php | 24 + .../RegisteredTransformersFormatter.php | 118 +++ .../TypeFormatter/ScalarFormatter.php | 24 + .../TypeFormatter/ShapedArrayFormatter.php | 101 +++ .../TypeFormatter/StdClassFormatter.php | 68 ++ .../TypeFormatter/TraversableFormatter.php | 114 +++ .../Compiler/TypeFormatter/TypeFormatter.php | 18 + .../Compiler/TypeFormatter/UnionFormatter.php | 83 ++ .../TypeFormatter/UnitEnumFormatter.php | 29 + .../TypeFormatter/UnsureTypeFormatter.php | 78 ++ .../Normalizer/Transformer/EmptyObject.php | 13 + .../Transformer/EvaluatedTransformer.php | 13 + .../Transformer/RecursiveTransformer.php | 194 +++++ .../Normalizer/Transformer/Transformer.php | 11 + .../Transformer/TransformerContainer.php | 102 +++ lib/Vendor/CuyZ/Valinor/Type/BooleanType.php | 11 + lib/Vendor/CuyZ/Valinor/Type/ClassType.php | 8 + .../CuyZ/Valinor/Type/CombiningType.php | 16 + .../Valinor/Type/CompositeTraversableType.php | 15 + .../CuyZ/Valinor/Type/CompositeType.php | 14 + lib/Vendor/CuyZ/Valinor/Type/FixedType.php | 9 + lib/Vendor/CuyZ/Valinor/Type/FloatType.php | 11 + lib/Vendor/CuyZ/Valinor/Type/GenericType.php | 17 + lib/Vendor/CuyZ/Valinor/Type/IntegerType.php | 11 + lib/Vendor/CuyZ/Valinor/Type/ObjectType.php | 17 + .../CuyZ/Valinor/Type/Parser/CachedParser.php | 21 + .../Constant/ClassConstantCaseNotFound.php | 24 + .../Constant/MissingClassConstantCase.php | 23 + .../Exception/Enum/EnumCaseNotFound.php | 24 + .../Parser/Exception/Enum/MissingEnumCase.php | 24 + .../Enum/MissingSpecificEnumCase.php | 24 + .../Generic/AssignedGenericNotFound.php | 24 + .../Exception/Generic/CannotAssignGeneric.php | 26 + .../Generic/GenericClosingBracketMissing.php | 30 + .../Exception/Generic/GenericCommaMissing.php | 30 + .../Generic/InvalidAssignedGeneric.php | 25 + .../Exception/Generic/MissingGenerics.php | 38 + .../Exception/InvalidIntersectionType.php | 20 + .../Type/Parser/Exception/InvalidType.php | 10 + .../Iterable/ArrayClosingBracketMissing.php | 22 + .../Exception/Iterable/ArrayCommaMissing.php | 32 + .../Exception/Iterable/InvalidArrayKey.php | 21 + .../Exception/Iterable/InvalidIterableKey.php | 22 + .../IterableClosingBracketMissing.php | 21 + .../Iterable/IterableCommaMissing.php | 21 + .../Iterable/ListClosingBracketMissing.php | 22 + .../ShapedArrayClosingBracketMissing.php | 35 + .../Iterable/ShapedArrayColonTokenMissing.php | 35 + .../Iterable/ShapedArrayCommaMissing.php | 28 + .../ShapedArrayElementDuplicatedKey.php | 20 + .../ShapedArrayElementTypeMissing.php | 42 + .../Iterable/ShapedArrayEmptyElements.php | 20 + .../ShapedArrayInvalidUnsealedType.php | 32 + ...pedArrayUnexpectedTokenAfterSealedType.php | 36 + ...apedArrayWithoutElementsWithSealedType.php | 23 + .../SimpleArrayClosingBracketMissing.php | 21 + .../Magic/ValueOfClosingBracketMissing.php | 21 + .../Magic/ValueOfIncorrectSubType.php | 21 + .../Magic/ValueOfOpeningBracketMissing.php | 20 + .../Exception/MissingClosingQuoteChar.php | 19 + .../RightIntersectionTypeMissing.php | 20 + .../Exception/RightUnionTypeMissing.php | 20 + .../ClassStringClosingBracketMissing.php | 21 + .../Scalar/IntegerRangeInvalidMaxValue.php | 22 + .../Scalar/IntegerRangeInvalidMinValue.php | 21 + .../IntegerRangeMissingClosingBracket.php | 21 + .../Scalar/IntegerRangeMissingComma.php | 21 + .../Scalar/IntegerRangeMissingMaxValue.php | 21 + .../Scalar/IntegerRangeMissingMinValue.php | 20 + .../Scalar/InvalidClassStringSubType.php | 21 + .../Scalar/ReversedValuesForIntegerRange.php | 20 + .../Scalar/SameValueForIntegerRange.php | 20 + .../Template/DuplicatedTemplateName.php | 22 + .../Template/InvalidClassTemplate.php | 24 + .../Type/Parser/Exception/UnknownSymbol.php | 16 + .../Factory/LexingTypeParserFactory.php | 96 +++ .../Specifications/AliasSpecification.php | 108 +++ .../ClassContextSpecification.php | 26 + .../TypeAliasAssignerSpecification.php | 29 + .../TypeParserSpecification.php | 13 + .../Type/Parser/Factory/TypeParserFactory.php | 30 + .../Type/Parser/GenericCheckerParser.php | 94 +++ .../Valinor/Type/Parser/Lexer/Annotations.php | 108 +++ .../Valinor/Type/Parser/Lexer/NativeLexer.php | 110 +++ .../Type/Parser/Lexer/SpecificationsLexer.php | 23 + .../Type/Parser/Lexer/Token/ArrayToken.php | 236 ++++++ .../Type/Parser/Lexer/Token/CallableToken.php | 26 + .../Type/Parser/Lexer/Token/CaseFinder.php | 70 ++ .../Parser/Lexer/Token/ClassNameToken.php | 167 ++++ .../Parser/Lexer/Token/ClassStringToken.php | 46 ++ .../Lexer/Token/ClosingBracketToken.php | 18 + .../Lexer/Token/ClosingCurlyBracketToken.php | 18 + .../Lexer/Token/ClosingSquareBracketToken.php | 18 + .../Type/Parser/Lexer/Token/ColonToken.php | 18 + .../Type/Parser/Lexer/Token/CommaToken.php | 18 + .../Parser/Lexer/Token/DoubleColonToken.php | 18 + .../Type/Parser/Lexer/Token/EnumNameToken.php | 53 ++ .../Parser/Lexer/Token/FloatValueToken.php | 25 + .../Type/Parser/Lexer/Token/IntegerToken.php | 78 ++ .../Parser/Lexer/Token/IntegerValueToken.php | 25 + .../Parser/Lexer/Token/IntersectionToken.php | 47 ++ .../Type/Parser/Lexer/Token/IterableToken.php | 64 ++ .../Lexer/Token/LeftTraversingToken.php | 14 + .../Type/Parser/Lexer/Token/ListToken.php | 59 ++ .../Type/Parser/Lexer/Token/NullableToken.php | 27 + .../Type/Parser/Lexer/Token/ObjectToken.php | 30 + .../Lexer/Token/OpeningBracketToken.php | 18 + .../Lexer/Token/OpeningCurlyBracketToken.php | 18 + .../Lexer/Token/OpeningSquareBracketToken.php | 31 + .../Parser/Lexer/Token/StringValueToken.php | 32 + .../Valinor/Type/Parser/Lexer/Token/Token.php | 11 + .../Parser/Lexer/Token/TraversingToken.php | 14 + .../Parser/Lexer/Token/TripleDotsToken.php | 18 + .../Type/Parser/Lexer/Token/TypeToken.php | 40 + .../Type/Parser/Lexer/Token/UnionToken.php | 31 + .../Type/Parser/Lexer/Token/VacantToken.php | 49 ++ .../Type/Parser/Lexer/Token/ValueOfToken.php | 65 ++ .../Valinor/Type/Parser/Lexer/TokenStream.php | 69 ++ .../Type/Parser/Lexer/TokenizedAnnotation.php | 62 ++ .../Type/Parser/Lexer/TokensExtractor.php | 65 ++ .../Valinor/Type/Parser/Lexer/TypeLexer.php | 13 + .../CuyZ/Valinor/Type/Parser/LexingParser.php | 24 + .../CuyZ/Valinor/Type/Parser/TypeParser.php | 17 + lib/Vendor/CuyZ/Valinor/Type/ScalarType.php | 17 + lib/Vendor/CuyZ/Valinor/Type/StringType.php | 11 + lib/Vendor/CuyZ/Valinor/Type/Type.php | 21 + .../CuyZ/Valinor/Type/Types/ArrayKeyType.php | 201 +++++ .../CuyZ/Valinor/Type/Types/ArrayType.php | 139 ++++ .../Valinor/Type/Types/BooleanValueType.php | 102 +++ .../CuyZ/Valinor/Type/Types/CallableType.php | 48 ++ .../Valinor/Type/Types/ClassStringType.php | 184 +++++ .../CuyZ/Valinor/Type/Types/EnumType.php | 148 ++++ .../Types/Exception/ForbiddenMixedType.php | 20 + .../Exception/InvalidUnionOfClassString.php | 21 + .../Factory/CannotBuildTypeFromValue.php | 23 + .../Type/Types/Factory/ValueTypeFactory.php | 78 ++ .../Valinor/Type/Types/FloatValueType.php | 80 ++ .../Valinor/Type/Types/IntegerRangeType.php | 140 ++++ .../Valinor/Type/Types/IntegerValueType.php | 86 ++ .../CuyZ/Valinor/Type/Types/InterfaceType.php | 89 +++ .../Valinor/Type/Types/IntersectionType.php | 117 +++ .../CuyZ/Valinor/Type/Types/IterableType.php | 143 ++++ .../CuyZ/Valinor/Type/Types/ListType.php | 131 +++ .../CuyZ/Valinor/Type/Types/MixedType.php | 41 + .../Valinor/Type/Types/NativeBooleanType.php | 80 ++ .../Valinor/Type/Types/NativeClassType.php | 92 +++ .../Valinor/Type/Types/NativeFloatType.php | 71 ++ .../Valinor/Type/Types/NativeIntegerType.php | 78 ++ .../Valinor/Type/Types/NativeStringType.php | 74 ++ .../Type/Types/NegativeIntegerType.php | 75 ++ .../Valinor/Type/Types/NonEmptyArrayType.php | 138 ++++ .../Valinor/Type/Types/NonEmptyListType.php | 141 ++++ .../Valinor/Type/Types/NonEmptyStringType.php | 74 ++ .../Type/Types/NonNegativeIntegerType.php | 77 ++ .../Type/Types/NonPositiveIntegerType.php | 70 ++ .../CuyZ/Valinor/Type/Types/NullType.php | 46 ++ .../Valinor/Type/Types/NumericStringType.php | 78 ++ .../Type/Types/PositiveIntegerType.php | 81 ++ .../Valinor/Type/Types/ScalarConcreteType.php | 87 ++ .../Valinor/Type/Types/ShapedArrayElement.php | 39 + .../Valinor/Type/Types/ShapedArrayType.php | 265 +++++++ .../Valinor/Type/Types/StringValueType.php | 108 +++ .../Type/Types/UndefinedObjectType.php | 51 ++ .../CuyZ/Valinor/Type/Types/UnionType.php | 143 ++++ .../Valinor/Type/Types/UnresolvableType.php | 143 ++++ .../CuyZ/Valinor/Utility/IsSingleton.php | 16 + lib/Vendor/CuyZ/Valinor/Utility/Package.php | 19 + lib/Vendor/CuyZ/Valinor/Utility/Polyfill.php | 25 + .../Valinor/Utility/Priority/HasPriority.php | 19 + .../Utility/Priority/PrioritizedList.php | 51 ++ .../Utility/Reflection/NamespaceFinder.php | 34 + .../Valinor/Utility/Reflection/PhpParser.php | 100 +++ .../Valinor/Utility/Reflection/Reflection.php | 69 ++ .../Utility/Reflection/TokenParser.php | 132 ++++ .../Valinor/Utility/String/StringCutter.php | 51 ++ .../Utility/String/StringFormatter.php | 74 ++ .../Utility/String/StringFormatterError.php | 19 + .../CuyZ/Valinor/Utility/TypeHelper.php | 88 +++ .../CuyZ/Valinor/Utility/ValueDumper.php | 146 ++++ .../Firebase/JWT/BeforeValidException.php | 18 + lib/Vendor/Firebase/JWT/CachedKeySet.php | 275 +++++++ lib/Vendor/Firebase/JWT/ExpiredException.php | 30 + lib/Vendor/Firebase/JWT/JWK.php | 363 +++++++++ lib/Vendor/Firebase/JWT/JWT.php | 745 ++++++++++++++++++ .../JWT/JWTExceptionWithPayloadInterface.php | 20 + lib/Vendor/Firebase/JWT/Key.php | 54 ++ .../JWT/SignatureInvalidException.php | 7 + lib/Vendor/Psr/SimpleCache/CacheException.php | 10 + lib/Vendor/Psr/SimpleCache/CacheInterface.php | 114 +++ .../SimpleCache/InvalidArgumentException.php | 13 + lib/Vendor/README-f7.md | 6 + 439 files changed, 24952 insertions(+) create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/ChainCache.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/Exception/CacheDirectoryNotWritable.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/Exception/CompiledPhpCacheFileNotWritten.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/Exception/CorruptedCompiledPhpCacheFile.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/Exception/InvalidSignatureToWarmup.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/FileSystemCache.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/FileWatchingCache.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/KeySanitizerCache.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/RuntimeCache.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/Warmup/RecursiveCacheWarmupService.php create mode 100644 lib/Vendor/CuyZ/Valinor/Cache/WarmupCache.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Compiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Library/NewAttributeNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Library/TypeAcceptNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/AnonymousClassNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayKeyAccessNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/AssignNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/CallNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/CastNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ClassNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/CloneNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ClosureNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ComplianceNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/DifferentNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/EqualsNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ExpressionNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ForEachNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionCallNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionNameNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterOrEqualsToNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterThanNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/IfNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/InstanceOfNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/LessOrEqualsToNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/LessThanNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalAndNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalOrNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/MatchNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/MethodCallNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/MethodNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/NegateNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/NewClassNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ParameterDeclarationNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/PhpFileNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyDeclarationNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ReturnNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ShortClosureNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticAccessNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticMethodCallNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/TernaryNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ThrowNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/ValueNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableAccessNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/WrapNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Native/YieldNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Compiler/Node.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/AttributeDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Attributes.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/ClassDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/ClassTypeAliasesDuplication.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/ExtendTagTypeError.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagClassName.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClass.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClassType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/SeveralExtendTagsFound.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Exception/UnknownTypeAliasImport.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/FunctionDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/FunctionObject.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/FunctionsContainer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/MethodDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Methods.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/ParameterDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Parameters.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Properties.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/PropertyDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/AttributesRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheClassDefinitionRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheFunctionDefinitionRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/AttributesCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/Exception/TypeCannotBeCompiled.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/FunctionDefinitionCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/MethodDefinitionCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ParameterDefinitionCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/PropertyDefinitionCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/TypeCompiler.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/ClassDefinitionRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/FunctionDefinitionRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionAttributesRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php create mode 100644 lib/Vendor/CuyZ/Valinor/Library/Container.php create mode 100644 lib/Vendor/CuyZ/Valinor/Library/Settings.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapper.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapperError.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Exception/InvalidMappingTypeSignature.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringArgumentsMapping.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringMapping.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/MappingError.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Argument.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Arguments.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/ArgumentsValues.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Constructor.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/DateTimeFormatConstructor.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/DynamicConstructor.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotFindObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotInstantiateObject.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotParseToDateTime.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorClassTypeParameter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorMethodWithAttributeReturnType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorReturnType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/MissingConstructorClassTypeParameter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/ObjectBuildersCollision.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/PermissiveTypeNotAllowed.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/CacheObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/SortingObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/StrictTypesObjectBuilderFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/FunctionObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodArguments.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeConstructorObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeEnumObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/ObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Object/ReflectionObjectBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/FileExtensionNotHandled.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/InvalidJson.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/InvalidSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/InvalidYaml.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/SourceNotIterable.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/UnableToReadFile.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/YamlExtensionNotEnabled.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/FileSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/IdentifiableSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/JsonSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/CamelCaseKeys.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/Mapping.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/PathMapping.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/Source.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Source/YamlSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ArrayNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/InterfaceNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ListNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/MixedNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/NodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/NullNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectImplementations.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/RootNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ScalarNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TreeNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TypeNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UndefinedObjectNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UnionNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotInferFinalClass.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotMapToPermissiveType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotResolveObjectType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotResolveTypeFromUnion.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CircularDependencyDetected.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidAbstractObjectName.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidIterableKeyType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidListKey.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeHasNoMappedValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidResolvedImplementationValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidTraversableKey.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingNodeValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingObjectImplementationRegistration.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationCallbackError.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationNotRegistered.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ResolvedImplementationIsNotAccepted.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceIsNotNull.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceMustBeIterable.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceValueWasNotFilled.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/TooManyResolvedTypesFromUnion.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnexpectedKeysInSource.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnresolvableShellType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/DefaultMessage.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/ErrorMessage.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/AggregateMessageFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/CallbackMessageFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/LocaleMessageFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/MessageFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/MessageMapFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/TranslationMessageFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/HasCode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/HasParameters.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Message.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/MessageBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Messages.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/NodeMessage.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/UserlandError.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Node.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/NodeTraverser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/Tree/Shell.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/TreeMapper.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/TypeArgumentsMapper.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapper.php create mode 100644 lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapperError.php create mode 100644 lib/Vendor/CuyZ/Valinor/MapperBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/ArrayNormalizer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/AsTransformer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/CircularReferenceFoundDuringNormalization.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/KeyTransformerHasTooManyParameters.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/KeyTransformerParameterInvalidType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasInvalidCallableParameter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasNoParameter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasTooManyParameters.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TypeUnhandledByNormalizer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Format.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Formatter/Exception/CannotFormatInvalidTypeToJson.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Formatter/JsonFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/JsonNormalizer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Normalizer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/StreamNormalizer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/CacheTransformer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinition.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinitionBuilder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerRootNode.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ClassFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeZoneFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/EnumFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/InterfaceFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/MixedFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/NullFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/RegisteredTransformersFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ScalarFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ShapedArrayFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/StdClassFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TraversableFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TypeFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnionFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnitEnumFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnsureTypeFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/EmptyObject.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/EvaluatedTransformer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/RecursiveTransformer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Transformer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/TransformerContainer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/BooleanType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/ClassType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/CombiningType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/CompositeTraversableType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/CompositeType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/FixedType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/FloatType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/GenericType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/IntegerType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/ObjectType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/CachedParser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Constant/MissingClassConstantCase.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Enum/EnumCaseNotFound.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Enum/MissingEnumCase.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Enum/MissingSpecificEnumCase.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/AssignedGenericNotFound.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/CannotAssignGeneric.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericCommaMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/InvalidAssignedGeneric.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/MissingGenerics.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidIntersectionType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ArrayClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ArrayCommaMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidArrayKey.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidIterableKey.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableCommaMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ListClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayColonTokenMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayCommaMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayElementDuplicatedKey.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayElementTypeMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayEmptyElements.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayInvalidUnsealedType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayUnexpectedTokenAfterSealedType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayWithoutElementsWithSealedType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/SimpleArrayClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfIncorrectSubType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfOpeningBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/MissingClosingQuoteChar.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/RightIntersectionTypeMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/RightUnionTypeMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ClassStringClosingBracketMissing.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMaxValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMinValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingClosingBracket.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingComma.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMaxValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMinValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/InvalidClassStringSubType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ReversedValuesForIntegerRange.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/SameValueForIntegerRange.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Template/DuplicatedTemplateName.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Template/InvalidClassTemplate.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/UnknownSymbol.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/LexingTypeParserFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/AliasSpecification.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/ClassContextSpecification.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeParserSpecification.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/TypeParserFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/GenericCheckerParser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Annotations.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/NativeLexer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/SpecificationsLexer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ArrayToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/CallableToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/CaseFinder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassNameToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassStringToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingBracketToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingCurlyBracketToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingSquareBracketToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ColonToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/CommaToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/DoubleColonToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/EnumNameToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/FloatValueToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerValueToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntersectionToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IterableToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/LeftTraversingToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ListToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/NullableToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ObjectToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/OpeningBracketToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/OpeningCurlyBracketToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/OpeningSquareBracketToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/StringValueToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/Token.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/TraversingToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/TripleDotsToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/TypeToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/UnionToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/VacantToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ValueOfToken.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenStream.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenizedAnnotation.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokensExtractor.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TypeLexer.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/LexingParser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Parser/TypeParser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/ScalarType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/StringType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Type.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ArrayKeyType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ArrayType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/BooleanValueType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/CallableType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ClassStringType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/EnumType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/Exception/ForbiddenMixedType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/Exception/InvalidUnionOfClassString.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/Factory/CannotBuildTypeFromValue.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/Factory/ValueTypeFactory.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/FloatValueType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/IntegerRangeType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/IntegerValueType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/InterfaceType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/IntersectionType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/IterableType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ListType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/MixedType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NativeBooleanType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NativeClassType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NativeFloatType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NativeIntegerType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NativeStringType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NegativeIntegerType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyArrayType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyListType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyStringType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NonNegativeIntegerType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NonPositiveIntegerType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NullType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/NumericStringType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/PositiveIntegerType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ScalarConcreteType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayElement.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/StringValueType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/UndefinedObjectType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/UnionType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Type/Types/UnresolvableType.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/IsSingleton.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Package.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Polyfill.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Priority/HasPriority.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Priority/PrioritizedList.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Reflection/NamespaceFinder.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Reflection/PhpParser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Reflection/Reflection.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/Reflection/TokenParser.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/String/StringCutter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatter.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatterError.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/TypeHelper.php create mode 100644 lib/Vendor/CuyZ/Valinor/Utility/ValueDumper.php create mode 100644 lib/Vendor/Firebase/JWT/BeforeValidException.php create mode 100644 lib/Vendor/Firebase/JWT/CachedKeySet.php create mode 100644 lib/Vendor/Firebase/JWT/ExpiredException.php create mode 100644 lib/Vendor/Firebase/JWT/JWK.php create mode 100644 lib/Vendor/Firebase/JWT/JWT.php create mode 100644 lib/Vendor/Firebase/JWT/JWTExceptionWithPayloadInterface.php create mode 100644 lib/Vendor/Firebase/JWT/Key.php create mode 100644 lib/Vendor/Firebase/JWT/SignatureInvalidException.php create mode 100644 lib/Vendor/Psr/SimpleCache/CacheException.php create mode 100644 lib/Vendor/Psr/SimpleCache/CacheInterface.php create mode 100644 lib/Vendor/Psr/SimpleCache/InvalidArgumentException.php create mode 100644 lib/Vendor/README-f7.md diff --git a/lib/Vendor/CuyZ/Valinor/Cache/ChainCache.php b/lib/Vendor/CuyZ/Valinor/Cache/ChainCache.php new file mode 100644 index 0000000..ff5f686 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/ChainCache.php @@ -0,0 +1,136 @@ + + */ +final class ChainCache implements WarmupCache +{ + /** @var array> */ + private array $delegates; + + private int $count; + + /** + * @param CacheInterface ...$delegates + */ + public function __construct(CacheInterface ...$delegates) + { + $this->delegates = $delegates; + $this->count = count($delegates); + } + + public function warmup(): void + { + foreach ($this->delegates as $delegate) { + if ($delegate instanceof WarmupCache) { + $delegate->warmup(); + } + } + } + + public function get($key, $default = null): mixed + { + foreach ($this->delegates as $i => $delegate) { + $value = $delegate->get($key, $default); + + if (null !== $value) { + while (--$i >= 0) { + $this->delegates[$i]->set($key, $value); + } + + return $value; + } + } + + return $default; + } + + public function set($key, $value, $ttl = null): bool + { + $saved = true; + $i = $this->count; + + while ($i--) { + $saved = $this->delegates[$i]->set($key, $value, $ttl) && $saved; + } + + return $saved; + } + + public function delete($key): bool + { + $deleted = true; + $i = $this->count; + + while ($i--) { + $deleted = $this->delegates[$i]->delete($key) && $deleted; + } + + return $deleted; + } + + public function clear(): bool + { + $cleared = true; + $i = $this->count; + + while ($i--) { + $cleared = $this->delegates[$i]->clear() && $cleared; + } + + return $cleared; + } + + /** + * @return Traversable + */ + public function getMultiple($keys, $default = null): Traversable + { + foreach ($keys as $key) { + yield $key => $this->get($key, $default); + } + } + + public function setMultiple($values, $ttl = null): bool + { + $saved = true; + + foreach ($values as $key => $value) { + $saved = $this->set($key, $value, $ttl) && $saved; + } + + return $saved; + } + + public function deleteMultiple($keys): bool + { + $deleted = true; + + foreach ($keys as $key) { + $deleted = $this->delete($key) && $deleted; + } + + return $deleted; + } + + public function has($key): bool + { + foreach ($this->delegates as $cache) { + if ($cache->has($key)) { + return true; + } + } + + return false; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/Exception/CacheDirectoryNotWritable.php b/lib/Vendor/CuyZ/Valinor/Cache/Exception/CacheDirectoryNotWritable.php new file mode 100644 index 0000000..333729b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/Exception/CacheDirectoryNotWritable.php @@ -0,0 +1,19 @@ +getMessage()}", + 1653330261, + $exception + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/FileSystemCache.php b/lib/Vendor/CuyZ/Valinor/Cache/FileSystemCache.php new file mode 100644 index 0000000..e3d2b4b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/FileSystemCache.php @@ -0,0 +1,223 @@ + + */ +final class FileSystemCache implements WarmupCache +{ + private const TEMPORARY_DIR_PERMISSION = 510; + + private const GENERATED_MESSAGE = 'Generated by ' . self::class; + + private string $cacheDir; + + private ClassDefinitionCompiler $classDefinitionCompiler; + + private FunctionDefinitionCompiler $functionDefinitionCompiler; + + public function __construct(string $cacheDir) + { + $this->cacheDir = $cacheDir; + $this->classDefinitionCompiler = new ClassDefinitionCompiler(); + $this->functionDefinitionCompiler = new FunctionDefinitionCompiler(); + } + + public function warmup(): void + { + $this->createTemporaryDir(); + } + + public function has($key): bool + { + $filename = $this->path($key); + + return file_exists($filename); + } + + public function get($key, $default = null): mixed + { + $filename = $this->path($key); + + if (! file_exists($filename)) { + return $default; + } + + try { + return include $filename; + } catch (Error) { + throw new CorruptedCompiledPhpCacheFile($filename); + } + } + + public function set($key, $value, $ttl = null): bool + { + $filename = $this->path($key); + + $code = $this->compile($value); + + $tmpDir = $this->createTemporaryDir(); + + /** @infection-ignore-all */ + $tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16)); + + try { + if (! @file_put_contents($tmpFilename, $code)) { + throw new CompiledPhpCacheFileNotWritten($tmpFilename); + } + + if (! file_exists($filename) && ! @rename($tmpFilename, $filename)) { + throw new CompiledPhpCacheFileNotWritten($filename); + } + } finally { + if (file_exists($tmpFilename)) { + unlink($tmpFilename); + } + } + + return true; + } + + public function delete($key): bool + { + $filename = $this->path($key); + + if (file_exists($filename)) { + return @unlink($filename); + } + + return true; + } + + public function clear(): bool + { + if (! is_dir($this->cacheDir)) { + return true; + } + + $success = true; + $shouldDeleteRootDir = true; + + /** @var FilesystemIterator $file */ + foreach (new FilesystemIterator($this->cacheDir) as $file) { + if ($file->getFilename() === '.valinor.tmp') { + $success = @rmdir($this->cacheDir . DIRECTORY_SEPARATOR . $file->getFilename()) && $success; + continue; + } + + if (! $file->isFile()) { + $shouldDeleteRootDir = false; + continue; + } + + $line = $file->openFile()->getCurrentLine(); + + if (! $line || ! str_contains($line, self::GENERATED_MESSAGE)) { + $shouldDeleteRootDir = false; + continue; + } + + $success = @unlink($this->cacheDir . DIRECTORY_SEPARATOR . $file->getFilename()) && $success; + } + + if ($shouldDeleteRootDir) { + $success = @rmdir($this->cacheDir) && $success; + } + + return $success; + } + + /** + * @return Traversable + */ + public function getMultiple($keys, $default = null): Traversable + { + foreach ($keys as $key) { + yield $key => $this->get($key, $default); + } + } + + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value, $ttl); + } + + return true; + } + + public function deleteMultiple($keys): bool + { + $deleted = true; + + foreach ($keys as $key) { + $deleted = $this->delete($key) && $deleted; + } + + return $deleted; + } + + private function compile(mixed $value): string + { + $generatedMessage = self::GENERATED_MESSAGE; + + $code = match (true) { + $value instanceof ClassDefinition => $this->classDefinitionCompiler->compile($value), + $value instanceof FunctionDefinition => $this->functionDefinitionCompiler->compile($value), + $value instanceof EvaluatedTransformer => $value->code, + default => var_export($value, true), + }; + + return <<cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp'; + + if (! is_dir($tmpDir) && ! @mkdir($tmpDir, self::TEMPORARY_DIR_PERMISSION, true)) { + throw new CacheDirectoryNotWritable($this->cacheDir); + } + + return $tmpDir; + } + + private function path(string $key): string + { + /** @infection-ignore-all */ + return $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.php'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/FileWatchingCache.php b/lib/Vendor/CuyZ/Valinor/Cache/FileWatchingCache.php new file mode 100644 index 0000000..2956936 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/FileWatchingCache.php @@ -0,0 +1,159 @@ + + * @template EntryType + * @implements WarmupCache + */ +final class FileWatchingCache implements WarmupCache +{ + /** @var array */ + private array $timestamps = []; + + public function __construct( + /** @var CacheInterface */ + private CacheInterface $delegate + ) {} + + public function warmup(): void + { + if ($this->delegate instanceof WarmupCache) { + $this->delegate->warmup(); + } + } + + public function has($key): bool + { + foreach ($this->timestamps($key) as $fileName => $timestamp) { + if (! file_exists($fileName)) { + return false; + } + + if (filemtime($fileName) !== $timestamp) { + return false; + } + } + + return $this->delegate->has($key); + } + + public function get($key, $default = null): mixed + { + if (! $this->has($key)) { + return $default; + } + + return $this->delegate->get($key, $default); + } + + public function set($key, $value, $ttl = null): bool + { + $this->saveTimestamps($key, $value); + + return $this->delegate->set($key, $value, $ttl); + } + + public function delete($key): bool + { + return $this->delegate->delete($key); + } + + public function clear(): bool + { + $this->timestamps = []; + + return $this->delegate->clear(); + } + + public function getMultiple($keys, $default = null): iterable + { + return $this->delegate->getMultiple($keys, $default); + } + + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->saveTimestamps($key, $value); + } + + return $this->delegate->setMultiple($values, $ttl); + } + + public function deleteMultiple($keys): bool + { + return $this->delegate->deleteMultiple($keys); + } + + /** + * @return TimestampsArray + */ + private function timestamps(string $key): array + { + return $this->timestamps[$key] ??= $this->delegate->get("$key.timestamps", []); // @phpstan-ignore-line + } + + private function saveTimestamps(string $key, mixed $value): void + { + $this->timestamps[$key] = []; + + $fileNames = []; + + if ($value instanceof ClassDefinition) { + $reflection = Reflection::class($value->name); + + do { + $fileNames[] = $reflection->getFileName(); + } while ($reflection = $reflection->getParentClass()); + } + + if ($value instanceof FunctionDefinition) { + $fileNames[] = $value->fileName; + } + + foreach ($fileNames as $fileName) { + if (! is_string($fileName)) { + // @infection-ignore-all + continue; + } + + $time = @filemtime($fileName); + + // @infection-ignore-all + if (false === $time) { + continue; + } + + $this->timestamps[$key][$fileName] = $time; + } + + if (! empty($this->timestamps[$key])) { + $this->delegate->set("$key.timestamps", $this->timestamps[$key]); + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/KeySanitizerCache.php b/lib/Vendor/CuyZ/Valinor/Cache/KeySanitizerCache.php new file mode 100644 index 0000000..a789f8d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/KeySanitizerCache.php @@ -0,0 +1,113 @@ + + */ +final class KeySanitizerCache implements WarmupCache +{ + private static string $version; + + public function __construct( + /** @var CacheInterface */ + private CacheInterface $delegate, + private Settings $settings, + ) {} + + /** + * Two things: + * 1. We append the current version of the package to the cache key in order + * to avoid collisions between entries from different versions of the + * library. + * 2. The key is hashed so that it does not contain illegal characters. + * @see https://www.php-fig.org/psr/psr-16/#12-definitions + * + * @infection-ignore-all + */ + private function sanitize(string $key): string + { + self::$version ??= PHP_VERSION . '/' . Package::version(); + + $firstPart = strstr($key, "\0", before_needle: true); + + return $firstPart . hash('xxh128', $key . $this->settings->hash() . self::$version); + } + + public function warmup(): void + { + if ($this->delegate instanceof WarmupCache) { + $this->delegate->warmup(); + } + } + + public function get($key, $default = null): mixed + { + return $this->delegate->get($this->sanitize($key), $default); + } + + public function set($key, $value, $ttl = null): bool + { + return $this->delegate->set($this->sanitize($key), $value, $ttl); + } + + public function delete($key): bool + { + return $this->delegate->delete($this->sanitize($key)); + } + + public function clear(): bool + { + return $this->delegate->clear(); + } + + public function has($key): bool + { + return $this->delegate->has($this->sanitize($key)); + } + + /** + * @return Traversable + */ + public function getMultiple($keys, $default = null): Traversable + { + foreach ($keys as $key) { + yield $key => $this->delegate->get($this->sanitize($key), $default); + } + } + + public function setMultiple($values, $ttl = null): bool + { + $versionedValues = []; + + foreach ($values as $key => $value) { + $versionedValues[$this->sanitize($key)] = $value; + } + + return $this->delegate->setMultiple($versionedValues, $ttl); + } + + public function deleteMultiple($keys): bool + { + $transformedKeys = []; + + foreach ($keys as $key) { + $transformedKeys[] = $this->sanitize($key); + } + + return $this->delegate->deleteMultiple($transformedKeys); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/RuntimeCache.php b/lib/Vendor/CuyZ/Valinor/Cache/RuntimeCache.php new file mode 100644 index 0000000..69153a8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/RuntimeCache.php @@ -0,0 +1,91 @@ + + */ +final class RuntimeCache implements CacheInterface +{ + /** @var array */ + private array $entries = []; + + public function get($key, $default = null): mixed + { + return $this->entries[$key] ?? $default; + } + + public function set($key, $value, $ttl = null): bool + { + if ($value instanceof EvaluatedTransformer) { + return false; + } + + $this->entries[$key] = $value; + + return true; + } + + public function delete($key): bool + { + unset($this->entries[$key]); + + return true; + } + + public function clear(): bool + { + $this->entries = []; + + return true; + } + + public function getMultiple($keys, $default = null): iterable + { + $entries = []; + + foreach ($keys as $key) { + $entries[$key] = $this->get($key, $default); + } + + return $entries; + } + + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value, $ttl); + } + + return true; + } + + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + public function has($key): bool + { + return isset($this->entries[$key]); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/Warmup/RecursiveCacheWarmupService.php b/lib/Vendor/CuyZ/Valinor/Cache/Warmup/RecursiveCacheWarmupService.php new file mode 100644 index 0000000..ac6a6b8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/Warmup/RecursiveCacheWarmupService.php @@ -0,0 +1,109 @@ + */ + private array $classesWarmedUp = []; + + private bool $warmupWasDone = false; + + public function __construct( + private TypeParser $parser, + /** @var CacheInterface */ + private CacheInterface $cache, + private ObjectImplementations $implementations, + private ClassDefinitionRepository $classDefinitionRepository, + private ObjectBuilderFactory $objectBuilderFactory + ) {} + + public function warmup(string ...$signatures): void + { + if (! $this->warmupWasDone) { + $this->warmupWasDone = true; + + if ($this->cache instanceof WarmupCache) { + $this->cache->warmup(); + } + } + + foreach ($signatures as $signature) { + try { + $this->warmupType($this->parser->parse($signature)); + } catch (InvalidType $exception) { + throw new InvalidSignatureToWarmup($signature, $exception); + } + } + } + + private function warmupType(Type $type): void + { + if ($type instanceof InterfaceType) { + $this->warmupInterfaceType($type); + } + + if ($type instanceof ClassType) { + $this->warmupClassType($type); + } + + if ($type instanceof CompositeType) { + foreach ($type->traverse() as $subType) { + $this->warmupType($subType); + } + } + } + + private function warmupInterfaceType(InterfaceType $type): void + { + $interfaceName = $type->className(); + + if (! $this->implementations->has($interfaceName)) { + return; + } + + $function = $this->implementations->function($interfaceName); + + $this->warmupType($function->returnType); + + foreach ($function->parameters as $parameter) { + $this->warmupType($parameter->type); + } + } + + private function warmupClassType(ClassType $type): void + { + if (in_array($type->className(), $this->classesWarmedUp, true)) { + return; + } + + $this->classesWarmedUp[] = $type->className(); + + $classDefinition = $this->classDefinitionRepository->for($type); + $objectBuilders = $this->objectBuilderFactory->for($classDefinition); + + foreach ($objectBuilders as $builder) { + foreach ($builder->describeArguments() as $argument) { + $this->warmupType($argument->type()); + } + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Cache/WarmupCache.php b/lib/Vendor/CuyZ/Valinor/Cache/WarmupCache.php new file mode 100644 index 0000000..cdd991e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Cache/WarmupCache.php @@ -0,0 +1,16 @@ + + */ +interface WarmupCache extends CacheInterface +{ + public function warmup(): void; +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Compiler.php b/lib/Vendor/CuyZ/Valinor/Compiler/Compiler.php new file mode 100644 index 0000000..1efd0dd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Compiler.php @@ -0,0 +1,60 @@ +compile($compiler); + + if ($nodes !== []) { + $compiler = $compiler->write("\n"); + } + } + + return $compiler; + } + + public function sub(): self + { + return new self(); + } + + public function write(string $code): self + { + $self = clone $this; + $self->code .= $code; + + return $self; + } + + public function indent(): self + { + $self = clone $this; + $self->indentation++; + + return $self; + } + + public function code(): string + { + $indent = str_repeat(' ', $this->indentation); + + return $indent . str_replace("\n", "\n" . $indent, $this->code); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Library/NewAttributeNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Library/NewAttributeNode.php new file mode 100644 index 0000000..9b16a50 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Library/NewAttributeNode.php @@ -0,0 +1,53 @@ +attribute->arguments); + + return $compiler->compile( + Node::newClass( + $this->attribute->class->name, + ...$argumentNodes, + ), + ); + } + + /** + * @param array $arguments + * @return array + */ + private static function argumentNode(array $arguments): array + { + return array_map(static function (mixed $argument) { + if (is_object($argument)) { + return Node::functionCall( + name: 'unserialize', + arguments: [Node::value(serialize($argument))], + ); + } + + if (is_array($argument)) { + return Node::array(self::argumentNode($argument)); + } + + /** @var scalar $argument */ + return Node::value($argument); + }, $arguments); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Library/TypeAcceptNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Library/TypeAcceptNode.php new file mode 100644 index 0000000..d00a9a9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Library/TypeAcceptNode.php @@ -0,0 +1,24 @@ +compile($this->type->compiledAccept($this->node)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/AnonymousClassNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/AnonymousClassNode.php new file mode 100644 index 0000000..80cd406 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/AnonymousClassNode.php @@ -0,0 +1,105 @@ + */ + private array $arguments = []; + + /** @var array */ + private array $interfaces = []; + + /** @var array */ + private array $properties = []; + + /** @var array */ + private array $methods = []; + + public function withArguments(Node ...$arguments): self + { + $self = clone $this; + $self->arguments = $arguments; + + return $self; + } + + /** + * @param interface-string ...$interfaces + */ + public function implements(string ...$interfaces): self + { + $self = clone $this; + $self->interfaces = $interfaces; + + return $self; + } + + public function withProperties(PropertyDeclarationNode ...$properties): self + { + $self = clone $this; + $self->properties = $properties; + + return $self; + } + + public function withMethods(MethodNode ...$methods): self + { + $self = clone $this; + + foreach ($methods as $method) { + $self->methods[$method->name()] = $method; + } + + return $self; + } + + public function hasMethod(string $name): bool + { + return isset($this->methods[$name]); + } + + public function compile(Compiler $compiler): Compiler + { + $arguments = implode(', ', array_map( + fn (Node $argument) => $compiler->sub()->compile($argument)->code(), + $this->arguments, + )); + + $compiler = $compiler->write("new class ($arguments)"); + + if ($this->interfaces !== []) { + $compiler = $compiler->write( + ' implements ' . implode(', ', $this->interfaces), + ); + } + + $body = [ + ...array_map( + fn (PropertyDeclarationNode $property) => $compiler->sub()->indent()->compile($property)->code(), + $this->properties, + ), + ...array_map( + fn (MethodNode $method) => $compiler->sub()->indent()->compile($method)->code(), + $this->methods, + ), + ]; + + $compiler = $compiler->write(' {'); + + if ($body !== []) { + $compiler = $compiler->write(PHP_EOL . implode(PHP_EOL . PHP_EOL, $body) . PHP_EOL); + } + + return $compiler->write('}'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayKeyAccessNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayKeyAccessNode.php new file mode 100644 index 0000000..3280be4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayKeyAccessNode.php @@ -0,0 +1,26 @@ +sub()->compile($this->key)->code(); + + return $compiler + ->compile($this->node) + ->write('[' . $key . ']'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayNode.php new file mode 100644 index 0000000..5ce001d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ArrayNode.php @@ -0,0 +1,36 @@ + */ + private array $assignments + ) {} + + public function compile(Compiler $compiler): Compiler + { + if ($this->assignments === []) { + return $compiler->write('[]'); + } + + $sub = []; + + foreach ($this->assignments as $key => $assignment) { + $sub[] = ' ' . var_export($key, true) . ' => ' . $compiler->sub()->compile($assignment)->code() . ","; + } + + $sub = implode("\n", $sub); + + return $compiler->write("[\n$sub\n]"); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/AssignNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/AssignNode.php new file mode 100644 index 0000000..179931f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/AssignNode.php @@ -0,0 +1,25 @@ +compile($this->node) + ->write(' = ') + ->compile($this->value); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/CallNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/CallNode.php new file mode 100644 index 0000000..9a983d4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/CallNode.php @@ -0,0 +1,39 @@ + */ + private array $arguments = [], + ) {} + + public function compile(Compiler $compiler): Compiler + { + $compiler = $compiler + ->compile($this->node) + ->write('('); + + if ($this->arguments !== []) { + $arguments = array_map( + fn (Node $argument) => $compiler->sub()->compile($argument)->code(), + $this->arguments, + ); + + $compiler = $compiler->write(implode(', ', $arguments)); + } + + return $compiler->write(')'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/CastNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/CastNode.php new file mode 100644 index 0000000..c0f4bca --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/CastNode.php @@ -0,0 +1,29 @@ +write('(' . $this->type . ')') + ->compile($this->node); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ClassNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ClassNode.php new file mode 100644 index 0000000..cb28532 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ClassNode.php @@ -0,0 +1,33 @@ + $arguments + */ + public function callStaticMethod( + string $method, + array $arguments = [], + ): Node { + return new StaticMethodCallNode($this, $method, $arguments); + } + + public function compile(Compiler $compiler): Compiler + { + return $compiler->write($this->name); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/CloneNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/CloneNode.php new file mode 100644 index 0000000..39de6ff --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/CloneNode.php @@ -0,0 +1,23 @@ +write('clone ') + ->compile($this->node); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ClosureNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ClosureNode.php new file mode 100644 index 0000000..e269aba --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ClosureNode.php @@ -0,0 +1,53 @@ + */ + private array $use = []; + + /** @var array */ + private array $nodes; + + public function __construct(Node ...$nodes) + { + $this->nodes = $nodes; + } + + /** + * @no-named-arguments + * @param non-empty-string ...$names + */ + public function uses(string ...$names): self + { + $self = clone $this; + $self->use = array_map(fn (string $name) => '$' . $name, $names); + + return $self; + } + + public function compile(Compiler $compiler): Compiler + { + $use = $this->use !== [] ? ' use (' . implode(', ', $this->use) . ')' : ''; + + $body = $compiler->sub()->indent()->compile(...$this->nodes)->code(); + + $code = <<write($code); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ComplianceNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ComplianceNode.php new file mode 100644 index 0000000..11ab1c4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ComplianceNode.php @@ -0,0 +1,118 @@ + $arguments + */ + public function call(array $arguments = []): self + { + return new self(new CallNode($this, $arguments)); + } + + /** + * @param non-empty-string $method + * @param array $arguments + */ + public function callMethod(string $method, array $arguments = []): self + { + return new self(new MethodCallNode($this, $method, $arguments)); + } + + public function different(Node $right): self + { + return new self(new DifferentNode($this, $right)); + } + + public function equals(Node $right): self + { + return new self(new EqualsNode($this, $right)); + } + + /** + * @no-named-arguments + */ + public function or(Node ...$nodes): self + { + return new self(new LogicalOrNode($this, ...$nodes)); + } + + /** + * @param class-string $className + */ + public function instanceOf(string $className): self + { + return new self(new InstanceOfNode($this, $className)); + } + + public function isLessThan(Node $right): self + { + return new self(new LessThanNode($this, $right)); + } + + public function isLessOrEqualsTo(Node $right): self + { + return new self(new LessOrEqualsToNode($this, $right)); + } + + public function isGreaterThan(Node $right): self + { + return new self(new GreaterThanNode($this, $right)); + } + + public function isGreaterOrEqualsTo(Node $right): self + { + return new self(new GreaterOrEqualsToNode($this, $right)); + } + + public function compile(Compiler $compiler): Compiler + { + return $compiler->compile($this->node); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/DifferentNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/DifferentNode.php new file mode 100644 index 0000000..fba16e0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/DifferentNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write(' !== ') + ->compile($this->right); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/EqualsNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/EqualsNode.php new file mode 100644 index 0000000..69b9e1f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/EqualsNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write(' === ') + ->compile($this->right); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ExpressionNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ExpressionNode.php new file mode 100644 index 0000000..c6e7080 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ExpressionNode.php @@ -0,0 +1,19 @@ +write($compiler->sub()->compile($this->node)->code() . ';'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ForEachNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ForEachNode.php new file mode 100644 index 0000000..fc0b7bc --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ForEachNode.php @@ -0,0 +1,35 @@ +sub()->compile($this->value)->code(); + $body = $compiler->sub()->indent()->compile($this->body)->code(); + + return $compiler->write( + <<key => $$this->item) { + $body + } + PHP + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionCallNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionCallNode.php new file mode 100644 index 0000000..c950e94 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionCallNode.php @@ -0,0 +1,25 @@ + */ + private array $arguments = [], + ) {} + + public function compile(Compiler $compiler): Compiler + { + return $compiler + ->compile(new CallNode(new FunctionNameNode($this->name), $this->arguments)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionNameNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionNameNode.php new file mode 100644 index 0000000..3a8fe3e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/FunctionNameNode.php @@ -0,0 +1,32 @@ +name, self::RESERVED_FUNCTIONS, true) + ? $this->name + : '\\' . $this->name; + + return $compiler->write($function); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterOrEqualsToNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterOrEqualsToNode.php new file mode 100644 index 0000000..27ad09f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterOrEqualsToNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write(' >= ') + ->compile($this->right); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterThanNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterThanNode.php new file mode 100644 index 0000000..c9fa88a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/GreaterThanNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write(' > ') + ->compile($this->right); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/IfNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/IfNode.php new file mode 100644 index 0000000..11d6146 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/IfNode.php @@ -0,0 +1,31 @@ +sub()->compile($this->condition)->code(); + $body = $compiler->sub()->indent()->compile($this->body)->code(); + + return $compiler->write( + <<className; + + return $compiler + ->compile($this->node) + ->write(' instanceof ') + ->write($className); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/LessOrEqualsToNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LessOrEqualsToNode.php new file mode 100644 index 0000000..77d3802 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LessOrEqualsToNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write(' <= ') + ->compile($this->right); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/LessThanNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LessThanNode.php new file mode 100644 index 0000000..3d3b008 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LessThanNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write(' < ') + ->compile($this->right); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalAndNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalAndNode.php new file mode 100644 index 0000000..65a0c74 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalAndNode.php @@ -0,0 +1,38 @@ + */ + private array $nodes; + + /** + * @no-named-arguments + */ + public function __construct(Node ...$nodes) + { + $this->nodes = $nodes; + } + + public function compile(Compiler $compiler): Compiler + { + $nodes = $this->nodes; + + while ($node = array_shift($nodes)) { + $compiler = $compiler->compile($node); + + if ($nodes !== []) { + $compiler = $compiler->write(' && '); + } + } + + return $compiler; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalOrNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalOrNode.php new file mode 100644 index 0000000..53ace4d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/LogicalOrNode.php @@ -0,0 +1,38 @@ + */ + private array $nodes; + + /** + * @no-named-arguments + */ + public function __construct(Node ...$nodes) + { + $this->nodes = $nodes; + } + + public function compile(Compiler $compiler): Compiler + { + $nodes = $this->nodes; + + while ($node = array_shift($nodes)) { + $compiler = $compiler->compile($node); + + if ($nodes !== []) { + $compiler = $compiler->write(' || '); + } + } + + return $compiler; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/MatchNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/MatchNode.php new file mode 100644 index 0000000..45e373b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/MatchNode.php @@ -0,0 +1,65 @@ + */ + private array $cases = []; + + private Node $defaultCase; + + public function __construct(private Node $value) {} + + public function withCase(Node $condition, Node $body): self + { + $self = clone $this; + $self->cases[] = ['condition' => $condition, 'body' => $body]; + + return $self; + } + + public function withDefaultCase(Node $defaultCase): self + { + $self = clone $this; + $self->defaultCase = $defaultCase; + + return $self; + } + + public function compile(Compiler $compiler): Compiler + { + $value = $compiler->sub()->compile($this->value)->code(); + $body = []; + + foreach ($this->cases as $case) { + $body[] = $compiler->sub()->indent()->compile($case['condition'])->code() . + ' => ' . + $compiler->sub()->compile($case['body'])->code() . + ','; + } + + if (isset($this->defaultCase)) { + $body[] = $compiler->sub()->indent()->write('default')->code() . + ' => ' . + $compiler->sub()->compile($this->defaultCase)->code() . + ','; + } + + $body = implode("\n", $body); + + return $compiler->write( + << */ + private array $arguments = [], + ) {} + + public function compile(Compiler $compiler): Compiler + { + return $compiler->compile( + new CallNode(new VariableAccessNode($this->node, $this->method), $this->arguments) + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/MethodNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/MethodNode.php new file mode 100644 index 0000000..d666b16 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/MethodNode.php @@ -0,0 +1,109 @@ + */ + private array $parameters = []; + + /** @var array */ + private array $nodes = []; + + /** + * @param non-empty-string $name + */ + public function __construct(string $name) + { + $this->name = $name; + } + + public static function constructor(): self + { + return new self('__construct'); + } + + /** + * @return non-empty-string + */ + public function name(): string + { + return $this->name; + } + + public function witParameters(ParameterDeclarationNode ...$parameters): self + { + $self = clone $this; + $self->parameters = $parameters; + + return $self; + } + + /** + * @param 'public'|'private' $visibility + */ + public function withVisibility(string $visibility): self + { + $self = clone $this; + $self->visibility = $visibility; + + return $self; + } + + /** + * @param non-empty-string $type + */ + public function withReturnType(string $type): self + { + $self = clone $this; + $self->returnType = $type; + + return $self; + } + + public function withBody(Node ...$nodes): self + { + $self = clone $this; + $self->nodes = $nodes; + + return $self; + } + + public function compile(Compiler $compiler): Compiler + { + $parameters = implode(', ', array_map( + fn (ParameterDeclarationNode $parameter) => $compiler->sub()->compile($parameter)->code(), + $this->parameters, + )); + + $compiler = $compiler->write("$this->visibility function $this->name($parameters)"); + + if ($this->name !== '__construct') { + $compiler = $compiler->write(': ' . ($this->returnType ?? 'void')); + } + + if ($this->nodes === []) { + return $compiler->write(' {}'); + } + + $body = $compiler->sub()->indent()->compile(...$this->nodes)->code(); + + return $compiler->write(PHP_EOL . '{' . PHP_EOL . $body . PHP_EOL . '}'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/NegateNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/NegateNode.php new file mode 100644 index 0000000..7143694 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/NegateNode.php @@ -0,0 +1,19 @@ +write('! ')->compile($this->node); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/NewClassNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/NewClassNode.php new file mode 100644 index 0000000..208ec9a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/NewClassNode.php @@ -0,0 +1,40 @@ + */ + private array $arguments; + + /** + * @param class-string $className + */ + public function __construct(string $className, Node ...$arguments) + { + $this->className = $className; + $this->arguments = $arguments; + } + + public function compile(Compiler $compiler): Compiler + { + $arguments = array_map( + fn (Node $argument) => $compiler->sub()->compile($argument)->code(), + $this->arguments, + ); + $arguments = implode(', ', $arguments); + + return $compiler->write("new {$this->className}($arguments)"); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ParameterDeclarationNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ParameterDeclarationNode.php new file mode 100644 index 0000000..ce77b00 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ParameterDeclarationNode.php @@ -0,0 +1,23 @@ +write($this->type . ' $' . $this->name); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/PhpFileNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/PhpFileNode.php new file mode 100644 index 0000000..1aee31d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/PhpFileNode.php @@ -0,0 +1,27 @@ + */ + private array $nodes; + + public function __construct(Node ...$nodes) + { + $this->nodes = $nodes; + } + + public function compile(Compiler $compiler): Compiler + { + return $compiler + ->write("compile(...$this->nodes); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyDeclarationNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyDeclarationNode.php new file mode 100644 index 0000000..76f0f4c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyDeclarationNode.php @@ -0,0 +1,23 @@ +write('private ' . $this->type . ' $' . $this->name . ';'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyNode.php new file mode 100644 index 0000000..766d23f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/PropertyNode.php @@ -0,0 +1,19 @@ +write('$this->' . $this->name); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ReturnNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ReturnNode.php new file mode 100644 index 0000000..813c992 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ReturnNode.php @@ -0,0 +1,22 @@ +node ? ' ' . $compiler->sub()->compile($this->node)->code() : ''; + + return $compiler->write("return$code;"); + } + +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ShortClosureNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ShortClosureNode.php new file mode 100644 index 0000000..8b67be8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ShortClosureNode.php @@ -0,0 +1,46 @@ + */ + private array $parameters = []; + + public function __construct(Node $returnNode) + { + $this->returnNode = $returnNode; + } + + public function witParameters(ParameterDeclarationNode ...$parameters): self + { + $self = clone $this; + $self->parameters = $parameters; + + return $self; + } + + public function compile(Compiler $compiler): Compiler + { + $parameters = implode(', ', array_map( + fn (ParameterDeclarationNode $parameter) => $compiler->sub()->compile($parameter)->code(), + $this->parameters, + )); + + $return = $compiler->sub()->compile($this->returnNode)->code(); + + return $compiler->write( + << $return + PHP, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticAccessNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticAccessNode.php new file mode 100644 index 0000000..21daf3f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticAccessNode.php @@ -0,0 +1,25 @@ +compile($this->left) + ->write("::$this->name"); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticMethodCallNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticMethodCallNode.php new file mode 100644 index 0000000..e74f9e6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/StaticMethodCallNode.php @@ -0,0 +1,27 @@ + */ + private array $arguments = [], + ) {} + + public function compile(Compiler $compiler): Compiler + { + return $compiler->compile( + new CallNode(new StaticAccessNode($this->node, $this->method), $this->arguments) + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/TernaryNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/TernaryNode.php new file mode 100644 index 0000000..fe52df3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/TernaryNode.php @@ -0,0 +1,28 @@ +compile($this->condition) + ->write(' ? ') + ->compile($this->ifTrue) + ->write(' : ') + ->compile($this->ifFalse); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ThrowNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ThrowNode.php new file mode 100644 index 0000000..f73b649 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ThrowNode.php @@ -0,0 +1,23 @@ +write('throw ') + ->compile($this->node); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/ValueNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ValueNode.php new file mode 100644 index 0000000..525c40b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/ValueNode.php @@ -0,0 +1,49 @@ +|bool|float|int|string|null */ + private array|bool|float|int|string|null $value, + ) {} + + public function compile(Compiler $compiler): Compiler + { + return $this->compileValue($this->value, $compiler); + } + + private function compileValue(mixed $value, Compiler $compiler): Compiler + { + if (is_array($value)) { + $compiler = $compiler->write('['); + + $i = 0; + $numItems = count($value); + foreach ($value as $key => $item) { + $compiler = $compiler->write(var_export($key, true) . ' => '); + $compiler = $this->compileValue($item, $compiler); + + if (++$i !== $numItems) { + $compiler = $compiler->write(', '); + } + } + + $compiler = $compiler->write(']'); + } else { + $compiler = $compiler->write(var_export($value, true)); + } + + return $compiler; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableAccessNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableAccessNode.php new file mode 100644 index 0000000..f526e13 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableAccessNode.php @@ -0,0 +1,25 @@ +compile($this->node) + ->write('->' . $this->value); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableNode.php new file mode 100644 index 0000000..8f79bda --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/VariableNode.php @@ -0,0 +1,19 @@ +write('$' . $this->name); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/WrapNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/WrapNode.php new file mode 100644 index 0000000..2b9e797 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/WrapNode.php @@ -0,0 +1,22 @@ +write('(') + ->compile($this->node) + ->write(')'); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Native/YieldNode.php b/lib/Vendor/CuyZ/Valinor/Compiler/Native/YieldNode.php new file mode 100644 index 0000000..bb6d052 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Native/YieldNode.php @@ -0,0 +1,26 @@ +write('yield ') + ->compile($this->key) + ->write(' => ') + ->compile($this->value); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Compiler/Node.php b/lib/Vendor/CuyZ/Valinor/Compiler/Node.php new file mode 100644 index 0000000..8055704 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Compiler/Node.php @@ -0,0 +1,203 @@ + $assignments + */ + public static function array(array $assignments = []): ArrayNode + { + return new ArrayNode($assignments); + } + + public static function anonymousClass(): AnonymousClassNode + { + return new AnonymousClassNode(); + } + + /** + * @param class-string $name + */ + public static function class(string $name): ClassNode + { + return new ClassNode($name); + } + + public static function closure(Node ...$nodes): ClosureNode + { + return new ClosureNode(...$nodes); + } + + /** + * @param non-empty-string $key + * @param non-empty-string $item + */ + public static function forEach(Node $value, string $key, string $item, Node $body): ForEachNode + { + return new ForEachNode($value, $key, $item, $body); + } + + /** + * @param non-empty-string $name + * @param array $arguments + */ + public static function functionCall(string $name, array $arguments = []): ComplianceNode + { + return new ComplianceNode(new FunctionCallNode($name, $arguments)); + } + + public static function if(Node $condition, Node $body): IfNode + { + return new IfNode($condition, $body); + } + + /** + * @no-named-arguments + */ + public static function logicalAnd(Node ...$nodes): ComplianceNode + { + return new ComplianceNode(new LogicalAndNode(...$nodes)); + } + + /** + * @no-named-arguments + */ + public static function logicalOr(Node ...$nodes): ComplianceNode + { + return new ComplianceNode(new LogicalOrNode(...$nodes)); + } + + public static function match(Node $value): MatchNode + { + return new MatchNode($value); + } + + /** + * @param non-empty-string $name + */ + public static function method(string $name): MethodNode + { + return new MethodNode($name); + } + + public static function negate(Node $node): NegateNode + { + return new NegateNode($node); + } + + /** + * @param class-string $className + */ + public static function newClass(string $className, Node ...$arguments): NewClassNode + { + return new NewClassNode($className, ...$arguments); + } + + /** + * @param non-empty-string $name + */ + public static function parameterDeclaration(string $name, string $type): ParameterDeclarationNode + { + return new ParameterDeclarationNode($name, $type); + } + + public static function property(string $name): ComplianceNode + { + return new ComplianceNode(new PropertyNode($name)); + } + + /** + * @param non-empty-string $name + */ + public static function propertyDeclaration(string $name, string $type): PropertyDeclarationNode + { + return new PropertyDeclarationNode($name, $type); + } + + public static function return(Node $node): ReturnNode + { + return new ReturnNode($node); + } + + public static function shortClosure(Node $return): ShortClosureNode + { + return new ShortClosureNode($return); + } + + public static function ternary(Node $condition, Node $ifTrue, Node $ifFalse): TernaryNode + { + return new TernaryNode($condition, $ifTrue, $ifFalse); + } + + public static function this(): ComplianceNode + { + return self::variable('this'); + } + + public static function throw(Node $node): ThrowNode + { + return new ThrowNode($node); + } + + /** + * @param array|bool|float|int|string|null $value + */ + public static function value(array|bool|float|int|string|null $value): ComplianceNode + { + return new ComplianceNode(new ValueNode($value)); + } + + public static function variable(string $name): ComplianceNode + { + return new ComplianceNode(new VariableNode($name)); + } + + public static function yield(Node $key, Node $value): YieldNode + { + return new YieldNode($key, $value); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/AttributeDefinition.php b/lib/Vendor/CuyZ/Valinor/Definition/AttributeDefinition.php new file mode 100644 index 0000000..19a1d1f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/AttributeDefinition.php @@ -0,0 +1,20 @@ + */ + public readonly array $arguments, + ) {} + + public function instantiate(): object + { + return new ($this->class->type->className())(...$this->arguments); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Attributes.php b/lib/Vendor/CuyZ/Valinor/Definition/Attributes.php new file mode 100644 index 0000000..5b80fd6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Attributes.php @@ -0,0 +1,81 @@ + + */ +final class Attributes implements IteratorAggregate, Countable +{ + private static self $empty; + + /** @var list */ + private array $attributes; + + /** + * @no-named-arguments + */ + public function __construct(AttributeDefinition ...$attributes) + { + $this->attributes = $attributes; + } + + public static function empty(): self + { + return self::$empty ??= new self(); + } + + public function has(string $className): bool + { + foreach ($this->attributes as $attribute) { + if (is_a($attribute->class->type->className(), $className, true)) { + return true; + } + } + + return false; + } + + /** + * @param callable(AttributeDefinition): bool $callback + */ + public function filter(callable $callback): self + { + return new self( + ...array_filter($this->attributes, $callback) + ); + } + + public function count(): int + { + return count($this->attributes); + } + + /** + * @return list + */ + public function toArray(): array + { + return $this->attributes; + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield from $this->attributes; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/ClassDefinition.php b/lib/Vendor/CuyZ/Valinor/Definition/ClassDefinition.php new file mode 100644 index 0000000..2874e43 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/ClassDefinition.php @@ -0,0 +1,22 @@ + $reflection + */ + public function __construct(ReflectionClass $reflection, InvalidType $previous) + { + parent::__construct( + "The `@extends` tag of the class `$reflection->name` is not valid: {$previous->getMessage()}", + 1670193574, + $previous, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagClassName.php b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagClassName.php new file mode 100644 index 0000000..d7900f4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagClassName.php @@ -0,0 +1,27 @@ + $reflection + */ + public function __construct(ReflectionClass $reflection, Type $invalidExtendTag) + { + /** @var ReflectionClass $parentClass */ + $parentClass = $reflection->getParentClass(); + + parent::__construct( + "The `@extends` tag of the class `$reflection->name` has invalid class `{$invalidExtendTag->toString()}`, it should be `$parentClass->name`.", + 1670183564, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagType.php b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagType.php new file mode 100644 index 0000000..02846c2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidExtendTagType.php @@ -0,0 +1,27 @@ + $reflection + */ + public function __construct(ReflectionClass $reflection, Type $invalidExtendTag) + { + /** @var ReflectionClass $parentClass */ + $parentClass = $reflection->getParentClass(); + + parent::__construct( + "The `@extends` tag of the class `$reflection->name` has invalid type `{$invalidExtendTag->toString()}`, it should be `{$parentClass->name}`.", + 1670181134, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClass.php b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClass.php new file mode 100644 index 0000000..00b2f50 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClass.php @@ -0,0 +1,20 @@ +className()}`.", + 1638535486 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClassType.php b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClassType.php new file mode 100644 index 0000000..91195e8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Exception/InvalidTypeAliasImportClassType.php @@ -0,0 +1,21 @@ +toString()}` was given in class `{$classType->className()}`.", + 1638535608 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Exception/SeveralExtendTagsFound.php b/lib/Vendor/CuyZ/Valinor/Definition/Exception/SeveralExtendTagsFound.php new file mode 100644 index 0000000..1798d1e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Exception/SeveralExtendTagsFound.php @@ -0,0 +1,23 @@ + $reflection + */ + public function __construct(ReflectionClass $reflection) + { + parent::__construct( + "Only one `@extends` tag should be set for the class `$reflection->name`.", + 1670195494, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Exception/UnknownTypeAliasImport.php b/lib/Vendor/CuyZ/Valinor/Definition/Exception/UnknownTypeAliasImport.php new file mode 100644 index 0000000..db3717f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Exception/UnknownTypeAliasImport.php @@ -0,0 +1,23 @@ +className()}` could not be found in `$importClassName`", + 1638535757 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/FunctionDefinition.php b/lib/Vendor/CuyZ/Valinor/Definition/FunctionDefinition.php new file mode 100644 index 0000000..80206a4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/FunctionDefinition.php @@ -0,0 +1,27 @@ +definition = $definition; + $this->callback = $callback; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/FunctionsContainer.php b/lib/Vendor/CuyZ/Valinor/Definition/FunctionsContainer.php new file mode 100644 index 0000000..4701778 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/FunctionsContainer.php @@ -0,0 +1,60 @@ + + */ +final class FunctionsContainer implements IteratorAggregate, Countable +{ + /** @var array */ + private array $functions = []; + + public function __construct( + private FunctionDefinitionRepository $functionDefinitionRepository, + /** @var array */ + private array $callables + ) {} + + public function has(string|int $key): bool + { + return isset($this->callables[$key]); + } + + public function get(string|int $key): FunctionObject + { + return $this->function($key); + } + + public function getIterator(): Traversable + { + foreach (array_keys($this->callables) as $key) { + yield $key => $this->function($key); + } + } + + private function function(string|int $key): FunctionObject + { + return $this->functions[$key] ??= new FunctionObject( + $this->functionDefinitionRepository->for($this->callables[$key]), + $this->callables[$key] + ); + } + + public function count(): int + { + return count($this->callables); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/MethodDefinition.php b/lib/Vendor/CuyZ/Valinor/Definition/MethodDefinition.php new file mode 100644 index 0000000..4c4412d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/MethodDefinition.php @@ -0,0 +1,23 @@ + + */ +final class Methods implements IteratorAggregate, Countable +{ + /** @var MethodDefinition[] */ + private array $methods = []; + + public function __construct(MethodDefinition ...$methods) + { + foreach ($methods as $method) { + $this->methods[$method->name] = $method; + } + } + + public function has(string $name): bool + { + return isset($this->methods[$name]); + } + + public function get(string $name): MethodDefinition + { + return $this->methods[$name]; + } + + public function hasConstructor(): bool + { + return $this->has('__construct'); + } + + public function constructor(): MethodDefinition + { + return $this->get('__construct'); + } + + public function count(): int + { + return count($this->methods); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield from $this->methods; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/ParameterDefinition.php b/lib/Vendor/CuyZ/Valinor/Definition/ParameterDefinition.php new file mode 100644 index 0000000..510c8b3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/ParameterDefinition.php @@ -0,0 +1,24 @@ + + */ +final class Parameters implements IteratorAggregate, Countable +{ + /** @var ParameterDefinition[] */ + private array $parameters = []; + + public function __construct(ParameterDefinition ...$parameters) + { + foreach ($parameters as $parameter) { + $this->parameters[$parameter->name] = $parameter; + } + } + + public function has(string $name): bool + { + return isset($this->parameters[$name]); + } + + public function get(string $name): ParameterDefinition + { + return $this->parameters[$name]; + } + + /** + * @param int<0, max> $index + */ + public function at(int $index): ParameterDefinition + { + return array_values($this->parameters)[$index]; + } + + /** + * @return list + */ + public function toList(): array + { + return array_values($this->parameters); + } + + public function count(): int + { + return count($this->parameters); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield from $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Properties.php b/lib/Vendor/CuyZ/Valinor/Definition/Properties.php new file mode 100644 index 0000000..ab0598d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Properties.php @@ -0,0 +1,50 @@ + + */ +final class Properties implements IteratorAggregate, Countable +{ + /** @var PropertyDefinition[] */ + private array $properties = []; + + public function __construct(PropertyDefinition ...$properties) + { + foreach ($properties as $property) { + $this->properties[$property->name] = $property; + } + } + + public function has(string $name): bool + { + return isset($this->properties[$name]); + } + + public function get(string $name): PropertyDefinition + { + return $this->properties[$name]; + } + + public function count(): int + { + return count($this->properties); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield from $this->properties; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/PropertyDefinition.php b/lib/Vendor/CuyZ/Valinor/Definition/PropertyDefinition.php new file mode 100644 index 0000000..7d8a581 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/PropertyDefinition.php @@ -0,0 +1,24 @@ +|ReflectionProperty|ReflectionMethod|ReflectionFunction|ReflectionParameter $reflection + * @return list + */ + public function for(Reflector $reflection): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheClassDefinitionRepository.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheClassDefinitionRepository.php new file mode 100644 index 0000000..3175154 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheClassDefinitionRepository.php @@ -0,0 +1,38 @@ + */ + private CacheInterface $cache + ) {} + + public function for(ObjectType $type): ClassDefinition + { + // @infection-ignore-all + $key = "class-definition-\0" . $type->toString(); + + $entry = $this->cache->get($key); + + if ($entry) { + return $entry; + } + + $class = $this->delegate->for($type); + + $this->cache->set($key, $class); + + return $class; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheFunctionDefinitionRepository.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheFunctionDefinitionRepository.php new file mode 100644 index 0000000..9b26aff --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/CacheFunctionDefinitionRepository.php @@ -0,0 +1,40 @@ + */ + private CacheInterface $cache + ) {} + + public function for(callable $function): FunctionDefinition + { + $reflection = Reflection::function($function); + + // @infection-ignore-all + $key = "function-definition-\0" . $reflection->getFileName() . ':' . $reflection->getStartLine() . '-' . $reflection->getEndLine(); + + $entry = $this->cache->get($key); + + if ($entry) { + return $entry; + } + + $definition = $this->delegate->for($function); + + $this->cache->set($key, $definition); + + return $definition; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/AttributesCompiler.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/AttributesCompiler.php new file mode 100644 index 0000000..6140f2c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/AttributesCompiler.php @@ -0,0 +1,79 @@ +compileAttributes($attributes); + + return <<classDefinitionCompiler->compile($attribute->class); + + if ($attribute->arguments === []) { + $arguments = ''; + } else { + $arguments = implode(', ', array_map( + fn (mixed $argument) => $this->compileAttributeArguments($argument), + $attribute->arguments, + )); + } + + $attributesListCode[] = << $subValue) { + $parts[] = var_export($key, true) . ' => ' . $this->compileAttributeArguments($subValue); + } + + return '[' . implode(', ', $parts) . ']'; + } + + return var_export($value, true); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php new file mode 100644 index 0000000..5b58a7a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ClassDefinitionCompiler.php @@ -0,0 +1,74 @@ +typeCompiler = new TypeCompiler(); + $this->attributesCompiler = new AttributesCompiler($this); + + $this->methodCompiler = new MethodDefinitionCompiler($this->typeCompiler, $this->attributesCompiler); + $this->propertyCompiler = new PropertyDefinitionCompiler($this->typeCompiler, $this->attributesCompiler); + } + + public function compile(mixed $value): string + { + assert($value instanceof ClassDefinition); + + $name = var_export($value->name, true); + $type = $this->typeCompiler->compile($value->type); + + $properties = array_map( + fn (PropertyDefinition $property) => $this->propertyCompiler->compile($property), + iterator_to_array($value->properties) + ); + + $properties = implode(', ', $properties); + + $methods = array_map( + fn (MethodDefinition $method) => $this->methodCompiler->compile($method), + iterator_to_array($value->methods) + ); + + $methods = implode(', ', $methods); + $attributes = $this->attributesCompiler->compile($value->attributes); + + $isFinal = var_export($value->isFinal, true); + $isAbstract = var_export($value->isAbstract, true); + + return <<typeCompiler = new TypeCompiler(); + $this->attributesCompiler = new AttributesCompiler(new ClassDefinitionCompiler()); + + $this->parameterCompiler = new ParameterDefinitionCompiler($this->typeCompiler, $this->attributesCompiler); + } + + public function compile(mixed $value): string + { + assert($value instanceof FunctionDefinition); + + $parameters = array_map( + fn (ParameterDefinition $parameter) => $this->parameterCompiler->compile($parameter), + iterator_to_array($value->parameters) + ); + + $attributes = $this->attributesCompiler->compile($value->attributes); + $fileName = var_export($value->fileName, true); + $class = var_export($value->class, true); + $isStatic = var_export($value->isStatic, true); + $isClosure = var_export($value->isClosure, true); + $parameters = implode(', ', $parameters); + $returnType = $this->typeCompiler->compile($value->returnType); + + return <<name}', + '{$value->signature}', + $attributes, + $fileName, + $class, + $isStatic, + $isClosure, + new \OCA\Talk\Vendor\CuyZ\Valinor\Definition\Parameters($parameters), + $returnType + ) + PHP; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/MethodDefinitionCompiler.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/MethodDefinitionCompiler.php new file mode 100644 index 0000000..ebb8207 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/MethodDefinitionCompiler.php @@ -0,0 +1,54 @@ +typeCompiler = $typeCompiler; + $this->attributesCompiler = $attributesCompiler; + $this->parameterCompiler = new ParameterDefinitionCompiler($typeCompiler, $attributesCompiler); + } + + public function compile(MethodDefinition $method): string + { + $attributes = $this->attributesCompiler->compile($method->attributes); + + $parameters = array_map( + fn (ParameterDefinition $parameter) => $this->parameterCompiler->compile($parameter), + iterator_to_array($method->parameters) + ); + + $parameters = implode(', ', $parameters); + $isStatic = var_export($method->isStatic, true); + $isPublic = var_export($method->isPublic, true); + $returnType = $this->typeCompiler->compile($method->returnType); + + return <<name}', + '{$method->signature}', + $attributes, + new \OCA\Talk\Vendor\CuyZ\Valinor\Definition\Parameters($parameters), + $isStatic, + $isPublic, + $returnType + ) + PHP; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ParameterDefinitionCompiler.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ParameterDefinitionCompiler.php new file mode 100644 index 0000000..0f30826 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/ParameterDefinitionCompiler.php @@ -0,0 +1,48 @@ +isOptional, true); + $isVariadic = var_export($parameter->isVariadic, true); + $defaultValue = $this->defaultValue($parameter); + $type = $this->typeCompiler->compile($parameter->type); + $nativeType = $this->typeCompiler->compile($parameter->nativeType); + $attributes = $this->attributesCompiler->compile($parameter->attributes); + + return <<name}', + '{$parameter->signature}', + $type, + $nativeType, + $isOptional, + $isVariadic, + $defaultValue, + $attributes + ) + PHP; + } + + private function defaultValue(ParameterDefinition $parameter): string + { + $defaultValue = $parameter->defaultValue; + + return is_object($defaultValue) + ? 'unserialize(' . var_export(serialize($defaultValue), true) . ')' + : var_export($defaultValue, true); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/PropertyDefinitionCompiler.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/PropertyDefinitionCompiler.php new file mode 100644 index 0000000..2dc1840 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/PropertyDefinitionCompiler.php @@ -0,0 +1,39 @@ +typeCompiler->compile($property->type); + $nativeType = $this->typeCompiler->compile($property->nativeType); + $hasDefaultValue = var_export($property->hasDefaultValue, true); + $defaultValue = var_export($property->defaultValue, true); + $isPublic = var_export($property->isPublic, true); + $attributes = $this->attributesCompiler->compile($property->attributes); + + return <<name}', + '{$property->signature}', + $type, + $nativeType, + $hasDefaultValue, + $defaultValue, + $isPublic, + $attributes + ) + PHP; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/TypeCompiler.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/TypeCompiler.php new file mode 100644 index 0000000..2fe10db --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Cache/Compiler/TypeCompiler.php @@ -0,0 +1,193 @@ +value() === true + ? "$class::true()" + : "$class::false()"; + case $type instanceof IntegerRangeType: + return "new $class({$type->min()}, {$type->max()})"; + case $type instanceof StringValueType: + $value = var_export($type->toString(), true); + + return "$class::from($value)"; + case $type instanceof IntegerValueType: + case $type instanceof FloatValueType: + $value = var_export($type->value(), true); + + return "new $class($value)"; + case $type instanceof IntersectionType: + case $type instanceof UnionType: + $subTypes = array_map( + fn (Type $subType) => $this->compile($subType), + $type->types() + ); + + return "new $class(" . implode(', ', $subTypes) . ')'; + case $type instanceof ArrayKeyType: + return match ($type->toString()) { + 'string' => "$class::string()", + 'int' => "$class::integer()", + default => "$class::default()", + }; + case $type instanceof ShapedArrayType: + $elements = implode(', ', array_map( + fn (ShapedArrayElement $element) => $this->compileArrayShapeElement($element), + $type->elements() + )); + + if ($type->hasUnsealedType()) { + $unsealedType = $this->compile($type->unsealedType()); + + return "$class::unsealed($unsealedType, $elements)"; + } elseif ($type->isUnsealed()) { + return "$class::unsealedWithoutType($elements)"; + } + + return "new $class($elements)"; + case $type instanceof ArrayType: + case $type instanceof NonEmptyArrayType: + if ($type->toString() === 'array' || $type->toString() === 'non-empty-array') { + return "$class::native()"; + } + + $keyType = $this->compile($type->keyType()); + $subType = $this->compile($type->subType()); + + return "new $class($keyType, $subType)"; + case $type instanceof ListType: + case $type instanceof NonEmptyListType: + if ($type->toString() === 'list' || $type->toString() === 'non-empty-list') { + return "$class::native()"; + } + + $subType = $this->compile($type->subType()); + + return "new $class($subType)"; + case $type instanceof IterableType: + $keyType = $this->compile($type->keyType()); + $subType = $this->compile($type->subType()); + + return "new $class($keyType, $subType)"; + case $type instanceof NativeClassType: + case $type instanceof InterfaceType: + $generics = []; + + foreach ($type->generics() as $key => $generic) { + $generics[] = var_export($key, true) . ' => ' . $this->compile($generic); + } + + $generics = implode(', ', $generics); + + return "new $class('{$type->className()}', [$generics])"; + case $type instanceof ClassStringType: + if (null === $type->subType()) { + return "new $class()"; + } + + $subType = $this->compile($type->subType()); + + return "new $class($subType)"; + case $type instanceof EnumType: + $enumName = var_export($type->className(), true); + $pattern = var_export($type->pattern(), true); + + $cases = array_map( + fn (string|int $key, UnitEnum $case) => var_export($key, true) . ' => ' . var_export($case, true), + array_keys($type->cases()), + $type->cases() + ); + $cases = implode(', ', $cases); + + return "new $class($enumName, $pattern, [$cases])"; + case $type instanceof UnresolvableType: + $raw = var_export($type->toString(), true); + $message = var_export($type->message(), true); + + return "new $class($raw, $message)"; + default: + throw new TypeCannotBeCompiled($type); + } + } + + private function compileArrayShapeElement(ShapedArrayElement $element): string + { + $class = ShapedArrayElement::class; + $key = $this->compile($element->key()); + $type = $this->compile($element->type()); + $optional = var_export($element->isOptional(), true); + + return "new $class($key, $type, $optional)"; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/ClassDefinitionRepository.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/ClassDefinitionRepository.php new file mode 100644 index 0000000..225c616 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/ClassDefinitionRepository.php @@ -0,0 +1,14 @@ + */ + private array $allowedAttributes, + ) {} + + public function for(Reflector $reflection): array + { + $attributes = array_filter( + $reflection->getAttributes(), + function (ReflectionAttribute $attribute) { + foreach ($this->allowedAttributes as $allowedAttribute) { + if (is_a($attribute->getName(), $allowedAttribute, true)) { + return $this->attributeCanBeInstantiated($attribute); + } + } + + $parentAttributes = Reflection::class($attribute->getName())->getAttributes(AsTransformer::class); + + return $parentAttributes !== []; + }, + ); + + return array_values(array_map( + fn (ReflectionAttribute $attribute) => new AttributeDefinition( + $this->classDefinitionRepository->for(new NativeClassType($attribute->getName())), + array_values($attribute->getArguments()), + ), + $attributes, + )); + } + + /** + * @param ReflectionAttribute $attribute + */ + private function attributeCanBeInstantiated(ReflectionAttribute $attribute): bool + { + try { + $attribute->newInstance(); + + return true; + } catch (Error) { + // Race condition when the attribute is affected to a property/parameter + // that was PROMOTED, in this case the attribute will be applied to both + // ParameterReflection AND PropertyReflection, BUT the target arg inside the attribute + // class is configured to support only ONE of them (parameter OR property) + // https://wiki.php.net/rfc/constructor_promotion#attributes for more details. + // Ignore attribute if the instantiation failed. + return false; + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php new file mode 100644 index 0000000..e25ad3a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php @@ -0,0 +1,177 @@ + */ + private array $typeResolver = []; + + /** + * @param list $allowedAttributes + */ + public function __construct( + TypeParserFactory $typeParserFactory, + array $allowedAttributes, + ) { + $this->typeParserFactory = $typeParserFactory; + $this->attributesRepository = new ReflectionAttributesRepository($this, $allowedAttributes); + $this->propertyBuilder = new ReflectionPropertyDefinitionBuilder($this->attributesRepository); + $this->methodBuilder = new ReflectionMethodDefinitionBuilder($this->attributesRepository); + } + + public function for(ObjectType $type): ClassDefinition + { + $reflection = Reflection::class($type->className()); + + return new ClassDefinition( + $reflection->name, + $type, + new Attributes(...$this->attributesRepository->for($reflection)), + new Properties(...$this->properties($type)), + new Methods(...$this->methods($type)), + $reflection->isFinal(), + $reflection->isAbstract(), + ); + } + + /** + * @return list + */ + private function properties(ObjectType $type): array + { + $reflection = Reflection::class($type->className()); + + $properties = []; + + foreach ($reflection->getProperties() as $property) { + $typeResolver = $this->typeResolver($type, $property->getDeclaringClass()); + + $properties[$property->name] = $this->propertyBuilder->for($property, $typeResolver); + } + + // Properties will be sorted by inheritance order, from parent to child. + $sortedProperties = []; + + while ($reflection) { + $currentProperties = array_map( + fn (ReflectionProperty $property) => $properties[$property->name], + array_filter( + $reflection->getProperties(), + fn (ReflectionProperty $property) => isset($properties[$property->name]), + ), + ); + + $sortedProperties = [...$currentProperties, ...$sortedProperties]; + + $reflection = $reflection->getParentClass(); + } + + return $sortedProperties; + } + + /** + * @return list + */ + private function methods(ObjectType $type): array + { + $reflection = Reflection::class($type->className()); + $methods = $reflection->getMethods(); + + // Because `ReflectionMethod::getMethods()` wont list the constructor if + // it comes from a parent class AND is not public, we need to manually + // fetch it and add it to the list. + if ($reflection->hasMethod('__construct')) { + $methods[] = $reflection->getMethod('__construct'); + } + + return array_map(function (ReflectionMethod $method) use ($type) { + $typeResolver = $this->typeResolver($type, $method->getDeclaringClass()); + + return $this->methodBuilder->for($method, $typeResolver); + }, $methods); + } + + /** + * @param ReflectionClass $target + */ + private function typeResolver(ObjectType $type, ReflectionClass $target): ReflectionTypeResolver + { + $typeKey = $target->isInterface() + ? "{$type->toString()}/{$type->className()}" + : "{$type->toString()}/$target->name"; + + if (isset($this->typeResolver[$typeKey])) { + return $this->typeResolver[$typeKey]; + } + + $parentTypeResolver = new ClassParentTypeResolver($this->typeParserFactory); + + while ($type->className() !== $target->name) { + $type = $parentTypeResolver->resolveParentTypeFor($type); + } + + $localTypeAliasResolver = new ClassLocalTypeAliasResolver($this->typeParserFactory); + $importedTypeAliasResolver = new ClassImportedTypeAliasResolver($this->typeParserFactory); + + $generics = $type instanceof GenericType ? $type->generics() : []; + $localAliases = $localTypeAliasResolver->resolveLocalTypeAliases($type); + $importedAliases = $importedTypeAliasResolver->resolveImportedTypeAliases($type); + + $duplicates = []; + $keys = [...array_keys($generics), ...array_keys($localAliases), ...array_keys($importedAliases)]; + + foreach ($keys as $key) { + $sameKeys = array_filter($keys, fn ($value) => $value === $key); + + if (count($sameKeys) > 1) { + $duplicates[$key] = null; + } + } + + if (count($duplicates) > 0) { + throw new ClassTypeAliasesDuplication($type->className(), ...array_keys($duplicates)); + } + + $advancedParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type, $generics + $localAliases + $importedAliases); + $nativeParser = $this->typeParserFactory->buildNativeTypeParserForClass($type->className()); + + return $this->typeResolver[$typeKey] = new ReflectionTypeResolver($nativeParser, $advancedParser); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php new file mode 100644 index 0000000..4e337b2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php @@ -0,0 +1,102 @@ +typeParserFactory = $typeParserFactory; + $this->attributesRepository = $attributesRepository; + $this->parameterBuilder = new ReflectionParameterDefinitionBuilder($attributesRepository); + } + + public function for(callable $function): FunctionDefinition + { + $reflection = Reflection::function($function); + + $nativeParser = $this->typeParserFactory->buildNativeTypeParserForFunction($reflection); + $advancedParser = $this->typeParserFactory->buildAdvancedTypeParserForFunction($reflection); + + $typeResolver = new ReflectionTypeResolver($nativeParser, $advancedParser); + + $returnTypeResolver = new FunctionReturnTypeResolver($typeResolver); + + $parameters = array_map( + fn (ReflectionParameter $parameter) => $this->parameterBuilder->for($parameter, $typeResolver), + $reflection->getParameters(), + ); + + $name = $reflection->getName(); + $signature = $this->signature($reflection); + $class = $reflection->getClosureScopeClass(); + $returnType = $returnTypeResolver->resolveReturnTypeFor($reflection); + $nativeReturnType = $returnTypeResolver->resolveNativeReturnTypeFor($reflection); + // PHP8.2 use `ReflectionFunction::isAnonymous()` + $isClosure = $name === '{closure}' || str_ends_with($name, '\\{closure}') || str_starts_with($name, '{closure:'); + + if ($returnType instanceof UnresolvableType) { + $returnType = $returnType->forFunctionReturnType($signature); + } elseif (! $returnType->matches($nativeReturnType)) { + $returnType = UnresolvableType::forNonMatchingFunctionReturnTypes($name, $nativeReturnType, $returnType); + } + + return new FunctionDefinition( + $name, + $signature, + new Attributes(...$this->attributesRepository->for($reflection)), + $reflection->getFileName() ?: null, + $class?->name, + $reflection->getClosureThis() === null, + $isClosure, + new Parameters(...$parameters), + $returnType, + ); + } + + /** + * @return non-empty-string + */ + private function signature(ReflectionFunction $reflection): string + { + // PHP8.2 use `ReflectionFunction::isAnonymous()` + if ($reflection->name === '{closure}' || str_ends_with($reflection->name, '\\{closure}') || str_starts_with($reflection->name, '{closure:')) { + $startLine = $reflection->getStartLine(); + $endLine = $reflection->getEndLine(); + + return $startLine === $endLine + ? "Closure (line $startLine of {$reflection->getFileName()})" + : "Closure (lines $startLine to $endLine of {$reflection->getFileName()})"; + } + + return $reflection->getClosureScopeClass() + ? $reflection->getClosureScopeClass()->name . '::' . $reflection->name . '()' + : $reflection->name . '()'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php new file mode 100644 index 0000000..daef36f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php @@ -0,0 +1,63 @@ +attributesRepository = $attributesRepository; + $this->parameterBuilder = new ReflectionParameterDefinitionBuilder($attributesRepository); + } + + public function for(ReflectionMethod $reflection, ReflectionTypeResolver $typeResolver): MethodDefinition + { + $name = $reflection->name; + $signature = $reflection->getDeclaringClass()->name . '::' . $reflection->name . '()'; + + $parameters = array_map( + fn (ReflectionParameter $parameter) => $this->parameterBuilder->for($parameter, $typeResolver), + $reflection->getParameters() + ); + + $returnTypeResolver = new FunctionReturnTypeResolver($typeResolver); + + $returnType = $returnTypeResolver->resolveReturnTypeFor($reflection); + $nativeReturnType = $returnTypeResolver->resolveNativeReturnTypeFor($reflection); + + if ($returnType instanceof UnresolvableType) { + $returnType = $returnType->forMethodReturnType($signature); + } elseif (! $returnType->matches($nativeReturnType)) { + $returnType = UnresolvableType::forNonMatchingMethodReturnTypes($name, $nativeReturnType, $returnType); + } + + return new MethodDefinition( + $name, + $signature, + new Attributes(...$this->attributesRepository->for($reflection)), + new Parameters(...$parameters), + $reflection->isStatic(), + $reflection->isPublic(), + $returnType + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php new file mode 100644 index 0000000..6ab3049 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php @@ -0,0 +1,74 @@ +name; + $signature = $this->signature($reflection); + $type = $parameterTypeResolver->resolveTypeFor($reflection); + $nativeType = $parameterTypeResolver->resolveNativeTypeFor($reflection); + $isOptional = $reflection->isOptional(); + $isVariadic = $reflection->isVariadic(); + + if ($reflection->isDefaultValueAvailable()) { + $defaultValue = $reflection->getDefaultValue(); + } elseif ($reflection->isVariadic()) { + $defaultValue = []; + } else { + $defaultValue = null; + } + + if ($type instanceof UnresolvableType) { + $type = $type->forParameter($signature); + } elseif (! $type->matches($nativeType)) { + $type = UnresolvableType::forNonMatchingParameterTypes($signature, $nativeType, $type); + } elseif ($isOptional && ! $type->accepts($defaultValue)) { + $type = UnresolvableType::forInvalidParameterDefaultValue($signature, $type, $defaultValue); + } + + return new ParameterDefinition( + $name, + $signature, + $type, + $nativeType, + $isOptional, + $isVariadic, + $defaultValue, + new Attributes(...$this->attributesRepository->for($reflection)), + ); + } + + /** + * @return non-empty-string + */ + private function signature(ReflectionParameter $reflection): string + { + $signature = $reflection->getDeclaringFunction()->name . "(\$$reflection->name)"; + $class = $reflection->getDeclaringClass(); + + if ($class) { + $signature = $class->name . '::' . $signature; + } + + return $signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php new file mode 100644 index 0000000..754a276 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php @@ -0,0 +1,64 @@ +name; + $signature = $reflection->getDeclaringClass()->name . '::$' . $reflection->name; + $type = $propertyTypeResolver->resolveTypeFor($reflection); + $nativeType = $propertyTypeResolver->resolveNativeTypeFor($reflection); + $hasDefaultValue = $this->hasDefaultValue($reflection, $type); + $defaultValue = $reflection->getDefaultValue(); + $isPublic = $reflection->isPublic(); + + if ($type instanceof UnresolvableType) { + $type = $type->forProperty($signature); + } elseif (! $type->matches($nativeType)) { + $type = UnresolvableType::forNonMatchingPropertyTypes($signature, $nativeType, $type); + } elseif ($hasDefaultValue && ! $type->accepts($defaultValue)) { + $type = UnresolvableType::forInvalidPropertyDefaultValue($signature, $type, $defaultValue); + } + + return new PropertyDefinition( + $name, + $signature, + $type, + $nativeType, + $hasDefaultValue, + $defaultValue, + $isPublic, + new Attributes(...$this->attributesRepository->for($reflection)), + ); + } + + private function hasDefaultValue(ReflectionProperty $reflection, Type $type): bool + { + if ($reflection->hasType()) { + return $reflection->hasDefaultValue(); + } + + return $reflection->getDeclaringClass()->getDefaultProperties()[$reflection->name] !== null + || NullType::get()->matches($type); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php new file mode 100644 index 0000000..aba775a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php @@ -0,0 +1,111 @@ + + */ + public function resolveImportedTypeAliases(ObjectType $type): array + { + $importedTypesRaw = $this->extractImportedAliasesFromDocBlock($type->className()); + + if ($importedTypesRaw === []) { + return []; + } + + $typeParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type); + + $importedTypes = []; + + foreach ($importedTypesRaw as $class => $types) { + try { + $classType = $typeParser->parse($class); + } catch (InvalidType) { + throw new InvalidTypeAliasImportClass($type, $class); + } + + if (! $classType instanceof ObjectType) { + throw new InvalidTypeAliasImportClassType($type, $classType); + } + + $localTypes = (new ClassLocalTypeAliasResolver($this->typeParserFactory))->resolveLocalTypeAliases($classType); + + foreach ($types as $importedType) { + if (! isset($localTypes[$importedType])) { + throw new UnknownTypeAliasImport($type, $classType->className(), $importedType); + } + + $importedTypes[$importedType] = $localTypes[$importedType]; + } + } + + return $importedTypes; + } + + /** + * @param class-string $className + * @return array> + */ + private function extractImportedAliasesFromDocBlock(string $className): array + { + $docBlock = Reflection::class($className)->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $importedAliases = []; + + $annotations = (new Annotations($docBlock))->filteredByPriority( + '@phpstan-import-type', + '@psalm-import-type', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $name = current($tokens); + $from = next($tokens); + + if ($from !== 'from') { + continue; + } + + next($tokens); + + $key = key($tokens); + + // @phpstan-ignore identical.alwaysFalse (Somehow PHPStan does not properly infer the key) + if ($key === null) { + continue; + } + + $class = $annotation->allAfter($key); + + $importedAliases[$class][] = $name; + } + + return $importedAliases; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php new file mode 100644 index 0000000..2aee7da --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php @@ -0,0 +1,89 @@ + + */ + public function resolveLocalTypeAliases(ObjectType $type): array + { + $localAliases = $this->extractLocalAliasesFromDocBlock($type->className()); + + if ($localAliases === []) { + return []; + } + + $types = []; + + foreach ($localAliases as $name => $raw) { + try { + $typeParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type, $types); + + $types[$name] = $typeParser->parse($raw); + } catch (InvalidType $exception) { + $types[$name] = UnresolvableType::forLocalAlias($raw, $name, $type, $exception); + } + } + + return $types; + } + + /** + * @param class-string $className + * @return array + */ + private function extractLocalAliasesFromDocBlock(string $className): array + { + $docBlock = Reflection::class($className)->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $aliases = []; + + $annotations = (new Annotations($docBlock))->filteredInOrder( + '@phpstan-type', + '@psalm-type', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $name = current($tokens); + $next = next($tokens); + + if ($next === '=') { + next($tokens); + } + + $key = key($tokens); + + // @phpstan-ignore notIdentical.alwaysTrue (Somehow PHPStan does not properly infer the key) + if ($key !== null) { + $aliases[$name] = $annotation->allAfter($key); + } + } + + return $aliases; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php new file mode 100644 index 0000000..286f5bf --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php @@ -0,0 +1,86 @@ +className()); + + /** @var ReflectionClass $parentReflection */ + $parentReflection = $reflection->getParentClass(); + + $extendedClass = $this->extractParentTypeFromDocBlock($reflection); + + if (count($extendedClass) > 1) { + throw new SeveralExtendTagsFound($reflection); + } elseif (count($extendedClass) === 0) { + $extendedClass = $parentReflection->name; + } else { + $extendedClass = $extendedClass[0]; + } + + $typeParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type); + + try { + $parentType = $typeParser->parse($extendedClass); + } catch (InvalidType $exception) { + throw new ExtendTagTypeError($reflection, $exception); + } + + if (! $parentType instanceof NativeClassType) { + throw new InvalidExtendTagType($reflection, $parentType); + } + + if ($parentType->className() !== $parentReflection->name) { + throw new InvalidExtendTagClassName($reflection, $parentType); + } + + return $parentType; + } + + /** + * @param ReflectionClass $reflection + * @return list + */ + private function extractParentTypeFromDocBlock(ReflectionClass $reflection): array + { + $docBlock = $reflection->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $annotations = (new Annotations($docBlock))->filteredByPriority( + '@phpstan-extends', + '@psalm-extends', + '@extends', + ); + + return array_map( + fn (TokenizedAnnotation $annotation) => $annotation->raw(), + $annotations, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php new file mode 100644 index 0000000..e519aad --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php @@ -0,0 +1,76 @@ + + */ + public function resolveTemplateNamesFrom(string $className): array + { + return array_keys($this->resolveTemplatesFrom($className)); + } + + /** + * @param class-string $className + * @return array + */ + public function resolveTemplatesFrom(string $className): array + { + $docBlock = Reflection::class($className)->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $templates = []; + + $annotations = (new Annotations($docBlock))->filteredByPriority( + '@phpstan-template', + '@psalm-template', + '@template', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $name = current($tokens); + + if (array_key_exists($name, $templates)) { + throw new DuplicatedTemplateName($className, $name); + } + + $of = next($tokens); + + if ($of !== 'of') { + // The keyword `of` was not found, the following tokens are + // considered as comments, and we ignore them. + $templates[$name] = null; + } else { + // The keyword `of` was found, the following tokens represent + // the template type. + next($tokens); + + $key = key($tokens); + + $templates[$name] = $key ? $annotation->allAfter($key) : null; + } + } + + return $templates; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php new file mode 100644 index 0000000..fe4c46b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php @@ -0,0 +1,42 @@ +extractReturnTypeFromDocBlock($reflection); + + return $this->typeResolver->resolveType($reflection->getReturnType(), $docBlockType); + } + + public function resolveNativeReturnTypeFor(ReflectionFunctionAbstract $reflection): Type + { + return $this->typeResolver->resolveNativeType($reflection->getReturnType()); + } + + private function extractReturnTypeFromDocBlock(ReflectionFunctionAbstract $reflection): ?string + { + $docBlock = $reflection->getDocComment(); + + if ($docBlock === false) { + return null; + } + + return (new Annotations($docBlock))->firstOf( + '@phpstan-return', + '@psalm-return', + '@return', + )?->raw(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php new file mode 100644 index 0000000..1b2c2f2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php @@ -0,0 +1,86 @@ +isPromoted()) { + // @phpstan-ignore-next-line / parameter is promoted so class exists for sure + $property = $reflection->getDeclaringClass()->getProperty($reflection->name); + + $docBlockType = (new PropertyTypeResolver($this->typeResolver))->extractTypeFromDocBlock($property); + } + + if ($docBlockType === null) { + $docBlockType = $this->extractTypeFromDocBlock($reflection); + } + + $type = $this->typeResolver->resolveType($reflection->getType(), $docBlockType); + + if ($reflection->isVariadic() && ! $type instanceof UnresolvableType) { + return new ArrayType(ArrayKeyType::default(), $type); + } + + return $type; + } + + public function resolveNativeTypeFor(ReflectionParameter $reflection): Type + { + $type = $this->typeResolver->resolveNativeType($reflection->getType()); + + if ($reflection->isVariadic()) { + return new ArrayType(ArrayKeyType::default(), $type); + } + + return $type; + } + + private function extractTypeFromDocBlock(ReflectionParameter $reflection): ?string + { + $docBlock = $reflection->getDeclaringFunction()->getDocComment(); + + if ($docBlock === false) { + return null; + } + + $annotations = (new Annotations($docBlock))->filteredByPriority( + '@phpstan-param', + '@psalm-param', + '@param', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $dollarSignKey = array_search('$', $tokens, true); + + if ($dollarSignKey === false) { + continue; + } + + $parameterName = $tokens[$dollarSignKey + 1] ?? null; + + if ($parameterName === $reflection->name) { + return $annotation->splice($dollarSignKey); + } + } + + return null; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php new file mode 100644 index 0000000..bee6584 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php @@ -0,0 +1,42 @@ +extractTypeFromDocBlock($reflection); + + return $this->typeResolver->resolveType($reflection->getType(), $docBlockType); + } + + public function resolveNativeTypeFor(ReflectionProperty $reflection): Type + { + return $this->typeResolver->resolveNativeType($reflection->getType()); + } + + public function extractTypeFromDocBlock(ReflectionProperty $reflection): ?string + { + $docBlock = $reflection->getDocComment(); + + if ($docBlock === false) { + return null; + } + + return (new Annotations($docBlock))->firstOf( + '@phpstan-var', + '@psalm-var', + '@var', + )?->raw(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php new file mode 100644 index 0000000..56d55df --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php @@ -0,0 +1,86 @@ +parseType($docBlock, $this->advancedParser); + } + + if ($native === null) { + return MixedType::get(); + } + + $type = $this->exportNativeType($native); + + // When the type is a class, it may declare templates that must be + // filled with generics. PHP does not handle generics natively, so we + // need to make sure that no generics are left unassigned by parsing the + // type using the advanced parser. + return $this->parseType($type, $this->advancedParser); + } + + public function resolveNativeType(?ReflectionType $reflection): Type + { + if ($reflection === null) { + return MixedType::get(); + } + + $type = $this->exportNativeType($reflection); + + return $this->parseType($type, $this->nativeParser); + } + + private function exportNativeType(ReflectionType $type): string + { + if ($type instanceof ReflectionUnionType) { + return implode('|', $type->getTypes()); + } + if ($type instanceof ReflectionIntersectionType) { + return implode('&', $type->getTypes()); + } + + /** @var ReflectionNamedType $type */ + $name = $type->getName(); + + if ($name !== 'null' && $type->allowsNull() && $name !== 'mixed') { + return $name . '|null'; + } + + return $name; + } + + private function parseType(string $raw, TypeParser $parser): Type + { + try { + return $parser->parse($raw); + } catch (InvalidType $exception) { + return new UnresolvableType($raw, $exception->getMessage()); + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Library/Container.php b/lib/Vendor/CuyZ/Valinor/Library/Container.php new file mode 100644 index 0000000..2aeaef3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Library/Container.php @@ -0,0 +1,276 @@ + */ + private array $services = []; + + /** @var array */ + private array $factories; + + public function __construct(Settings $settings) + { + $this->factories = [ + TreeMapper::class => fn () => new TypeTreeMapper( + $this->get(TypeParser::class), + $this->get(RootNodeBuilder::class), + $settings, + ), + + ArgumentsMapper::class => fn () => new TypeArgumentsMapper( + $this->get(FunctionDefinitionRepository::class), + $this->get(RootNodeBuilder::class), + $settings, + ), + + RootNodeBuilder::class => fn () => new RootNodeBuilder( + $this->get(NodeBuilder::class), + ), + + NodeBuilder::class => function () use ($settings) { + $builder = new TypeNodeBuilder( + new ArrayNodeBuilder(), + new ListNodeBuilder(), + new ShapedArrayNodeBuilder(), + new ScalarNodeBuilder(), + new UnionNodeBuilder(), + new NullNodeBuilder(), + new MixedNodeBuilder(), + new UndefinedObjectNodeBuilder(), + new ObjectNodeBuilder( + $this->get(ClassDefinitionRepository::class), + $this->get(ObjectBuilderFactory::class), + $settings->exceptionFilter, + ), + ); + + $builder = new InterfaceNodeBuilder( + $builder, + $this->get(ObjectImplementations::class), + $this->get(ClassDefinitionRepository::class), + new FunctionsContainer( + $this->get(FunctionDefinitionRepository::class), + $settings->customConstructors, + ), + $settings->exceptionFilter, + ); + + if (count($settings->valueModifier) > 0) { + $builder = new ValueAlteringNodeBuilder( + $builder, + new FunctionsContainer( + $this->get(FunctionDefinitionRepository::class), + $settings->valueModifier, + ), + ); + } + + return $builder; + }, + + ObjectImplementations::class => fn () => new ObjectImplementations( + new FunctionsContainer( + $this->get(FunctionDefinitionRepository::class), + $settings->inferredMapping, + ), + $this->get(TypeParser::class), + ), + + ObjectBuilderFactory::class => function () use ($settings) { + $constructors = new FunctionsContainer( + $this->get(FunctionDefinitionRepository::class), + $settings->customConstructors, + ); + + $factory = new ReflectionObjectBuilderFactory(); + $factory = new ConstructorObjectBuilderFactory($factory, $settings->nativeConstructors, $constructors); + $factory = new DateTimeZoneObjectBuilderFactory($factory, $this->get(FunctionDefinitionRepository::class)); + $factory = new DateTimeObjectBuilderFactory($factory, $settings->supportedDateFormats, $this->get(FunctionDefinitionRepository::class)); + $factory = new SortingObjectBuilderFactory($factory); + + if (! $settings->allowPermissiveTypes) { + $factory = new StrictTypesObjectBuilderFactory($factory); + } + + /** @var RuntimeCache> $cache */ + $cache = new RuntimeCache(); + + return new CacheObjectBuilderFactory($factory, $cache); + }, + + Transformer::class => function () use ($settings) { + if (isset($settings->cache)) { + return new CacheTransformer( + $this->get(TransformerDefinitionBuilder::class), + $this->get(CacheInterface::class), + $settings->transformersSortedByPriority(), + ); + } + + return new RecursiveTransformer( + $this->get(ClassDefinitionRepository::class), + $this->get(FunctionDefinitionRepository::class), + $this->get(TransformerContainer::class), + ); + }, + + TransformerContainer::class => fn () => new TransformerContainer( + $this->get(FunctionDefinitionRepository::class), + $settings->transformersSortedByPriority(), + ), + + ArrayNormalizer::class => fn () => new ArrayNormalizer( + $this->get(Transformer::class), + ), + + JsonNormalizer::class => fn () => new JsonNormalizer( + $this->get(Transformer::class), + ), + + TransformerDefinitionBuilder::class => fn () => new TransformerDefinitionBuilder( + $this->get(ClassDefinitionRepository::class), + $this->get(FunctionDefinitionRepository::class), + $this->get(TransformerContainer::class), + ), + + ClassDefinitionRepository::class => fn () => new CacheClassDefinitionRepository( + new ReflectionClassDefinitionRepository( + $this->get(TypeParserFactory::class), + $settings->allowedAttributes(), + ), + $this->get(CacheInterface::class), + ), + + FunctionDefinitionRepository::class => fn () => new CacheFunctionDefinitionRepository( + new ReflectionFunctionDefinitionRepository( + $this->get(TypeParserFactory::class), + new ReflectionAttributesRepository( + $this->get(ClassDefinitionRepository::class), + $settings->allowedAttributes(), + ), + ), + $this->get(CacheInterface::class), + ), + + TypeParserFactory::class => fn () => new LexingTypeParserFactory(), + + TypeParser::class => fn () => $this->get(TypeParserFactory::class)->buildDefaultTypeParser(), + + RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService( + $this->get(TypeParser::class), + $this->get(CacheInterface::class), + $this->get(ObjectImplementations::class), + $this->get(ClassDefinitionRepository::class), + $this->get(ObjectBuilderFactory::class), + ), + + CacheInterface::class => function () use ($settings) { + $cache = new RuntimeCache(); + + if (isset($settings->cache)) { + $cache = new ChainCache($cache, new KeySanitizerCache($settings->cache, $settings)); + } + + return $cache; + }, + ]; + } + + public function treeMapper(): TreeMapper + { + return $this->get(TreeMapper::class); + } + + public function argumentsMapper(): ArgumentsMapper + { + return $this->get(ArgumentsMapper::class); + } + + /** + * @template T of Normalizer + * + * @param Format $format + * @return T + */ + public function normalizer(Format $format): Normalizer + { + return $this->get($format->type()); + } + + public function cacheWarmupService(): RecursiveCacheWarmupService + { + return $this->get(RecursiveCacheWarmupService::class); + } + + /** + * @template T of object + * @param class-string $name + * @return T + */ + private function get(string $name): object + { + return $this->services[$name] ??= call_user_func($this->factories[$name]); // @phpstan-ignore-line + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Library/Settings.php b/lib/Vendor/CuyZ/Valinor/Library/Settings.php new file mode 100644 index 0000000..bd3e20a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Library/Settings.php @@ -0,0 +1,138 @@ + */ + public const DEFAULT_SUPPORTED_DATETIME_FORMATS = [ + 'Y-m-d\\TH:i:sP', // RFC 3339 + 'Y-m-d\\TH:i:s.uP', // RFC 3339 with microseconds + 'U', // Unix Timestamp + 'U.u', // Unix Timestamp with microseconds + ]; + + /** @var array */ + public array $inferredMapping = []; + + /** @var array */ + public array $nativeConstructors = []; + + /** @var list */ + public array $customConstructors = []; + + /** @var list */ + public array $valueModifier = []; + + /** @var CacheInterface */ + public CacheInterface $cache; + + /** @var non-empty-list */ + public array $supportedDateFormats = self::DEFAULT_SUPPORTED_DATETIME_FORMATS; + + public bool $allowScalarValueCasting = false; + + public bool $allowNonSequentialList = false; + + public bool $allowUndefinedValues = false; + + public bool $allowSuperfluousKeys = false; + + public bool $allowPermissiveTypes = false; + + /** @var callable(Throwable): ErrorMessage */ + public $exceptionFilter; + + /** @var array> */ + public array $transformers = []; + + /** @var array */ + public array $transformerAttributes = []; + + private string $hash; + + public function __construct() + { + $this->inferredMapping[DateTimeInterface::class] = static fn () => DateTimeImmutable::class; + $this->exceptionFilter = fn (Throwable $exception) => throw $exception; + } + + /** + * @return non-empty-list + */ + public function allowedAttributes(): array + { + return [ + AsTransformer::class, + Constructor::class, + DynamicConstructor::class, + ...array_keys($this->transformerAttributes), + ]; + } + + /** + * @return list + */ + public function transformersSortedByPriority(): array + { + krsort($this->transformers); + + $callables = []; + + foreach ($this->transformers as $list) { + $callables = [...$callables, ...$list]; + } + + return $callables; + } + + /** + * Returns a unique hash, based on all the settings values that were set in + * this instance. + */ + public function hash(): string + { + return $this->hash ??= hash('xxh128', serialize([ + implode('', array_map($this->callableSignature(...), $this->inferredMapping)), + $this->nativeConstructors, + implode('', array_map($this->callableSignature(...), $this->customConstructors)), + implode('', array_map($this->callableSignature(...), $this->valueModifier)), + $this->supportedDateFormats, + $this->allowScalarValueCasting, + $this->allowNonSequentialList, + $this->allowUndefinedValues, + $this->allowSuperfluousKeys, + $this->allowPermissiveTypes, + $this->callableSignature($this->exceptionFilter), + array_map( + fn (array $transformers) => implode('', array_map($this->callableSignature(...), $transformers)), + $this->transformers, + ), + $this->transformerAttributes, + ])); + } + + private function callableSignature(callable $callable): string + { + $reflection = new ReflectionFunction(Closure::fromCallable($callable)); + + return ($reflection->getClosureCalledClass()->name ?? $reflection->getFileName()) . $reflection->getStartLine() . $reflection->getEndLine(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapper.php b/lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapper.php new file mode 100644 index 0000000..76f7715 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapper.php @@ -0,0 +1,16 @@ + + * + * @throws MappingError + */ + public function mapArguments(callable $callable, mixed $source): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapperError.php b/lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapperError.php new file mode 100644 index 0000000..12bdf67 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/ArgumentsMapperError.php @@ -0,0 +1,42 @@ +node = $node; + + $errors = Messages::flattenFromNode($node)->errors(); + $errorsCount = count($errors); + + if ($errorsCount === 1) { + $body = $errors + ->toArray()[0] + ->withBody("Could not map arguments of `$function->signature`. An error occurred at path {node_path}: {original_message}") + ->toString(); + } else { + $source = ValueDumper::dump($node->sourceValue()); + $body = "Could not map arguments of `$function->signature` with value $source. A total of $errorsCount errors were encountered."; + } + + parent::__construct($body, 1671115362); + } + + public function node(): Node + { + return $this->node; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Exception/InvalidMappingTypeSignature.php b/lib/Vendor/CuyZ/Valinor/Mapper/Exception/InvalidMappingTypeSignature.php new file mode 100644 index 0000000..3855356 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Exception/InvalidMappingTypeSignature.php @@ -0,0 +1,21 @@ +getMessage()}", + 1630959692, + $exception + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringArgumentsMapping.php b/lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringArgumentsMapping.php new file mode 100644 index 0000000..a1bfb22 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringArgumentsMapping.php @@ -0,0 +1,22 @@ +signature`: {$exception->getMessage()}", + 1711534351, + $exception, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringMapping.php b/lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringMapping.php new file mode 100644 index 0000000..0b89aae --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Exception/TypeErrorDuringMapping.php @@ -0,0 +1,22 @@ +toString()}`: {$exception->getMessage()}", + 1711526329, + $exception, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/MappingError.php b/lib/Vendor/CuyZ/Valinor/Mapper/MappingError.php new file mode 100644 index 0000000..20ea62d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/MappingError.php @@ -0,0 +1,14 @@ +name = $name; + $this->signature = $signature; + $this->type = $type; + } + + public static function fromParameter(ParameterDefinition $parameter): self + { + $instance = new self($parameter->name, $parameter->signature, $parameter->type); + $instance->attributes = $parameter->attributes; + + if ($parameter->isOptional) { + $instance->defaultValue = $parameter->defaultValue; + $instance->isRequired = false; + } + + return $instance; + } + + public static function fromProperty(PropertyDefinition $property): self + { + $instance = new self($property->name, $property->signature, $property->type); + $instance->attributes = $property->attributes; + + if ($property->hasDefaultValue) { + $instance->defaultValue = $property->defaultValue; + $instance->isRequired = false; + } + + return $instance; + } + + /** + * @return non-empty-string + */ + public function name(): string + { + return $this->name; + } + + /** + * @return non-empty-string + */ + public function signature(): string + { + return $this->signature; + } + + public function type(): Type + { + return $this->type; + } + + public function defaultValue(): mixed + { + return $this->defaultValue; + } + + public function isRequired(): bool + { + return $this->isRequired; + } + + public function attributes(): Attributes + { + return $this->attributes ??= Attributes::empty(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Arguments.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Arguments.php new file mode 100644 index 0000000..e1bf656 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Arguments.php @@ -0,0 +1,86 @@ + + */ +final class Arguments implements IteratorAggregate, Countable +{ + /** @var array */ + private array $arguments = []; + + public function __construct(Argument ...$arguments) + { + foreach ($arguments as $argument) { + $this->arguments[$argument->name()] = $argument; + } + } + + public static function fromParameters(Parameters $parameters): self + { + return new self(...array_map( + fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter), + [...$parameters], + )); + } + + public static function fromProperties(Properties $properties): self + { + return new self(...array_map( + fn (PropertyDefinition $property) => Argument::fromProperty($property), + [...$properties], + )); + } + + public function at(int $index): Argument + { + return array_values($this->arguments)[$index]; + } + + /** + * @return list + */ + public function names(): array + { + return array_keys($this->arguments); + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->arguments; + } + + public function count(): int + { + return count($this->arguments); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield from $this->arguments; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/ArgumentsValues.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/ArgumentsValues.php new file mode 100644 index 0000000..f50728a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/ArgumentsValues.php @@ -0,0 +1,142 @@ + + */ +final class ArgumentsValues implements IteratorAggregate, Countable +{ + /** @var array */ + private array $value = []; + + private Arguments $arguments; + + private bool $hasInvalidValue = false; + + private bool $forInterface = false; + + private bool $hadSingleArgument = false; + + private function __construct(Arguments $arguments) + { + $this->arguments = $arguments; + } + + public static function forInterface(Arguments $arguments, Shell $shell): self + { + $self = new self($arguments); + $self->forInterface = true; + + if (count($arguments) > 0) { + $self->transform($shell); + } + + return $self; + } + + public static function forClass(Arguments $arguments, Shell $shell): self + { + $self = new self($arguments); + $self->transform($shell); + + return $self; + } + + public function hasInvalidValue(): bool + { + return $this->hasInvalidValue; + } + + public function hasValue(string $name): bool + { + return array_key_exists($name, $this->value); + } + + public function getValue(string $name): mixed + { + return $this->value[$name]; + } + + public function hadSingleArgument(): bool + { + return $this->hadSingleArgument; + } + + private function transform(Shell $shell): void + { + $value = $shell->value(); + + $transformedValue = $this->transformValueForSingleArgument($value, $shell->allowSuperfluousKeys()); + + if (! is_array($transformedValue)) { + $this->hasInvalidValue = true; + + return; + } + + if ($transformedValue !== $value) { + $this->hadSingleArgument = true; + } + + foreach ($this->arguments as $argument) { + $name = $argument->name(); + + if (! array_key_exists($name, $transformedValue) && ! $argument->isRequired()) { + $transformedValue[$name] = $argument->defaultValue(); + } + } + + $this->value = $transformedValue; + } + + private function transformValueForSingleArgument(mixed $value, bool $allowSuperfluousKeys): mixed + { + if (count($this->arguments) !== 1) { + return $value; + } + + $argument = $this->arguments->at(0); + $name = $argument->name(); + $type = $argument->type(); + $isTraversableAndAllowsStringKeys = $type instanceof CompositeTraversableType + && $type->keyType() !== ArrayKeyType::integer(); + + if (is_array($value) && array_key_exists($name, $value)) { + if ($this->forInterface || ! $isTraversableAndAllowsStringKeys || $allowSuperfluousKeys || count($value) === 1) { + return $value; + } + } + + if ($value === [] && ! $isTraversableAndAllowsStringKeys) { + return $value; + } + + return [$name => $value]; + } + + public function count(): int + { + return count($this->arguments); + } + + public function getIterator(): Traversable + { + yield from $this->arguments; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Constructor.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Constructor.php new file mode 100644 index 0000000..4e3550a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Constructor.php @@ -0,0 +1,45 @@ +mapper() + * ->map(Email::class, [ + * 'userName' => 'john.doe', + * 'domainName' => 'example.com', + * ]); // john.doe@example.com + * ``` + * + * @api + */ +#[Attribute(Attribute::TARGET_METHOD)] +final class Constructor {} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/DateTimeFormatConstructor.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/DateTimeFormatConstructor.php new file mode 100644 index 0000000..83f8b6d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/DateTimeFormatConstructor.php @@ -0,0 +1,63 @@ +registerConstructor(new DateTimeFormatConstructor(DATE_COOKIE, DATE_ATOM)) + * ->mapper() + * ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); + * ``` + * + * @internal + */ +final class DateTimeFormatConstructor +{ + /** @var non-empty-list */ + private array $formats; + + /** + * @no-named-arguments + * @param non-empty-string $format + * @param non-empty-string ...$formats + */ + public function __construct(string $format, string ...$formats) + { + $this->formats = [$format, ...$formats]; + } + + /** + * @param class-string $className + * @param non-empty-string|int|float $value + */ + #[DynamicConstructor] + public function __invoke(string $className, string|int|float $value): DateTimeInterface + { + foreach ($this->formats as $format) { + $date = $className::createFromFormat($format, (string)$value) ?: null; + + if ($date) { + return $date; + } + } + + throw new CannotParseToDateTime($this->formats); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/DynamicConstructor.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/DynamicConstructor.php new file mode 100644 index 0000000..c417fc6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/DynamicConstructor.php @@ -0,0 +1,51 @@ +registerConstructor( + * #[\OCA\Talk\Vendor\CuyZ\Valinor\Attribute\DynamicConstructor] + * function (string $className, string $value): SomeInterfaceWithStaticConstructor { + * return $className::from($value); + * } + * ) + * ->mapper() + * ->map(SomeClassWithInheritedStaticConstructor::class, 'foo'); + * ``` + * + * @api + */ +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)] +final class DynamicConstructor {} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotFindObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotFindObjectBuilder.php new file mode 100644 index 0000000..d031cbd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotFindObjectBuilder.php @@ -0,0 +1,68 @@ + */ + private array $parameters; + + /** + * @param non-empty-list $builders + */ + public function __construct(array $builders) + { + $this->parameters = [ + 'allowed_types' => (function () use ($builders) { + $signatures = []; + $sortedSignatures = []; + + foreach ($builders as $builder) { + $arguments = $builder->describeArguments(); + $count = count($arguments); + $signature = TypeHelper::dumpArguments($arguments); + + $signatures[$count][$signature] = null; + } + + ksort($signatures); + + foreach ($signatures as $list) { + foreach (array_keys($list) as $signature) { + $sortedSignatures[] = $signature; + } + } + + return implode(', ', $sortedSignatures); + })(), + ]; + + parent::__construct(StringFormatter::for($this), 1642183169); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotInstantiateObject.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotInstantiateObject.php new file mode 100644 index 0000000..7a6f3f5 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotInstantiateObject.php @@ -0,0 +1,20 @@ +name}`.", + 1646916477 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotParseToDateTime.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotParseToDateTime.php new file mode 100644 index 0000000..b8b79cb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/CannotParseToDateTime.php @@ -0,0 +1,41 @@ + */ + private array $parameters; + + /** + * @param non-empty-list $formats + */ + public function __construct(array $formats) + { + $this->parameters = [ + 'formats' => '`' . implode('`, `', $formats) . '`', + ]; + + parent::__construct(StringFormatter::for($this), 1630686564); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorClassTypeParameter.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorClassTypeParameter.php new file mode 100644 index 0000000..a2aa36a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorClassTypeParameter.php @@ -0,0 +1,21 @@ +toString()}` for the first parameter of the constructor `{$function->signature}`, it should be of type `class-string`.", + 1661517000 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorMethodWithAttributeReturnType.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorMethodWithAttributeReturnType.php new file mode 100644 index 0000000..2952147 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorMethodWithAttributeReturnType.php @@ -0,0 +1,27 @@ +returnType instanceof UnresolvableType) { + $message = $method->returnType->message(); + } else { + $message = "Invalid return type `{$method->returnType->toString()}` for constructor `{$method->signature}`, it must be `$expectedClassName`."; + } + + parent::__construct($message, 1708104783); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorReturnType.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorReturnType.php new file mode 100644 index 0000000..1b94dbd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidConstructorReturnType.php @@ -0,0 +1,24 @@ +returnType instanceof UnresolvableType) { + $message = $function->returnType->message(); + } else { + $message = "Invalid return type `{$function->returnType->toString()}` for constructor `{$function->signature}`, it must be a valid class name."; + } + + parent::__construct($message, 1659446121); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidSource.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidSource.php new file mode 100644 index 0000000..c4e8719 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/InvalidSource.php @@ -0,0 +1,44 @@ + */ + private array $parameters; + + public function __construct(mixed $source, Arguments $arguments) + { + $this->parameters = [ + 'expected_type' => TypeHelper::dumpArguments($arguments), + ]; + + $this->body = $source === null + ? 'Cannot be empty and must be filled with a value matching type {expected_type}.' + : 'Value {source_value} does not match type {expected_type}.'; + + parent::__construct(StringFormatter::for($this), 1632903281); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/MissingConstructorClassTypeParameter.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/MissingConstructorClassTypeParameter.php new file mode 100644 index 0000000..3ff1297 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/MissingConstructorClassTypeParameter.php @@ -0,0 +1,20 @@ +signature}`.", + 1661516853 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/ObjectBuildersCollision.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/ObjectBuildersCollision.php new file mode 100644 index 0000000..f847302 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/ObjectBuildersCollision.php @@ -0,0 +1,20 @@ +signature()}` and `{$builderB->signature()}`.", + 1654955787 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/PermissiveTypeNotAllowed.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/PermissiveTypeNotAllowed.php new file mode 100644 index 0000000..e35730d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Exception/PermissiveTypeNotAllowed.php @@ -0,0 +1,21 @@ +toString()}`, which is not " . + "allowed in strict mode. If really needed, the `allowPermissiveTypes` setting can be used.", + 1655389255, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/CacheObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/CacheObjectBuilderFactory.php new file mode 100644 index 0000000..749d2ef --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/CacheObjectBuilderFactory.php @@ -0,0 +1,36 @@ +> */ + private CacheInterface $cache + ) {} + + public function for(ClassDefinition $class): array + { + $signature = $class->type->toString(); + + $entry = $this->cache->get($signature); + + if ($entry) { + return $entry; + } + + $builders = $this->delegate->for($class); + + $this->cache->set($signature, $builders); + + return $builders; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php new file mode 100644 index 0000000..3a69711 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php @@ -0,0 +1,197 @@ + */ + private array $filteredConstructors; + + public function __construct( + private ObjectBuilderFactory $delegate, + /** @var array */ + private array $nativeConstructors, + private FunctionsContainer $constructors, + ) {} + + public function for(ClassDefinition $class): array + { + $builders = $this->builders($class); + + if (count($builders) === 0) { + if ($class->methods->hasConstructor()) { + throw new CannotInstantiateObject($class); + } + + return $this->delegate->for($class); + } + + return $builders; + } + + /** + * @return list + */ + private function builders(ClassDefinition $class): array + { + $className = $class->name; + $classType = $class->type; + $methods = $class->methods; + + $builders = []; + + foreach ($this->filteredConstructors() as $constructor) { + if (! $this->constructorMatches($constructor, $classType)) { + continue; + } + + $definition = $constructor->definition; + $functionClass = $definition->class; + + if ($functionClass && $definition->isStatic && ! $definition->isClosure) { + $scopedClass = is_a($className, $functionClass, true) ? $className : $functionClass; + + $builders[$definition->signature] = new MethodObjectBuilder($scopedClass, $definition->name, $definition->parameters); + } else { + $builders[$definition->signature] = new FunctionObjectBuilder($constructor, $classType); + } + } + + foreach ($methods as $method) { + if (! $method->isStatic) { + continue; + } + + if (! $method->attributes->has(Constructor::class)) { + continue; + } + + if (! $method->returnType instanceof ClassType) { + throw new InvalidConstructorMethodWithAttributeReturnType($className, $method); + } + + if (! is_a($className, $method->returnType->className(), true)) { + throw new InvalidConstructorMethodWithAttributeReturnType($className, $method); + } + + if (! $class->type->matches($method->returnType)) { + continue; + } + + $builders[$method->signature] = new MethodObjectBuilder($className, $method->name, $method->parameters); + } + + if ($classType instanceof EnumType) { + $buildersWithOneArguments = array_filter($builders, fn (ObjectBuilder $builder) => $builder->describeArguments()->count() === 1); + + if (count($buildersWithOneArguments) === 0) { + $builders[] = new NativeEnumObjectBuilder($classType); + } + } elseif ($methods->hasConstructor() + && $methods->constructor()->isPublic + && ( + count($builders) === 0 + || $methods->constructor()->attributes->has(Constructor::class) + || array_key_exists($className, $this->nativeConstructors) + ) + ) { + $builders[] = new NativeConstructorObjectBuilder($class); + } + + return array_values($builders); + } + + private function constructorMatches(FunctionObject $function, ObjectType $classType): bool + { + $definition = $function->definition; + + if (! $classType->matches($definition->returnType)) { + return false; + } + + if (! $definition->attributes->has(DynamicConstructor::class)) { + return true; + } + + if (count($definition->parameters) === 0) { + throw new MissingConstructorClassTypeParameter($definition); + } + + $parameterType = $definition->parameters->at(0)->type; + + if ($parameterType instanceof NativeStringType) { + $parameterType = ClassStringType::get(); + } + + if (! $parameterType instanceof ClassStringType) { + throw new InvalidConstructorClassTypeParameter($definition, $parameterType); + } + + $subType = $parameterType->subType(); + + if ($subType) { + return $classType->matches($subType); + } + + return true; + } + + /** + * @return list + */ + private function filteredConstructors(): array + { + if (! isset($this->filteredConstructors)) { + $this->filteredConstructors = []; + + foreach ($this->constructors as $constructor) { + $function = $constructor->definition; + + if ($function->class + && Reflection::enumExists($function->class) + && in_array($function->name, ['from', 'tryFrom'], true) + ) { + continue; + } + + if (! $function->returnType instanceof ObjectType) { + throw new InvalidConstructorReturnType($function); + } + + $this->filteredConstructors[] = $constructor; + } + } + + return $this->filteredConstructors; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php new file mode 100644 index 0000000..69294eb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php @@ -0,0 +1,62 @@ + */ + private array $supportedDateFormats, + private FunctionDefinitionRepository $functionDefinitionRepository + ) {} + + public function for(ClassDefinition $class): array + { + $className = $class->name; + + $builders = $this->delegate->for($class); + + if ($className !== DateTime::class && $className !== DateTimeImmutable::class) { + return $builders; + } + + // Remove `DateTime` & `DateTimeImmutable` native constructors + $builders = array_filter($builders, fn (ObjectBuilder $builder) => ! $builder instanceof NativeConstructorObjectBuilder); + + $buildersWithOneArgument = array_filter($builders, fn (ObjectBuilder $builder) => count($builder->describeArguments()) === 1); + + if (count($buildersWithOneArgument) === 0 || $this->supportedDateFormats !== Settings::DEFAULT_SUPPORTED_DATETIME_FORMATS) { + $builders[] = $this->internalDateTimeBuilder($class->type); + } + + /** @var non-empty-list */ + return $builders; + } + + private function internalDateTimeBuilder(ObjectType $type): FunctionObjectBuilder + { + $constructor = new DateTimeFormatConstructor(...$this->supportedDateFormats); + $function = new FunctionObject($this->functionDefinitionRepository->for($constructor), $constructor); + + return new FunctionObjectBuilder($function, $type); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php new file mode 100644 index 0000000..e05b853 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php @@ -0,0 +1,78 @@ +delegate = $delegate; + $this->functionDefinitionRepository = $functionDefinitionRepository; + } + + public function for(ClassDefinition $class): array + { + $builders = $this->delegate->for($class); + + if ($class->name !== DateTimeZone::class) { + return $builders; + } + + // Remove `DateTimeZone` native constructors + $builders = array_filter($builders, fn (ObjectBuilder $builder) => ! $builder instanceof NativeConstructorObjectBuilder); + + $useDefaultBuilder = true; + + foreach ($builders as $builder) { + if (count($builder->describeArguments()) === 1) { + $useDefaultBuilder = false; + // @infection-ignore-all + break; + } + } + + if ($useDefaultBuilder) { + // @infection-ignore-all / Ignore memoization + $builders[] = $this->defaultBuilder($class->type); + } + + /** @var non-empty-list */ + return $builders; + } + + private function defaultBuilder(ObjectType $type): FunctionObjectBuilder + { + $constructor = function (string $timezone) { + try { + return new DateTimeZone($timezone); + } catch (Exception) { + throw MessageBuilder::newError('Value {source_value} is not a valid timezone.')->build(); + } + }; + + $function = new FunctionObject($this->functionDefinitionRepository->for($constructor), $constructor); + + return new FunctionObjectBuilder($function, $type); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ObjectBuilderFactory.php new file mode 100644 index 0000000..5dc098b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ObjectBuilderFactory.php @@ -0,0 +1,17 @@ + + */ + public function for(ClassDefinition $class): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php new file mode 100644 index 0000000..cb24db9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php @@ -0,0 +1,17 @@ +delegate->for($class); + + $sortedByArgumentsNumber = []; + $sortedByPriority = []; + + foreach ($builders as $builder) { + $sortedByArgumentsNumber[$builder->describeArguments()->count()][] = $builder; + } + + krsort($sortedByArgumentsNumber); + + foreach ($sortedByArgumentsNumber as $sortedBuilders) { + usort($sortedBuilders, $this->sortObjectBuilders(...)); + + $sortedByPriority = array_merge($sortedByPriority, $sortedBuilders); + } + + return $sortedByPriority; + } + + private function sortObjectBuilders(ObjectBuilder $builderA, ObjectBuilder $builderB): int + { + $argumentsA = $builderA->describeArguments()->toArray(); + $argumentsB = $builderB->describeArguments()->toArray(); + + $sharedArguments = array_keys(array_intersect_key($argumentsA, $argumentsB)); + + $winner = null; + + foreach ($sharedArguments as $name) { + $typeA = $argumentsA[$name]->type(); + $typeB = $argumentsB[$name]->type(); + + $score = $this->sortTypes($typeA, $typeB); + + if ($score === 0) { + continue; + } + + $newWinner = $score === 1 ? $builderB : $builderA; + + if ($winner && $winner !== $newWinner) { + throw new ObjectBuildersCollision($builderA, $builderB); + } + + $winner = $newWinner; + } + + if ($winner === null && count($sharedArguments) === count($argumentsA)) { + throw new ObjectBuildersCollision($builderA, $builderB); + } + + // @infection-ignore-all / Incrementing or decrementing sorting value makes no sense, so we ignore it. + return $winner === $builderA ? -1 : 1; + } + + private function sortTypes(Type $typeA, Type $typeB): int + { + if ($typeA instanceof ScalarType && $typeB instanceof ScalarType) { + return TypeHelper::typePriority($typeB) <=> TypeHelper::typePriority($typeA); + } + + if (! $typeA instanceof ScalarType) { + // @infection-ignore-all / Decrementing sorting value makes no sense, so we ignore it. + return -1; + } + + return 1; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/StrictTypesObjectBuilderFactory.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/StrictTypesObjectBuilderFactory.php new file mode 100644 index 0000000..1074613 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/Factory/StrictTypesObjectBuilderFactory.php @@ -0,0 +1,47 @@ +delegate->for($class); + + foreach ($builders as $builder) { + $arguments = $builder->describeArguments(); + + foreach ($arguments as $argument) { + $this->checkPresenceOfPermissiveType($argument, $argument->type()); + } + } + + return $builders; + } + + private function checkPresenceOfPermissiveType(Argument $argument, Type $type): void + { + if ($type instanceof CompositeType) { + foreach ($type->traverse() as $subType) { + self::checkPresenceOfPermissiveType($argument, $subType); + } + } + + if ($type instanceof MixedType || $type instanceof UndefinedObjectType) { + throw new PermissiveTypeNotAllowed($argument->signature(), $type); + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/FunctionObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/FunctionObjectBuilder.php new file mode 100644 index 0000000..87533d7 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/FunctionObjectBuilder.php @@ -0,0 +1,74 @@ +definition; + + $arguments = array_map( + fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter), + array_values([...$definition->parameters]) + ); + + $this->isDynamicConstructor = $definition->attributes->has(DynamicConstructor::class); + + if ($this->isDynamicConstructor) { + array_shift($arguments); + } + + $this->function = $function; + $this->className = $type->className(); + $this->arguments = new Arguments(...$arguments); + } + + public function describeArguments(): Arguments + { + return $this->arguments; + } + + public function build(array $arguments): object + { + $parameters = $this->function->definition->parameters; + + if ($this->isDynamicConstructor) { + $arguments[$parameters->at(0)->name] = $this->className; + } + + $arguments = new MethodArguments($parameters, $arguments); + + try { + /** @var object */ + return ($this->function->callback)(...$arguments); + } catch (Exception $exception) { + throw UserlandError::from($exception); + } + } + + public function signature(): string + { + return $this->function->definition->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodArguments.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodArguments.php new file mode 100644 index 0000000..5b6706c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodArguments.php @@ -0,0 +1,43 @@ + + */ +final class MethodArguments implements IteratorAggregate +{ + /** @var list */ + private array $arguments = []; + + /** + * @param array $arguments + */ + public function __construct(Parameters $parameters, array $arguments) + { + foreach ($parameters as $parameter) { + $name = $parameter->name; + + if ($parameter->isVariadic) { + $this->arguments = [...$this->arguments, ...array_values($arguments[$name])]; // @phpstan-ignore-line we know that the argument is iterable + } else { + $this->arguments[] = $arguments[$name]; + } + } + } + + public function getIterator(): Traversable + { + yield from $this->arguments; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodObjectBuilder.php new file mode 100644 index 0000000..9cb72f4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/MethodObjectBuilder.php @@ -0,0 +1,43 @@ +arguments ??= Arguments::fromParameters($this->parameters); + } + + public function build(array $arguments): object + { + $methodName = $this->methodName; + $arguments = new MethodArguments($this->parameters, $arguments); + + try { + return ($this->className)::$methodName(...$arguments); // @phpstan-ignore-line + } catch (Exception $exception) { + throw UserlandError::from($exception); + } + } + + public function signature(): string + { + return "$this->className::$this->methodName()"; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeConstructorObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeConstructorObjectBuilder.php new file mode 100644 index 0000000..f4aacf6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeConstructorObjectBuilder.php @@ -0,0 +1,39 @@ +arguments ??= Arguments::fromParameters($this->class->methods->constructor()->parameters); + } + + public function build(array $arguments): object + { + $className = $this->class->name; + $arguments = new MethodArguments($this->class->methods->constructor()->parameters, $arguments); + + try { + return new $className(...$arguments); + } catch (Exception $exception) { + throw UserlandError::from($exception); + } + } + + public function signature(): string + { + return $this->class->methods->constructor()->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeEnumObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeEnumObjectBuilder.php new file mode 100644 index 0000000..286c1b6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/NativeEnumObjectBuilder.php @@ -0,0 +1,51 @@ +cases() as $case) { + $value = $case instanceof BackedEnum ? $case->value : $case->name; + + $types[] = ValueTypeFactory::from($value); + } + + $argumentType = count($types) === 1 + ? $types[0] + : new UnionType(...$types); + + $this->enum = $type; + $this->arguments = new Arguments( + new Argument('value', $type->className() . '::$value', $argumentType) + ); + } + + public function describeArguments(): Arguments + { + return $this->arguments; + } + + public function build(array $arguments): object + { + return $this->enum->cases()[$arguments['value']]; + } + + public function signature(): string + { + return $this->enum->readableSignature(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/ObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/ObjectBuilder.php new file mode 100644 index 0000000..a46c900 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/ObjectBuilder.php @@ -0,0 +1,21 @@ + $arguments + */ + public function build(array $arguments): object; + + /** + * @return non-empty-string + */ + public function signature(): string; +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Object/ReflectionObjectBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Object/ReflectionObjectBuilder.php new file mode 100644 index 0000000..4d2a2dd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Object/ReflectionObjectBuilder.php @@ -0,0 +1,42 @@ +arguments ??= Arguments::fromProperties($this->class->properties); + } + + public function build(array $arguments): object + { + $object = new ($this->class->name)(); + + if (count($arguments) > 0) { + (function () use ($arguments): void { + foreach ($arguments as $name => $value) { + $this->{$name} = $value; // @phpstan-ignore-line + } + })->call($object); + } + + return $object; + } + + public function signature(): string + { + return $this->class->name . ' (properties)'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/FileExtensionNotHandled.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/FileExtensionNotHandled.php new file mode 100644 index 0000000..196a98d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/FileExtensionNotHandled.php @@ -0,0 +1,19 @@ +source; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/InvalidSource.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/InvalidSource.php new file mode 100644 index 0000000..f185088 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/InvalidSource.php @@ -0,0 +1,13 @@ +source; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/SourceNotIterable.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/SourceNotIterable.php new file mode 100644 index 0000000..19a4d06 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/SourceNotIterable.php @@ -0,0 +1,24 @@ +source; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/UnableToReadFile.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/UnableToReadFile.php new file mode 100644 index 0000000..e908d96 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Exception/UnableToReadFile.php @@ -0,0 +1,19 @@ + + */ +final class FileSource implements IteratorAggregate, IdentifiableSource +{ + private string $filePath; + + /** @var Traversable */ + private Traversable $delegate; + + public function __construct(SplFileObject $file) + { + $this->filePath = $file->getPathname(); + + $content = $file->fread($file->getSize()); + + /** @infection-ignore-all */ + if ($content === false || $content === '') { + throw new UnableToReadFile($this->filePath); + } + + $this->delegate = match (strtolower($file->getExtension())) { + 'json' => new JsonSource($content), + 'yaml', 'yml' => new YamlSource($content), + default => throw new FileExtensionNotHandled($file->getExtension()), + }; + } + + public function sourceName(): string + { + return $this->filePath; + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + yield from $this->delegate; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/IdentifiableSource.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/IdentifiableSource.php new file mode 100644 index 0000000..9aa2b6e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/IdentifiableSource.php @@ -0,0 +1,11 @@ + + */ +final class JsonSource implements IteratorAggregate +{ + /** @var iterable */ + private iterable $source; + + /** + * @throws InvalidSource + */ + public function __construct(string $jsonSource) + { + try { + $source = json_decode($jsonSource, associative: true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new InvalidJson($jsonSource, $e); + } + + if (! is_iterable($source)) { + throw new SourceNotIterable($jsonSource); + } + + $this->source = $source; + } + + /** + * @return Iterator + */ + public function getIterator(): Traversable + { + yield from $this->source; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/CamelCaseKeys.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/CamelCaseKeys.php new file mode 100644 index 0000000..c7af2c3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/CamelCaseKeys.php @@ -0,0 +1,68 @@ + + */ +final class CamelCaseKeys implements IteratorAggregate +{ + /** @var array */ + private array $source; + + /** + * @param iterable $source + */ + public function __construct(iterable $source) + { + $this->source = $this->replace($source); + } + + /** + * @param iterable $source + * @return array + */ + private function replace(iterable $source): array + { + $result = []; + + foreach ($source as $key => $value) { + if (is_iterable($value)) { + $value = $this->replace($value); + } + + if (! is_string($key)) { + $result[$key] = $value; + continue; + } + + $camelCaseKey = $this->camelCaseKeys($key); + + if (isset($result[$camelCaseKey])) { + continue; + } + + $result[$camelCaseKey] = $value; + } + + return $result; + } + + private function camelCaseKeys(string $key): string + { + return lcfirst(str_replace([' ', '_', '-'], '', ucwords($key, ' _-'))); + } + + public function getIterator(): Traversable + { + yield from $this->source; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/Mapping.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/Mapping.php new file mode 100644 index 0000000..67b4d57 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/Mapping.php @@ -0,0 +1,44 @@ + */ + private array $keys; + + private string $to; + + private int $depth; + + /** + * @param array $keys + */ + public function __construct(array $keys, string $to) + { + $this->keys = $keys; + $this->to = $to; + $this->depth = count($keys) - 1; + } + + public function matches(int|string $key, int $atDepth): bool + { + $from = $this->keys[$atDepth] ?? null; + + return $from === (string)$key || $from === '*'; + } + + public function findMappedKey(int|string $key, int $atDepth): ?string + { + if ($atDepth < $this->depth + || !$this->matches($key, $atDepth) + ) { + return null; + } + + return $this->to; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/PathMapping.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/PathMapping.php new file mode 100644 index 0000000..f4f9374 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Modifier/PathMapping.php @@ -0,0 +1,94 @@ + + */ +final class PathMapping implements IteratorAggregate +{ + /** @var array */ + private array $source; + + /** + * @param iterable $source + * @param array $map + */ + public function __construct(iterable $source, array $map) + { + $this->source = $this->map($source, $this->prepareMappings($map)); + } + + public function getIterator(): Traversable + { + yield from $this->source; + } + + /** + * @param iterable $source + * @param array $mappings + * @return array + */ + private function map(iterable $source, array $mappings, int $depth = 0): array + { + $out = []; + + foreach ($source as $key => $value) { + /** @var int|string $key */ + $newMappings = array_filter($mappings, fn (Mapping $mapping) => $mapping->matches($key, $depth)); + + $newKey = $this->findMapping($newMappings, $depth, $key); + + if (is_array($value)) { + $out[$newKey] = $this->map($value, $newMappings, $depth + 1); + + continue; + } + + $out[$newKey] = $value; + } + + return $out; + } + + /** + * @param array $map + * @return array + */ + private function prepareMappings(array $map): array + { + $mappings = []; + + foreach ($map as $from => $to) { + $mappings[] = new Mapping(explode('.', (string)$from), $to); + } + + return $mappings; + } + + /** + * @param array $mappings + */ + private function findMapping(array $mappings, int $atDepth, int|string $key): int|string + { + foreach ($mappings as $mapping) { + $mappedKey = $mapping->findMappedKey($key, $atDepth); + + if (null !== $mappedKey) { + return $mappedKey; + } + } + + return $key; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/Source.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Source.php new file mode 100644 index 0000000..d1f9ce2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/Source.php @@ -0,0 +1,80 @@ + + */ +final class Source implements IteratorAggregate +{ + private function __construct( + /** @var iterable */ + private iterable $delegate + ) {} + + /** + * @param iterable $data + */ + public static function iterable(iterable $data): Source + { + return new Source($data); + } + + /** + * @param array $data + */ + public static function array(array $data): Source + { + return new Source($data); + } + + /** + * @throws InvalidSource + */ + public static function json(string $jsonSource): Source + { + return new Source(new JsonSource($jsonSource)); + } + + /** + * @throws InvalidSource + */ + public static function yaml(string $yamlSource): Source + { + return new Source(new YamlSource($yamlSource)); + } + + public static function file(SplFileObject $file): Source + { + return new Source(new FileSource($file)); + } + + public function camelCaseKeys(): Source + { + return new Source(new CamelCaseKeys($this)); + } + + /** + * @param array $map + */ + public function map(array $map): Source + { + return new Source(new PathMapping($this, $map)); + } + + public function getIterator(): Traversable + { + yield from $this->delegate; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Source/YamlSource.php b/lib/Vendor/CuyZ/Valinor/Mapper/Source/YamlSource.php new file mode 100644 index 0000000..ce8fddb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Source/YamlSource.php @@ -0,0 +1,62 @@ + + */ +final class YamlSource implements IteratorAggregate +{ + /** @var iterable */ + private iterable $source; + + /** + * @throws InvalidSource + */ + public function __construct(string $yamlSource) + { + /** @infection-ignore-all */ + // @codeCoverageIgnoreStart + if (! function_exists('yaml_parse')) { + throw new YamlExtensionNotEnabled(); + } + // @codeCoverageIgnoreEnd + + $source = @yaml_parse($yamlSource); + + if ($source === false) { + throw new InvalidYaml($yamlSource); + } + + if (! is_iterable($source)) { + throw new SourceNotIterable($yamlSource); + } + + $this->source = $source; + } + + /** + * @return Iterator + */ + public function getIterator(): Traversable + { + yield from $this->source; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ArrayNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ArrayNodeBuilder.php new file mode 100644 index 0000000..6cb6baf --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ArrayNodeBuilder.php @@ -0,0 +1,92 @@ +type(); + $value = $shell->value(); + + assert($type instanceof ArrayType || $type instanceof NonEmptyArrayType || $type instanceof IterableType); + + if ($shell->allowUndefinedValues() && $value === null) { + return TreeNode::branch($shell, [], []); + } + + if (! is_iterable($value)) { + return TreeNode::error($shell, new SourceMustBeIterable($value, $type)); + } + + $children = $this->children($type, $shell, $rootBuilder); + $array = $this->buildArray($children); + + return TreeNode::branch($shell, $array, $children); + } + + /** + * @return array + */ + private function children(CompositeTraversableType $type, Shell $shell, RootNodeBuilder $rootBuilder): array + { + /** @var iterable $values */ + $values = $shell->value(); + $keyType = $type->keyType(); + $subType = $type->subType(); + + $children = []; + + foreach ($values as $key => $value) { + if (! is_string($key) && ! is_int($key)) { + throw new InvalidIterableKeyType($key, $shell->path()); + } + + $child = $shell->child((string)$key, $subType); + + if (! $keyType->accepts($key)) { + $children[$key] = TreeNode::error($child, new InvalidTraversableKey($key, $keyType)); + } else { + $children[$key] = $rootBuilder->build($child->withValue($value)); + } + } + + return $children; + } + + /** + * @param array $children + * @return mixed[]|null + */ + private function buildArray(array $children): ?array + { + $array = []; + + foreach ($children as $key => $child) { + if (! $child->isValid()) { + return null; + } + + $array[$key] = $child->value(); + } + + return $array; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/InterfaceNodeBuilder.php new file mode 100644 index 0000000..64705d0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/InterfaceNodeBuilder.php @@ -0,0 +1,144 @@ +type(); + + if (! $type instanceof InterfaceType && ! $type instanceof NativeClassType) { + return $this->delegate->build($shell, $rootBuilder); + } + + if ($type->accepts($shell->value())) { + return TreeNode::leaf($shell, $shell->value()); + } + + if ($this->constructorRegisteredFor($type)) { + if ($this->implementations->has($type->className())) { + throw new InterfaceHasBothConstructorAndInfer($type->className()); + } + + return $this->delegate->build($shell, $rootBuilder); + } + + if ($shell->allowUndefinedValues() && $shell->value() === null) { + $shell = $shell->withValue([]); + } else { + $shell = $shell->transformIteratorToArray(); + } + + $className = $type->className(); + + if (! $this->implementations->has($className)) { + if ($type instanceof InterfaceType || $this->classDefinitionRepository->for($type)->isAbstract) { + throw new CannotResolveObjectType($className); + } + + return $this->delegate->build($shell, $rootBuilder); + } + + $function = $this->implementations->function($className); + $arguments = Arguments::fromParameters($function->parameters); + + if ($type instanceof NativeClassType && $this->classDefinitionRepository->for($type)->isFinal) { + throw new CannotInferFinalClass($type, $function); + } + + $argumentsValues = ArgumentsValues::forInterface($arguments, $shell); + + if ($argumentsValues->hasInvalidValue()) { + return TreeNode::error($shell, new InvalidSource($shell->value(), $arguments)); + } + + $children = $this->children($shell, $argumentsValues, $rootBuilder); + + $values = []; + + foreach ($children as $child) { + if (! $child->isValid()) { + return TreeNode::branch($shell, null, $children); + } + + $values[] = $child->value(); + } + + try { + $classType = $this->implementations->implementation($className, $values); + } catch (ObjectImplementationCallbackError $exception) { + $exception = ($this->exceptionFilter)($exception->original()); + + return TreeNode::error($shell, $exception); + } + + $shell = $shell->withType($classType); + $shell = $shell->withAllowedSuperfluousKeys($arguments->names()); + + return $this->delegate->build($shell, $rootBuilder); + } + + private function constructorRegisteredFor(Type $type): bool + { + foreach ($this->constructors as $constructor) { + if ($type->matches($constructor->definition->returnType)) { + return true; + } + } + + return false; + } + + /** + * @return array + */ + private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array + { + $children = []; + + foreach ($arguments as $argument) { + $name = $argument->name(); + $type = $argument->type(); + $attributes = $argument->attributes(); + + $child = $shell->child($name, $type, $attributes); + + if ($arguments->hasValue($name)) { + $child = $child->withValue($arguments->getValue($name)); + } + + $children[] = $rootBuilder->build($child); + } + + return $children; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ListNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ListNodeBuilder.php new file mode 100644 index 0000000..c2e20ba --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ListNodeBuilder.php @@ -0,0 +1,93 @@ +type(); + $value = $shell->value(); + + assert($type instanceof ListType || $type instanceof NonEmptyListType); + + if ($shell->allowUndefinedValues() && $value === null) { + return TreeNode::branch($shell, [], []); + } + + if (! is_iterable($value)) { + return TreeNode::error($shell, new SourceMustBeIterable($value, $type)); + } + + $children = $this->children($type, $shell, $rootBuilder); + $array = $this->buildArray($children); + + return TreeNode::branch($shell, $array, $children); + } + + /** + * @return array + */ + private function children(CompositeTraversableType $type, Shell $shell, RootNodeBuilder $rootBuilder): array + { + /** @var iterable $values */ + $values = $shell->value(); + $subType = $type->subType(); + + $expected = 0; + $children = []; + + foreach ($values as $key => $value) { + if (! is_string($key) && ! is_int($key)) { + throw new InvalidIterableKeyType($key, $shell->path()); + } + + if ($shell->allowNonSequentialList() || $key === $expected) { + $child = $shell->child((string)$expected, $subType); + $children[$expected] = $rootBuilder->build($child->withValue($value)); + } else { + $child = $shell->child((string)$key, $subType); + $children[$key] = TreeNode::error($child, new InvalidListKey($key, $expected)); + } + + $expected++; + } + + return $children; + } + + /** + * @param array $children + * @return mixed[]|null + */ + private function buildArray(array $children): ?array + { + $array = []; + + foreach ($children as $key => $child) { + if (! $child->isValid()) { + return null; + } + + $array[$key] = $child->value(); + } + + return $array; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/MixedNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/MixedNodeBuilder.php new file mode 100644 index 0000000..baa0abc --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/MixedNodeBuilder.php @@ -0,0 +1,24 @@ +type() instanceof MixedType); + + if (! $shell->allowPermissiveTypes()) { + throw new CannotMapToPermissiveType($shell); + } + + return TreeNode::leaf($shell, $shell->value()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/NodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/NodeBuilder.php new file mode 100644 index 0000000..31dd4b9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/NodeBuilder.php @@ -0,0 +1,11 @@ +type(); + $value = $shell->value(); + + assert($type instanceof NullType); + + if ($value !== null) { + return TreeNode::error($shell, new SourceIsNotNull()); + } + + return TreeNode::leaf($shell, null); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectImplementations.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectImplementations.php new file mode 100644 index 0000000..8f1cffe --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectImplementations.php @@ -0,0 +1,151 @@ +> */ + private array $implementations = []; + + public function __construct( + private FunctionsContainer $functions, + private TypeParser $typeParser + ) {} + + public function has(string $name): bool + { + return $this->functions->has($name); + } + + public function function(string $name): FunctionDefinition + { + return $this->functions->get($name)->definition; + } + + /** + * @param mixed[] $arguments + */ + public function implementation(string $name, array $arguments): ClassType + { + /** @infection-ignore-all / We cannot test the assignment */ + $this->implementations[$name] ??= $this->implementations($name); + + $class = $this->call($name, $arguments); + + return $this->implementations[$name][$class] + ?? throw new ObjectImplementationNotRegistered($class, $name, $this->implementations[$name]); + } + + /** + * @param mixed[] $arguments + */ + private function call(string $name, array $arguments): string + { + try { + $signature = ($this->functions->get($name)->callback)(...$arguments); + } catch (Exception $exception) { + throw new ObjectImplementationCallbackError($name, $exception); + } + + if (! is_string($signature)) { + throw new InvalidResolvedImplementationValue($name, $signature); + } + + return $signature; + } + + /** + * @return non-empty-array + */ + private function implementations(string $name): array + { + $function = $this->functions->get($name)->definition; + + $type = $this->typeParser->parse($name); + + /** @infection-ignore-all */ + assert($type instanceof InterfaceType || $type instanceof ClassType); + + $classes = $this->implementationsByReturnSignature($name, $function); + + if ($classes === []) { + throw new MissingObjectImplementationRegistration($name, $function); + } + + foreach ($classes as $classType) { + if (! $classType instanceof ClassType || ! $classType->matches($type)) { + throw new ResolvedImplementationIsNotAccepted($name, $classType); + } + } + + /** @var non-empty-array $classes */ + return $classes; + } + + /** + * @return array + */ + private function implementationsByReturnSignature(string $name, FunctionDefinition $function): array + { + $returnType = $function->returnType; + + if (! $returnType instanceof ClassStringType && ! $returnType instanceof UnionType) { + if (count($function->parameters) > 0) { + return []; + } + + $class = $this->call($name, []); + $classType = $this->typeParser->parse($class); + + return [$classType->toString() => $classType]; + } + + $types = $returnType instanceof UnionType + ? $returnType->types() + : [$returnType]; + + $classes = []; + + foreach ($types as $type) { + if (! $type instanceof ClassStringType) { + return []; + } + + $subType = $type->subType(); + + if ($subType === null) { + return []; + } + + $subTypes = $subType instanceof UnionType + ? $subType->types() + : [$subType]; + + foreach ($subTypes as $classType) { + $classes[$classType->toString()] = $classType; + } + } + + return $classes; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectNodeBuilder.php new file mode 100644 index 0000000..d67115f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ObjectNodeBuilder.php @@ -0,0 +1,155 @@ +type(); + + // @infection-ignore-all + assert($type instanceof ObjectType); + + if ($type->accepts($shell->value())) { + return TreeNode::leaf($shell, $shell->value()); + } + + if ($shell->allowUndefinedValues() && $shell->value() === null) { + $shell = $shell->withValue([]); + } else { + $shell = $shell->transformIteratorToArray(); + } + + $class = $this->classDefinitionRepository->for($type); + $builders = $this->objectBuilderFactory->for($class); + + foreach ($builders as $builder) { + $argumentsValues = ArgumentsValues::forClass($builder->describeArguments(), $shell); + + if ($argumentsValues->hasInvalidValue()) { + if (count($builders) === 1) { + return TreeNode::error($shell, new InvalidSource($shell->value(), $builder->describeArguments())); + } + + continue; + } + + $children = $this->children($shell, $argumentsValues, $rootBuilder); + + try { + $object = $this->buildObject($builder, $children); + } catch (Message $exception) { + if ($exception instanceof UserlandError) { + $exception = ($this->exceptionFilter)($exception->previous()); + } + + return TreeNode::error($shell, $exception); + } + + if ($argumentsValues->hadSingleArgument()) { + $node = TreeNode::flattenedBranch($shell, $object, $children[0]); + } else { + $node = TreeNode::branch($shell, $object, $children); + $node = $node->checkUnexpectedKeys(); + } + + if ($node->isValid() || count($builders) === 1) { + return $node; + } + } + + return TreeNode::error($shell, new CannotFindObjectBuilder($builders)); + } + + /** + * @return list + */ + private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array + { + $children = []; + + foreach ($arguments as $argument) { + $name = $argument->name(); + $type = $argument->type(); + $attributes = $argument->attributes(); + + $child = $shell->child($name, $type, $attributes); + + if ($arguments->hasValue($name)) { + $child = $child->withValue($arguments->getValue($name)); + } + + // This whole block is needed to detect object circular dependencies + // and prevent infinite loops. + if ($rootBuilder->typeWasSeen($type)) { + // An exception is thrown only when the type of the property is + // literally the same as the type of the object being built. + // Otherwise, the property type might be a union, for instance, + // so we do not want to stop the script execution right away + // because the value might be valid. + if (count($arguments) === 1 && $type instanceof ObjectType) { + throw new CircularDependencyDetected($argument); + } + + $children[] = TreeNode::error($shell, new InvalidNodeValue($type)); + } else { + $childBuilder = $rootBuilder; + + if ($type->matches($shell->type())) { + $childBuilder = $rootBuilder->withTypeAsCurrentRoot($type); + } + + $children[] = $childBuilder->build($child); + } + } + + return $children; + } + + /** + * @param list $children + */ + private function buildObject(ObjectBuilder $builder, array $children): ?object + { + $arguments = []; + + foreach ($children as $child) { + if (! $child->isValid()) { + return null; + } + + $arguments[$child->name()] = $child->value(); + } + + return $builder->build($arguments); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/RootNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/RootNodeBuilder.php new file mode 100644 index 0000000..659370c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/RootNodeBuilder.php @@ -0,0 +1,47 @@ +hasValue()) { + if (! $shell->allowUndefinedValues()) { + return TreeNode::error($shell, new MissingNodeValue($shell->type())); + } + + $shell = $shell->withValue(null); + } + + return $this->root->build($shell, $this); + } + + public function withTypeAsCurrentRoot(Type $type): self + { + $self = clone $this; + $self->currentRootType = $type; + + return $self; + } + + public function typeWasSeen(Type $type): bool + { + return isset($this->currentRootType) + && $type->toString() === $this->currentRootType->toString(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ScalarNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ScalarNodeBuilder.php new file mode 100644 index 0000000..b1c15cb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ScalarNodeBuilder.php @@ -0,0 +1,32 @@ +type(); + $value = $shell->value(); + + assert($type instanceof ScalarType); + + if ($type->accepts($value)) { + return TreeNode::leaf($shell, $value); + } + + if (! $shell->allowScalarValueCasting() || ! $type->canCast($value)) { + return TreeNode::error($shell, $type->errorMessage()); + } + + return TreeNode::leaf($shell, $type->cast($value)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php new file mode 100644 index 0000000..5c73325 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php @@ -0,0 +1,100 @@ +type(); + $value = $shell->value(); + + assert($type instanceof ShapedArrayType); + + if (! is_iterable($value)) { + return TreeNode::error($shell, new SourceMustBeIterable($value, $type)); + } + + $children = $this->children($type, $shell, $rootBuilder); + + $array = $this->buildArray($children); + + $node = TreeNode::branch($shell, $array, $children); + $node = $node->checkUnexpectedKeys(); + + return $node; + } + + /** + * @return array + */ + private function children(ShapedArrayType $type, Shell $shell, RootNodeBuilder $rootBuilder): array + { + /** @var iterable $value */ + $value = $shell->value(); + $elements = $type->elements(); + $children = []; + + if (! is_array($value)) { + $value = iterator_to_array($value); + } + + foreach ($elements as $element) { + $key = $element->key()->value(); + + $child = $shell->child((string)$key, $element->type()); + + if (array_key_exists($key, $value)) { + $child = $child->withValue($value[$key]); + } elseif ($element->isOptional()) { + continue; + } + + $children[$key] = $rootBuilder->build($child); + + unset($value[$key]); + } + + if ($type->isUnsealed()) { + $unsealedShell = $shell->withType($type->unsealedType())->withValue($value); + $unsealedChildren = $rootBuilder->build($unsealedShell)->children(); + + foreach ($unsealedChildren as $unsealedChild) { + $children[$unsealedChild->name()] = $unsealedChild; + } + } + + return $children; + } + + /** + * @param array $children + * @return mixed[]|null + */ + private function buildArray(array $children): ?array + { + $array = []; + + foreach ($children as $key => $child) { + if (! $child->isValid()) { + return null; + } + + $array[$key] = $child->value(); + } + + return $array; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TreeNode.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TreeNode.php new file mode 100644 index 0000000..4794779 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TreeNode.php @@ -0,0 +1,184 @@ + */ + private array $children = []; + + /** @var array */ + private array $messages = []; + + private bool $valid = true; + + private function __construct(Shell $shell, mixed $value) + { + $this->shell = $shell; + $this->value = $value; + } + + public static function leaf(Shell $shell, mixed $value): self + { + $instance = new self($shell, $value); + $instance->check(); + + return $instance; + } + + /** + * @param array $children + */ + public static function branch(Shell $shell, mixed $value, array $children): self + { + $instance = new self($shell, $value); + + foreach ($children as $child) { + $instance->children[$child->name()] = $child; + } + + $instance->check(); + + return $instance; + } + + public static function flattenedBranch(Shell $shell, mixed $value, self $child): self + { + $instance = new self($shell, $value); + $instance->messages = $child->messages; + $instance->children = $child->children; + $instance->valid = $child->valid; + + return $instance; + } + + public static function error(Shell $shell, Throwable&Message $message): self + { + return (new self($shell, null))->withMessage($message); + } + + public function name(): string + { + return $this->shell->name(); + } + + public function type(): Type + { + return $this->shell->type(); + } + + /** + * @return array + */ + public function children(): array + { + return $this->children; + } + + public function isValid(): bool + { + return $this->valid; + } + + public function withValue(mixed $value): self + { + $clone = clone $this; + $clone->value = $value; + $clone->check(); + + return $clone; + } + + public function value(): mixed + { + assert($this->valid, "Trying to get value of an invalid node at path `{$this->shell->path()}`."); + + return $this->value; + } + + public function withMessage(Message $message): self + { + $clone = clone $this; + $clone->messages[] = $message; + $clone->valid = $clone->valid && ! $message instanceof Throwable; + + return $clone; + } + + public function node(): Node + { + return $this->buildNode($this); + } + + public function checkUnexpectedKeys(): self + { + $value = $this->shell->value(); + + if ($this->shell->allowSuperfluousKeys() || ! is_array($value)) { + return $this; + } + + $diff = array_diff(array_keys($value), array_keys($this->children), $this->shell->allowedSuperfluousKeys()); + + if ($diff !== []) { + return $this->withMessage(new UnexpectedKeysInSource($value, $this->children)); + } + + return $this; + } + + private function check(): void + { + foreach ($this->children as $child) { + if (! $child->valid) { + $this->valid = false; + } + } + + $type = $this->shell->type(); + + if ($this->valid && ! $type->accepts($this->value)) { + $this->valid = false; + $this->messages[] = new InvalidNodeValue($type); + } + } + + private function buildNode(self $self): Node + { + return new Node( + $self->shell->isRoot(), + $self->shell->name(), + $self->shell->path(), + $self->shell->type()->toString(), + $self->shell->hasValue(), + $self->shell->hasValue() ? $self->shell->value() : null, + $self->valid ? $self->value : null, + $self->messages, + array_map( + fn (self $child) => $self->buildNode($child), + $self->children + ) + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TypeNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TypeNodeBuilder.php new file mode 100644 index 0000000..bfc58e7 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/TypeNodeBuilder.php @@ -0,0 +1,75 @@ +type()::class) { + // List + ListType::class, + NonEmptyListType::class => $this->listNodeBuilder, + + // Array + ArrayType::class, + NonEmptyArrayType::class, + IterableType::class => $this->arrayNodeBuilder, + + // ShapedArray + ShapedArrayType::class => $this->shapedArrayNodeBuilder, + + // Union + UnionType::class => $this->unionNodeBuilder, + + // Null + NullType::class => $this->nullNodeBuilder, + + // Mixed + MixedType::class => $this->mixedNodeBuilder, + + // Undefined object + UndefinedObjectType::class => $this->undefinedObjectNodeBuilder, + + // Object + NativeClassType::class, + EnumType::class, + InterfaceType::class => $this->objectNodeBuilder, + + // Scalar + default => $this->scalarNodeBuilder, + }; + + return $builder->build($shell, $rootBuilder); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UndefinedObjectNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UndefinedObjectNodeBuilder.php new file mode 100644 index 0000000..e5d1b95 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UndefinedObjectNodeBuilder.php @@ -0,0 +1,26 @@ +type() instanceof UndefinedObjectType); + + if (! $shell->allowPermissiveTypes()) { + throw new CannotMapToPermissiveType($shell); + } + + return TreeNode::leaf($shell, $shell->value()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UnionNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UnionNodeBuilder.php new file mode 100644 index 0000000..229cd13 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/UnionNodeBuilder.php @@ -0,0 +1,108 @@ +type(); + + assert($type instanceof UnionType); + + $structs = []; + $scalars = []; + $all = []; + + foreach ($type->types() as $subType) { + // @infection-ignore-all / This is a performance optimisation, so we + // cannot easily test this behavior. + if ($subType instanceof NullType && $shell->value() === null) { + return TreeNode::leaf($shell, null); + } + + try { + $node = $rootBuilder->build($shell->withType($subType)); + } catch (CannotResolveObjectType) { + // We catch a special case where an interface type from the + // union has no implementation. In this case, we just ignore the + // exception and let the other types handle the value. + continue; + } + + if (! $node->isValid()) { + continue; + } + + $all[] = $node; + + if ($subType instanceof InterfaceType || $subType instanceof ClassType || $subType instanceof ShapedArrayType) { + $structs[] = $node; + } elseif ($subType instanceof ScalarType) { + $scalars[] = $node; + } + } + + if ($all === []) { + return TreeNode::error($shell, new CannotResolveTypeFromUnion($shell->value(), $type)); + } + + if (count($all) === 1) { + return $all[0]; + } + + // If there is only one scalar and one struct, the scalar has priority. + if (count($scalars) === 1 && count($structs) === 1) { + return $scalars[0]; + } + + if ($structs !== []) { + // Structs can be either an interface, a class or a shaped array. + // We prioritize the one with the most children, as it's the most + // specific type. If there are multiple types with the same number + // of children, we consider it as a collision. + $childrenCount = []; + + foreach ($structs as $node) { + $childrenCount[count($node->children())][] = $node; + } + + krsort($childrenCount); + + $first = reset($childrenCount); + + if (count($first) === 1) { + return $first[0]; + } + } elseif ($scalars !== []) { + usort( + $scalars, + fn (TreeNode $a, TreeNode $b): int => TypeHelper::typePriority($b->type()) <=> TypeHelper::typePriority($a->type()), + ); + + return $scalars[0]; + } + + return TreeNode::error($shell, new TooManyResolvedTypesFromUnion($type)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php new file mode 100644 index 0000000..9391bbd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Builder/ValueAlteringNodeBuilder.php @@ -0,0 +1,47 @@ +delegate->build($shell, $rootBuilder); + + if (! $node->isValid()) { + return $node; + } + + $value = $node->value(); + + foreach ($this->functions as $function) { + $parameters = $function->definition->parameters; + + if (count($parameters) === 0) { + continue; + } + + $firstParameterType = $parameters->at(0)->type; + + if (! $firstParameterType->accepts($value)) { + continue; + } + + $value = ($function->callback)($value); + $node = $node->withValue($value); + } + + return $node; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotInferFinalClass.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotInferFinalClass.php new file mode 100644 index 0000000..c9f6ba5 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotInferFinalClass.php @@ -0,0 +1,21 @@ +className()}` with function `$function->signature`.", + 1671468163 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotMapToPermissiveType.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotMapToPermissiveType.php new file mode 100644 index 0000000..1f90c12 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotMapToPermissiveType.php @@ -0,0 +1,23 @@ +type()->toString(); + + parent::__construct( + "Type `$type` at path `{$shell->path()}` is not allowed in strict mode. " . + "In case `$type` is really needed, the `allowPermissiveTypes` setting can be used.", + 1736935538, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotResolveObjectType.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotResolveObjectType.php new file mode 100644 index 0000000..ad2d83c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CannotResolveObjectType.php @@ -0,0 +1,19 @@ + */ + private array $parameters; + + public function __construct(mixed $source, UnionType $unionType) + { + $this->parameters = [ + 'allowed_types' => implode( + ', ', + array_map(TypeHelper::dump(...), $unionType->types()) + ), + ]; + + if ($source === null) { + $this->body = TypeHelper::containsObject($unionType) + ? 'Cannot be empty.' + : 'Cannot be empty and must be filled with a value matching any of {allowed_types}.'; + } else { + $this->body = TypeHelper::containsObject($unionType) + ? 'Invalid value {source_value}.' + : 'Value {source_value} does not match any of {allowed_types}.'; + } + + parent::__construct(StringFormatter::for($this), 1607027306); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CircularDependencyDetected.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CircularDependencyDetected.php new file mode 100644 index 0000000..37d6d16 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/CircularDependencyDetected.php @@ -0,0 +1,20 @@ +signature()}`.", + 1739903374, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php new file mode 100644 index 0000000..3bdde3b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InterfaceHasBothConstructorAndInfer.php @@ -0,0 +1,22 @@ + */ + private array $parameters; + + public function __construct(int|string $key, int $expected) + { + $this->parameters = [ + 'key' => ValueDumper::dump($key), + 'expected' => (string)$expected, + ]; + + parent::__construct(StringFormatter::for($this), 1654273010); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeHasNoMappedValue.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeHasNoMappedValue.php new file mode 100644 index 0000000..c6ca963 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeHasNoMappedValue.php @@ -0,0 +1,19 @@ +isValid()`.", + 1657466305 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeValue.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeValue.php new file mode 100644 index 0000000..61ea110 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidNodeValue.php @@ -0,0 +1,44 @@ + */ + private array $parameters; + + public function __construct(Type $type) + { + $this->parameters = [ + 'expected_type' => TypeHelper::dump($type), + ]; + + $this->body = TypeHelper::containsObject($type) + ? 'Invalid value {source_value}.' + : 'Value {source_value} does not match type {expected_type}.'; + + parent::__construct(StringFormatter::for($this), 1630678334); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidResolvedImplementationValue.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidResolvedImplementationValue.php new file mode 100644 index 0000000..128f312 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/InvalidResolvedImplementationValue.php @@ -0,0 +1,22 @@ + */ + private array $parameters; + + public function __construct(string|int $key, ArrayKeyType $type) + { + $this->parameters = [ + 'key' => ValueDumper::dump($key), + 'expected_type' => TypeHelper::dump($type), + ]; + + parent::__construct(StringFormatter::for($this), 1630946163); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingNodeValue.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingNodeValue.php new file mode 100644 index 0000000..58ecde4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingNodeValue.php @@ -0,0 +1,40 @@ + */ + private array $parameters; + + public function __construct(Type $type) + { + $this->parameters = [ + 'expected_type' => TypeHelper::dump($type), + ]; + + parent::__construct(StringFormatter::for($this), 1655449641); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingObjectImplementationRegistration.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingObjectImplementationRegistration.php new file mode 100644 index 0000000..61f1e2d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/MissingObjectImplementationRegistration.php @@ -0,0 +1,20 @@ +returnType->toString()}` of `$functionDefinition->signature`.", + 1653990549 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationCallbackError.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationCallbackError.php new file mode 100644 index 0000000..7fb7b92 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationCallbackError.php @@ -0,0 +1,26 @@ +getMessage(), + 1653983061, + $original + ); + } + + public function original(): Exception + { + return $this->original; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationNotRegistered.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationNotRegistered.php new file mode 100644 index 0000000..15e309d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ObjectImplementationNotRegistered.php @@ -0,0 +1,28 @@ + $allowed + */ + public function __construct(string $implementation, string $name, array $allowed) + { + $allowed = implode('`, `', array_map(fn (ClassType $type) => $type->toString(), $allowed)); + + parent::__construct( + "Invalid implementation `$implementation` for `$name`, it should be one of `$allowed`.", + 1653990989 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ResolvedImplementationIsNotAccepted.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ResolvedImplementationIsNotAccepted.php new file mode 100644 index 0000000..43cf6dd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/ResolvedImplementationIsNotAccepted.php @@ -0,0 +1,20 @@ +toString()}`, expected a subtype of `$name`.", + 1618049487 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceIsNotNull.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceIsNotNull.php new file mode 100644 index 0000000..895be04 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceIsNotNull.php @@ -0,0 +1,26 @@ +body = 'Value {source_value} is not null.'; + + parent::__construct($this->body, 1710263908); + } + + public function body(): string + { + return $this->body; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceMustBeIterable.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceMustBeIterable.php new file mode 100644 index 0000000..d3f78e8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceMustBeIterable.php @@ -0,0 +1,50 @@ + */ + private array $parameters; + + public function __construct(mixed $value, Type $type) + { + $this->parameters = [ + 'expected_type' => TypeHelper::dump($type), + ]; + + if ($value === null) { + $this->body = TypeHelper::containsObject($type) + ? 'Cannot be empty.' + : 'Cannot be empty and must be filled with a value matching type {expected_type}.'; + } else { + $this->body = TypeHelper::containsObject($type) + ? 'Invalid value {source_value}.' + : 'Value {source_value} does not match type {expected_type}.'; + } + + parent::__construct(StringFormatter::for($this), 1618739163); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceValueWasNotFilled.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceValueWasNotFilled.php new file mode 100644 index 0000000..b9fd2e3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/SourceValueWasNotFilled.php @@ -0,0 +1,19 @@ +sourceFilled()`.", + 1657466107 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/TooManyResolvedTypesFromUnion.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/TooManyResolvedTypesFromUnion.php new file mode 100644 index 0000000..a965361 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/TooManyResolvedTypesFromUnion.php @@ -0,0 +1,50 @@ + */ + private array $parameters; + + public function __construct(UnionType $unionType) + { + $this->parameters = [ + 'allowed_types' => implode( + ', ', + array_map(TypeHelper::dump(...), $unionType->types()) + ), + ]; + + $this->body = TypeHelper::containsObject($unionType) + ? 'Invalid value {source_value}, it matches two or more types from union: cannot take a decision.' + : 'Invalid value {source_value}, it matches two or more types from {allowed_types}: cannot take a decision.'; + + parent::__construct(StringFormatter::for($this), 1710262975); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnexpectedKeysInSource.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnexpectedKeysInSource.php new file mode 100644 index 0000000..b9ec018 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnexpectedKeysInSource.php @@ -0,0 +1,56 @@ + */ + private array $parameters; + + /** + * @param array $value + * @param array $children + */ + public function __construct(array $value, array $children) + { + $expected = array_map(fn (TreeNode $child) => $child->name(), $children); + $superfluous = array_filter( + array_keys($value), + fn (string $key) => ! in_array($key, $expected, true) + ); + + $this->parameters = [ + 'keys' => '`' . implode('`, `', $superfluous) . '`', + 'expected_keys' => '`' . implode('`, `', $expected) . '`', + ]; + + parent::__construct(StringFormatter::for($this), 1655117782); + } + + public function body(): string + { + return $this->body; + } + + public function parameters(): array + { + return $this->parameters; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnresolvableShellType.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnresolvableShellType.php new file mode 100644 index 0000000..1bbcd1c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Exception/UnresolvableShellType.php @@ -0,0 +1,17 @@ +message()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/DefaultMessage.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/DefaultMessage.php new file mode 100644 index 0000000..bf619c1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/DefaultMessage.php @@ -0,0 +1,105 @@ + [ + 'en' => 'Value {source_value} does not match any of {allowed_values}.', + ], + 'Value {source_value} does not match any of {allowed_types}.' => [ + 'en' => 'Value {source_value} does not match any of {allowed_types}.', + ], + 'Cannot be empty and must be filled with a value matching any of {allowed_types}.' => [ + 'en' => 'Cannot be empty and must be filled with a value matching any of {allowed_types}.', + ], + 'Value {source_value} does not match type {expected_type}.' => [ + 'en' => 'Value {source_value} does not match type {expected_type}.', + ], + 'Value {source_value} does not match {expected_value}.' => [ + 'en' => 'Value {source_value} does not match {expected_value}.', + ], + 'Value {source_value} does not match boolean value {expected_value}.' => [ + 'en' => 'Value {source_value} does not match boolean value {expected_value}.', + ], + 'Value {source_value} does not match float value {expected_value}.' => [ + 'en' => 'Value {source_value} does not match float value {expected_value}.', + ], + 'Value {source_value} does not match integer value {expected_value}.' => [ + 'en' => 'Value {source_value} does not match integer value {expected_value}.', + ], + 'Value {source_value} does not match string value {expected_value}.' => [ + 'en' => 'Value {source_value} does not match string value {expected_value}.', + ], + 'Value {source_value} is not null.' => [ + 'en' => 'Value {source_value} is not null.', + ], + 'Value {source_value} is not a valid boolean.' => [ + 'en' => 'Value {source_value} is not a valid boolean.', + ], + 'Value {source_value} is not a valid float.' => [ + 'en' => 'Value {source_value} is not a valid float.', + ], + 'Value {source_value} is not a valid integer.' => [ + 'en' => 'Value {source_value} is not a valid integer.', + ], + 'Value {source_value} is not a valid string.' => [ + 'en' => 'Value {source_value} is not a valid string.', + ], + 'Value {source_value} is not a valid negative integer.' => [ + 'en' => 'Value {source_value} is not a valid negative integer.', + ], + 'Value {source_value} is not a valid positive integer.' => [ + 'en' => 'Value {source_value} is not a valid positive integer.', + ], + 'Value {source_value} is not a valid non-empty string.' => [ + 'en' => 'Value {source_value} is not a valid non-empty string.', + ], + 'Value {source_value} is not a valid numeric string.' => [ + 'en' => 'Value {source_value} is not a valid numeric string.', + ], + 'Value {source_value} is not a valid integer between {min} and {max}.' => [ + 'en' => 'Value {source_value} is not a valid integer between {min} and {max}.', + ], + 'Value {source_value} is not a valid timezone.' => [ + 'en' => 'Value {source_value} is not a valid timezone.', + ], + 'Value {source_value} is not a valid class string.' => [ + 'en' => 'Value {source_value} is not a valid class string.', + ], + 'Value {source_value} is not a valid class string of `{expected_class_type}`.' => [ + 'en' => 'Value {source_value} is not a valid class string of `{expected_class_type}`.', + ], + 'Invalid value {source_value}.' => [ + 'en' => 'Invalid value {source_value}.', + ], + 'Invalid value {source_value}, it matches two or more types from union: cannot take a decision.' => [ + 'en' => 'Invalid value {source_value}, it matches two or more types from union: cannot take a decision.', + ], + 'Invalid value {source_value}, it matches two or more types from {allowed_types}: cannot take a decision.' => [ + 'en' => 'Invalid value {source_value}, it matches two or more types from {allowed_types}: cannot take a decision.', + ], + 'Invalid sequential key {key}, expected {expected}.' => [ + 'en' => 'Invalid sequential key {key}, expected {expected}.', + ], + 'Cannot be empty.' => [ + 'en' => 'Cannot be empty.', + ], + 'Cannot be empty and must be filled with a value matching type {expected_type}.' => [ + 'en' => 'Cannot be empty and must be filled with a value matching type {expected_type}.', + ], + 'Key {key} does not match type {expected_type}.' => [ + 'en' => 'Key {key} does not match type {expected_type}.', + ], + 'Value {source_value} does not match a valid date format.' => [ + 'en' => 'Value {source_value} does not match a valid date format.', + ], + 'Value {source_value} does not match any of the following formats: {formats}.' => [ + 'en' => 'Value {source_value} does not match any of the following formats: {formats}.', + ], + ]; +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/ErrorMessage.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/ErrorMessage.php new file mode 100644 index 0000000..123880d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/ErrorMessage.php @@ -0,0 +1,10 @@ +formatters = $formatters; + } + + public function format(NodeMessage $message): NodeMessage + { + foreach ($this->formatters as $formatter) { + $message = $formatter->format($message); + } + + return $message; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/CallbackMessageFormatter.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/CallbackMessageFormatter.php new file mode 100644 index 0000000..e5afb27 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/CallbackMessageFormatter.php @@ -0,0 +1,47 @@ + match ($message->code()) { + * 'some_code_a', + * 'some_code_b', + * 'some_code_c' => $message->withBody('some new message body'), + * default => $message + * } + * ); + * + * $message = $formatter->format($message); + * ``` + * + * @api + */ +final class CallbackMessageFormatter implements MessageFormatter +{ + /** @var callable(NodeMessage): NodeMessage */ + private $callback; + + /** + * @param callable(NodeMessage): NodeMessage $callback + */ + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + public function format(NodeMessage $message): NodeMessage + { + return ($this->callback)($message); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/LocaleMessageFormatter.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/LocaleMessageFormatter.php new file mode 100644 index 0000000..2932a7d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/LocaleMessageFormatter.php @@ -0,0 +1,18 @@ +withLocale($this->locale); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/MessageFormatter.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/MessageFormatter.php new file mode 100644 index 0000000..e93c8ee --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/MessageFormatter.php @@ -0,0 +1,13 @@ + 'New content / code: {message_code}', + * + * // Will match if the given message has this exact content + * 'Some message content' => 'New content / previous: {original_message}', + * + * // Will match if the given message is an instance of `SomeError` + * SomeError::class => 'New content / value: {source_value}', + * + * // A callback can be used to get access to the message instance + * OtherError::class => function (NodeMessage $message): string { + * if ($message->path() === 'foo.bar') { + * return 'Some custom message'; + * } + * + * return $message->body(); + * }, + * + * // For greedy operation, it is advised to use a lazy-callback + * 'foo' => fn () => $this->translator->translate('foo.bar'), + * ])) + * ->defaultsTo('some default message') + * // …or… + * ->defaultsTo(fn () => $this->translator->translate('default_message')); + * + * $message = $formatter->format($message); + * ``` + * + * @api + */ +final class MessageMapFormatter implements MessageFormatter +{ + /** @var null|string|callable(NodeMessage): string */ + private $default; + + public function __construct( + /** @var array */ + private array $map + ) {} + + public function format(NodeMessage $message): NodeMessage + { + $target = $this->target($message); + + if ($target) { + return $message->withBody(is_string($target) ? $target : $target($message)); + } + + return $message; + } + + /** + * @param string|callable(NodeMessage): string $default + */ + public function defaultsTo(string|callable $default): self + { + $clone = clone $this; + $clone->default = $default; + + return $clone; + } + + /** + * @return false|string|callable(NodeMessage): string + */ + private function target(NodeMessage $message): false|string|callable + { + return $this->map[$message->code()] + ?? $this->map[$message->body()] + ?? $this->map[$message->originalMessage()::class] + ?? $this->default + ?? false; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/TranslationMessageFormatter.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/TranslationMessageFormatter.php new file mode 100644 index 0000000..34a7020 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Formatter/TranslationMessageFormatter.php @@ -0,0 +1,88 @@ +> */ + private array $translations = []; + + /** + * Returns an instance of the class with the default translations provided + * by the library. + */ + public static function default(): self + { + $instance = new self(); + $instance->translations = DefaultMessage::TRANSLATIONS; + + return $instance; + } + + /** + * Creates or overrides a single translation. + * + * ```php + * (TranslationMessageFormatter::default())->withTranslation( + * 'fr', + * 'Invalid value {source_value}.', + * 'Valeur invalide {source_value}.', + * ); + * ``` + */ + public function withTranslation(string $locale, string $original, string $translation): self + { + $clone = clone $this; + $clone->translations[$original][$locale] = $translation; + + return $clone; + } + + /** + * Creates or overrides a list of translations. + * + * The given array consists of messages to be translated and for each one a + * list of locales with their associated translations. + * + * ```php + * $formatter = (TranslationMessageFormatter::default())->withTranslations([ + * 'Invalid value {source_value}.' => [ + * 'fr' => 'Valeur invalide {source_value}.', + * 'es' => 'Valor inválido {source_value}.', + * ], + * 'Some custom message' => [ + * // … + * ], + * ]); + * + * $message = $formatter->format($message); + * ``` + * + * @param array> $translations + */ + public function withTranslations(array $translations): self + { + $clone = clone $this; + // @phpstan-ignore assign.propertyType (PHPStan does not properly infer the return type of the function) + $clone->translations = array_replace_recursive($this->translations, $translations); + + return $clone; + } + + public function format(NodeMessage $message): NodeMessage + { + $body = $this->translations[$message->body()][$message->locale()] ?? null; + + if ($body) { + return $message->withBody($body); + } + + return $message; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/HasCode.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/HasCode.php new file mode 100644 index 0000000..d37aa38 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/HasCode.php @@ -0,0 +1,16 @@ +someParameter = $someParameter; + * } + * + * public function body(): string + * { + * return 'Some message with {some_parameter}'; + * } + * + * public function parameters(): array + * { + * return [ + * 'some_parameter' => $this->someParameter, + * ]; + * } + * } + * ``` + * + * @api + */ +interface HasParameters extends Message +{ + /** + * @return array + */ + public function parameters(): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Message.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Message.php new file mode 100644 index 0000000..ddacba2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Message.php @@ -0,0 +1,11 @@ +withCode('some_code') + * ->withParameter('some_parameter', 'some_value') + * ->build(); + * ``` + * + * @api + * + * @template MessageType of Message + */ +final class MessageBuilder +{ + private bool $isError = false; + + private string $code = 'unknown'; + + /** @var array */ + private array $parameters = []; + + private function __construct(private string $body) {} + + /** + * @return self + */ + public static function new(string $body): self + { + return new self($body); + } + + /** + * @return self + */ + public static function newError(string $body): self + { + /** @var self $instance */ + $instance = new self($body); + $instance->isError = true; + + return $instance; + } + + public static function from(Throwable $error): ErrorMessage + { + if ($error instanceof ErrorMessage) { + return $error; + } + + return self::newError($error->getMessage()) + ->withCode((string)$error->getCode()) + ->build(); + } + + /** + * @return self + */ + public function withBody(string $body): self + { + $clone = clone $this; + $clone->body = $body; + + return $clone; + } + + public function body(): string + { + return $this->body; + } + + /** + * @return self + */ + public function withCode(string $code): self + { + $clone = clone $this; + $clone->code = $code; + + return $clone; + } + + public function code(): string + { + return $this->code; + } + + /** + * @return self + */ + public function withParameter(string $name, string $value): self + { + $clone = clone $this; + $clone->parameters[$name] = $value; + + return $clone; + } + + /** + * @return array + */ + public function parameters(): array + { + return $this->parameters; + } + + /** + * @return MessageType&HasCode&HasParameters + */ + public function build(): Message&HasCode&HasParameters + { + /** @var MessageType&HasCode&HasParameters */ + return $this->isError + ? $this->buildErrorMessage() + : $this->buildMessage(); + } + + private function buildMessage(): Message&HasCode&HasParameters + { + return new class ($this->body, $this->code, $this->parameters) implements Message, HasCode, HasParameters { + /** + * @param array $parameters + */ + public function __construct( + private string $body, + private string $code, + private array $parameters + ) {} + + public function body(): string + { + return $this->body; + } + + public function code(): string + { + return $this->code; + } + + public function parameters(): array + { + return $this->parameters; + } + }; + } + + private function buildErrorMessage(): ErrorMessage&HasCode&HasParameters + { + return new class ($this->body, $this->code, $this->parameters) extends RuntimeException implements ErrorMessage, HasCode, HasParameters { + /** + * @param array $parameters + */ + public function __construct(string $body, string $code, private array $parameters) + { + parent::__construct($body); + + $this->code = $code; + } + + public function body(): string + { + /** @var string */ + return $this->message; + } + + public function code(): string + { + /** @var string */ + return $this->code; + } + + public function parameters(): array + { + return $this->parameters; + } + }; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Messages.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Messages.php new file mode 100644 index 0000000..47783c9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/Messages.php @@ -0,0 +1,134 @@ +mapper() + * ->map(SomeClass::class, [/* … * /]); + * } catch (\OCA\Talk\Vendor\CuyZ\Valinor\Mapper\MappingError $error) { + * // Get a flatten list of all messages through the whole nodes tree + * $messages = \OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Tree\Message\Messages::flattenFromNode( + * $error->node() + * ); + * + * // Formatters can be added and will be applied on all messages + * $messages = $messages->formatWith( + * new \OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([ + * // … + * ]), + * (new \OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter()) + * ->withTranslations([ + * // … + * ]) + * ); + * + * // If only errors are wanted, they can be filtered + * $errors = $messages->errors(); + * + * foreach ($errors as $errorMessage) { + * // … + * } + * } + * ``` + * + * @api + * + * @implements IteratorAggregate + */ +final class Messages implements IteratorAggregate, Countable +{ + /** @var list */ + private array $messages; + + /** @var array */ + private array $formatters = []; + + /** + * @no-named-arguments + */ + public function __construct(NodeMessage ...$messages) + { + $this->messages = $messages; + } + + public static function flattenFromNode(Node $node): self + { + $nodeMessages = (new NodeTraverser( + fn (Node $node) => $node->messages() + ))->traverse($node); + + $messages = []; + + foreach ($nodeMessages as $messagesGroup) { + $messages = [...$messages, ...$messagesGroup]; + } + + return new self(...$messages); + } + + public function errors(): self + { + $clone = clone $this; + $clone->messages = array_values( + array_filter($clone->messages, fn (NodeMessage $message) => $message->isError()) + ); + + return $clone; + } + + public function formatWith(MessageFormatter ...$formatters): self + { + $clone = clone $this; + $clone->formatters = [...$clone->formatters, ...$formatters]; + + return $clone; + } + + /** + * @return list + */ + public function toArray(): array + { + return [...$this]; + } + + public function count(): int + { + return count($this->messages); + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + foreach ($this->messages as $message) { + foreach ($this->formatters as $formatter) { + $message = $formatter->format($message); + } + + yield $message; + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/NodeMessage.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/NodeMessage.php new file mode 100644 index 0000000..9cd0e35 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/NodeMessage.php @@ -0,0 +1,161 @@ + */ + private array $parameters = []; + + private string $locale = StringFormatter::DEFAULT_LOCALE; + + public function __construct(Node $node, Message $message) + { + $this->node = $node; + $this->message = $message; + $this->body = $message->body(); + } + + public function node(): Node + { + return $this->node; + } + + public function withLocale(string $locale): self + { + $clone = clone $this; + $clone->locale = $locale; + + return $clone; + } + + public function locale(): string + { + return $this->locale; + } + + /** + * Allows to customize the body of the message. It can contain placeholders + * that will be replaced by their corresponding values, as described below: + * + * | Placeholder | Description | + * |----------------------|------------------------------------------------| + * | `{message_code}` | The code of the message | + * | `{node_name}` | Name of the node to which the message is bound | + * | `{node_path}` | Path of the node to which the message is bound | + * | `{node_type}` | Type of the node to which the message is bound | + * | `{source_value}` | The source value that was given to the node | + * | `{original_message}` | The original message before being customized | + * + * Example: + * + * ```php + * $message = $message->withBody('new message for value: {source_value}'); + * ``` + */ + public function withBody(string $body): self + { + $clone = clone $this; + $clone->body = $body; + + return $clone; + } + + public function body(): string + { + return $this->body; + } + + /** + * Adds a parameter that can replace a placeholder in the message body. + * + * @see self::withBody() + */ + public function withParameter(string $name, string $value): self + { + $clone = clone $this; + $clone->parameters[$name] = $value; + + return $clone; + } + + public function originalMessage(): Message + { + return $this->message; + } + + public function isError(): bool + { + return $this->message instanceof Throwable; + } + + public function code(): string + { + if ($this->message instanceof HasCode) { + return $this->message->code(); + } + + if ($this->message instanceof Throwable) { + return (string)$this->message->getCode(); + } + + return 'unknown'; + } + + public function toString(): string + { + return $this->format($this->body, $this->parameters()); + } + + public function __toString(): string + { + return $this->toString(); + } + + /** + * @param array $parameters + */ + private function format(string $body, array $parameters): string + { + return StringFormatter::format($this->locale, $body, $parameters); + } + + /** + * @return array + */ + private function parameters(): array + { + $parameters = [ + 'message_code' => $this->code(), + 'node_name' => $this->node->name(), + 'node_path' => $this->node->path(), + 'node_type' => "`{$this->node->type()}`", + 'source_value' => $this->node->sourceFilled() ? ValueDumper::dump($this->node->sourceValue()) : '*missing*', + ]; + + if ($this->message instanceof HasParameters) { + $parameters += $this->message->parameters(); + } + + $parameters['original_message'] = $this->format($this->message->body(), $parameters); + + return array_merge($parameters, $this->parameters); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/UserlandError.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/UserlandError.php new file mode 100644 index 0000000..83b7b88 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Message/UserlandError.php @@ -0,0 +1,30 @@ +getPrevious(); // @phpstan-ignore-line + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Node.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Node.php new file mode 100644 index 0000000..bc4b8c4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Node.php @@ -0,0 +1,136 @@ + */ + private array $messages = []; + + /** @var array */ + private array $children; + + /** + * @param array $messages + * @param array $children + */ + public function __construct( + bool $isRoot, + string $name, + string $path, + string $type, + bool $sourceFilled, + mixed $sourceValue, + mixed $mappedValue, + array $messages, + array $children + ) { + $this->isRoot = $isRoot; + $this->name = $name; + $this->path = $path; + $this->type = $type; + $this->sourceFilled = $sourceFilled; + $this->sourceValue = $sourceValue; + $this->mappedValue = $mappedValue; + $this->children = $children; + + foreach ($messages as $message) { + $message = new NodeMessage($this, $message); + + $this->messages[] = $message; + $this->isValid = $this->isValid && ! $message->isError(); + } + + foreach ($this->children as $child) { + $this->isValid = $this->isValid && $child->isValid(); + } + } + + public function isRoot(): bool + { + return $this->isRoot; + } + + public function name(): string + { + return $this->name; + } + + public function path(): string + { + return $this->path; + } + + public function type(): string + { + return $this->type; + } + + public function sourceFilled(): bool + { + return $this->sourceFilled; + } + + public function sourceValue(): mixed + { + if (! $this->sourceFilled) { + throw new SourceValueWasNotFilled($this->path); + } + + return $this->sourceValue; + } + + public function isValid(): bool + { + return $this->isValid; + } + + public function mappedValue(): mixed + { + if (! $this->isValid) { + throw new InvalidNodeHasNoMappedValue($this->path); + } + + return $this->mappedValue; + } + + /** + * @return list + */ + public function messages(): array + { + return $this->messages; + } + + /** + * @return array + */ + public function children(): array + { + return $this->children; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/NodeTraverser.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/NodeTraverser.php new file mode 100644 index 0000000..d8cf9c8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/NodeTraverser.php @@ -0,0 +1,44 @@ +callback = $callback; + } + + /** + * @return iterable + */ + public function traverse(Node $node): iterable + { + return $this->recurse($node); + } + + /** + * @return iterable + */ + private function recurse(Node $node): iterable + { + yield ($this->callback)($node); + + foreach ($node->children() as $child) { + yield from $this->recurse($child); + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Shell.php b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Shell.php new file mode 100644 index 0000000..3ad1239 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/Tree/Shell.php @@ -0,0 +1,201 @@ + */ + private array $path; + + /** @var list */ + private array $allowedSuperfluousKeys = []; + + /** + * @param list $path + */ + private function __construct(Settings $settings, Type $type, array $path = []) + { + if ($type instanceof UnresolvableType) { + throw new UnresolvableShellType($type); + } + + $this->settings = $settings; + $this->type = $type; + $this->path = $path; + } + + public static function root( + Settings $settings, + Type $type, + mixed $value, + ): self { + return (new self($settings, $type))->withValue($value); + } + + public function child(string $name, Type $type, ?Attributes $attributes = null): self + { + $path = $this->path; + $path[] = $name; + $instance = new self($this->settings, $type, $path); + $instance->name = $name; + + if ($attributes) { + $instance->attributes = $attributes; + } + + return $instance; + } + + public function name(): string + { + return $this->name ?? ''; + } + + public function isRoot(): bool + { + return ! isset($this->name); + } + + public function withType(Type $newType): self + { + $clone = clone $this; + $clone->type = $newType; + $clone->value = self::castCompatibleValue($newType, $this->value); + + return $clone; + } + + public function type(): Type + { + return $this->type; + } + + public function withValue(mixed $value): self + { + $clone = clone $this; + $clone->hasValue = true; + $clone->value = self::castCompatibleValue($clone->type, $value); + + return $clone; + } + + public function hasValue(): bool + { + return $this->hasValue; + } + + public function value(): mixed + { + assert($this->hasValue); + + return $this->value; + } + + public function allowScalarValueCasting(): bool + { + return $this->settings->allowScalarValueCasting; + } + public function allowNonSequentialList(): bool + { + return $this->settings->allowNonSequentialList; + } + public function allowUndefinedValues(): bool + { + return $this->settings->allowUndefinedValues; + } + + public function allowSuperfluousKeys(): bool + { + return $this->settings->allowSuperfluousKeys; + } + + public function allowPermissiveTypes(): bool + { + return $this->settings->allowPermissiveTypes; + } + + public function attributes(): Attributes + { + return $this->attributes ?? Attributes::empty(); + } + + /** + * @param list $allowedSuperfluousKeys + */ + public function withAllowedSuperfluousKeys(array $allowedSuperfluousKeys): self + { + $clone = clone $this; + $clone->allowedSuperfluousKeys = $allowedSuperfluousKeys; + + return $clone; + } + + /** + * @return list + */ + public function allowedSuperfluousKeys(): array + { + return $this->allowedSuperfluousKeys; + } + + public function transformIteratorToArray(): self + { + if (is_iterable($this->value) && ! is_array($this->value)) { + $self = clone $this; + $self->value = iterator_to_array($this->value); + + return $self; + } + + return $this; + } + + public function path(): string + { + if ($this->isRoot()) { + return '*root*'; + } + + return implode('.', $this->path); + } + + private static function castCompatibleValue(Type $type, mixed $value): mixed + { + // When the value is an integer and the type is a float, the value is + // cast to float, to follow the rule of PHP regarding acceptance of an + // integer value in a float type. Note that PHPStan/Psalm analysis + // applies the same rule. + if ($type instanceof FloatType && is_int($value)) { + return (float)$value; + } + + return $value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/TreeMapper.php b/lib/Vendor/CuyZ/Valinor/Mapper/TreeMapper.php new file mode 100644 index 0000000..4721400 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/TreeMapper.php @@ -0,0 +1,24 @@ + $signature + * @return T + * @phpstan-return ( + * $signature is class-string + * ? T + * : ($signature is class-string ? object : mixed) + * ) + * + * @throws MappingError + */ + public function map(string $signature, mixed $source): mixed; +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/TypeArgumentsMapper.php b/lib/Vendor/CuyZ/Valinor/Mapper/TypeArgumentsMapper.php new file mode 100644 index 0000000..4f81d4f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/TypeArgumentsMapper.php @@ -0,0 +1,71 @@ +functionDefinitionRepository->for($callable); + + $elements = array_map( + fn (ParameterDefinition $parameter) => new ShapedArrayElement( + new StringValueType($parameter->name), + $parameter->type, + $parameter->isOptional, + ), + $function->parameters->toList(), + ); + + $type = new ShapedArrayType(...$elements); + + $shell = Shell::root($this->settings, $type, $source); + + try { + $node = $this->nodeBuilder->build($shell); + } catch (UnresolvableShellType $exception) { + throw new TypeErrorDuringArgumentsMapping($function, $exception); + } + + if ($node->isValid()) { + /** @var array */ + return $node->value(); + } + + // Transforms the source value if there is only one object argument, to + // ensure the source can contain flattened values. + if (count($elements) === 1 && $elements[0]->type() instanceof ObjectType) { + $shell = $shell->withType($elements[0]->type()); + + $node = $this->nodeBuilder->build($shell); + + if ($node->isValid()) { + /** @var array */ + return [$elements[0]->key()->value() => $node->value()]; + } + } + + throw new ArgumentsMapperError($function, $node->node()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapper.php b/lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapper.php new file mode 100644 index 0000000..5ae74d3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapper.php @@ -0,0 +1,53 @@ +node($signature, $source); + + if (! $node->isValid()) { + throw new TypeTreeMapperError($node->node()); + } + + return $node->value(); + } + + private function node(string $signature, mixed $source): TreeNode + { + try { + $type = $this->typeParser->parse($signature); + } catch (InvalidType $exception) { + throw new InvalidMappingTypeSignature($signature, $exception); + } + + $shell = Shell::root($this->settings, $type, $source); + + try { + return $this->nodeBuilder->build($shell); + } catch (UnresolvableShellType $exception) { + throw new TypeErrorDuringMapping($type, $exception); + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapperError.php b/lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapperError.php new file mode 100644 index 0000000..5e31d39 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Mapper/TypeTreeMapperError.php @@ -0,0 +1,38 @@ +errors(); + $errorsCount = count($errors); + + if ($errorsCount === 1) { + $body = $errors + ->toArray()[0] + ->withParameter('root_type', $node->type()) + ->withBody("Could not map type `{root_type}`. An error occurred at path {node_path}: {original_message}") + ->toString(); + } else { + $source = ValueDumper::dump($node->sourceValue()); + $body = "Could not map type `{$node->type()}` with value $source. A total of $errorsCount errors were encountered."; + } + + parent::__construct($body, 1617193185); + } + + public function node(): Node + { + return $this->node; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/MapperBuilder.php b/lib/Vendor/CuyZ/Valinor/MapperBuilder.php new file mode 100644 index 0000000..0e99e91 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/MapperBuilder.php @@ -0,0 +1,609 @@ +settings = new Settings(); + } + + /** + * Allows the mapper to infer an implementation for a given interface. + * + * The callback can take any arguments, that will automatically be mapped + * using the given source. These arguments can then be used to decide which + * implementation should be used. + * + * Example: + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->infer(UuidInterface::class, fn () => MyUuid::class) + * ->infer(SomeInterface::class, fn (string $type) => match($type) { + * 'foo' => Foo::class, + * 'bar' => Bar::class, + * default => throw new DomainException("Unhandled type `$type`.") + * }) + * ->mapper() + * ->map(SomeInterface::class, [ + * 'type' => 'foo', + * 'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6', + * ]); + * ``` + * + * @param interface-string|class-string $name + */ + public function infer(string $name, callable $callback): self + { + $clone = clone $this; + $clone->settings->inferredMapping[$name] = $callback; + + return $clone; + } + + /** + * Registers a constructor that can be used by the mapper to create an + * instance of an object. A constructor is a callable that can be either: + * + * 1. A named constructor, also known as a static factory method + * 2. The method of a service — for instance a repository + * 3. A "callable object" — a class that declares an `__invoke` method + * 4. Any other callable — including anonymous functions + * + * In any case, the return type of the callable will be resolved by the + * mapper to know when to use it. Any argument can be provided and will + * automatically be mapped using the given source. These arguments can then + * be used to instantiate the object in the desired way. + * + * Registering any constructor will disable the native constructor — the + * `__construct` method — of the targeted class. If for some reason it still + * needs to be handled as well, the name of the class must be given to this + * method. + * + * ```php + * final class SomeClass + * { + * private string $foo; + * + * private int $bar; + * + * private array $otherClasses = []; + * + * public function __construct(string $foo) + * { + * $this->foo = $foo; + * } + * + * public static function namedConstructor(string $foo, int $bar): self + * { + * $instance = new self($foo); + * $instance->bar = $bar; + * + * return $instance; + * } + * + * public function addOtherClass(OtherClass $otherClass): void + * { + * $this->otherClasses[] = $otherClass; + * } + * } + * + * final class SomeRepository + * { + * public function findById(int $id): SomeClass + * { + * // … + * } + * } + * + * final class SomeCallableObject + * { + * public function __invoke(string $foo, int $bar, int $baz): SomeClass + * { + * // … + * } + * } + * + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->registerConstructor( + * // Named constructor + * SomeClass::namedConstructor(...), + * // …or for PHP < 8.1: + * [SomeClass::class, 'namedConstructor'], + * + * // Method of an object + * (new SomeRepository())->findById(...), + * // …or for PHP < 8.1: + * [new SomeRepository(), 'findById'], + * + * // Callable object + * new SomeCallableObject(), + * + * // Anonymous function + * function(string $string, OtherClass $otherClass): SomeClass { + * $someClass = new SomeClass($string); + * $someClass->addOtherClass($otherClass); + * + * return $someClass; + * }, + * + * // Also allow the native constructor — the `__construct` method + * SomeClass::class, + * ) + * ->mapper() + * ->map(SomeClass::class, [ + * // … + * ]); + * ``` + * + * Enum constructors can be registered the same way: + * + * * ```php + * enum SomeEnum: string + * { + * case CASE_A = 'FOO_VALUE_1'; + * case CASE_B = 'FOO_VALUE_2'; + * case CASE_C = 'BAR_VALUE_1'; + * case CASE_D = 'BAR_VALUE_2'; + * + * /** + * * \@param 'FOO'|'BAR' $type + * * \@param int<1, 2> $number + * * / + * public static function fromMatrix(string $type, int $number): self + * { + * return self::from("{$type}_VALUE_{$number}"); + * } + * } + * + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->registerConstructor( + * // Allow the native constructor to be used + * SomeEnum::class, + * + * // Register a named constructor + * SomeEnum::fromMatrix(...) + * ) + * ->mapper() + * ->map(SomeEnum::class, [ + * 'type' => 'FOO', + * 'number' => 'BAR', + * ]); + * ``` + * + * @param callable|class-string ...$constructors + */ + public function registerConstructor(callable|string ...$constructors): self + { + $clone = clone $this; + + foreach ($constructors as $constructor) { + if (is_callable($constructor)) { + $clone->settings->customConstructors[] = $constructor; + } else { + $clone->settings->nativeConstructors[$constructor] = null; + } + } + + return $clone; + } + + /** + * Describes which date formats will be supported during mapping. + * + * By default, the dates will accept any valid timestamp or RFC 3339-formatted + * value. + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * // Both `Cookie` and `ATOM` formats will be accepted + * ->supportDateFormats(DATE_COOKIE, DATE_ATOM) + * ->mapper() + * ->map(DateTimeInterface::class, 'Monday, 08-Nov-1971 13:37:42 UTC'); + * ``` + * + * @param non-empty-string $format + * @param non-empty-string ...$formats + */ + public function supportDateFormats(string $format, string ...$formats): self + { + $clone = clone $this; + $clone->settings->supportedDateFormats = array_values(array_unique([$format, ...$formats])); + + return $clone; + } + + /** + * Returns the date formats supported during mapping. + * + * By default, any valid timestamp or RFC 3339-formatted value are accepted. + * Custom formats can be set using method `supportDateFormats()`. + * + * @return non-empty-array + */ + public function supportedDateFormats(): array + { + return $this->settings->supportedDateFormats; + } + + /** + * Inject a cache implementation that will be in charge of caching heavy + * data used by the mapper. + * + * An implementation is provided by the library, which writes cache entries + * in the file system; it is strongly recommended to use it when the + * application runs in production environment. + * + * It is also possible to use any PSR-16 compliant implementation, as long + * as it is capable of caching the entries handled by the library. + * + * When the application runs in a development environment, the cache + * implementation should be decorated with `FileWatchingCache`, which will + * watch the files of the application and invalidate cache entries when a + * PHP file is modified by a developer — preventing the library not behaving + * as expected when the signature of a property or a method changes. + * + * ```php + * $cache = new \OCA\Talk\Vendor\CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir'); + * + * if ($isApplicationInDevelopmentEnvironment) { + * $cache = new \OCA\Talk\Vendor\CuyZ\Valinor\Cache\FileWatchingCache($cache); + * } + * + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->withCache($cache) + * ->mapper() + * ->map(SomeClass::class, [ + * // … + * ]); + * ``` + */ + public function withCache(CacheInterface $cache): self + { + $clone = clone $this; + $clone->settings->cache = $cache; + + return $clone; + } + + /** + * @template T + * @param callable(T): T $callback + */ + public function alter(callable $callback): self + { + $clone = clone $this; + $clone->settings->valueModifier[] = $callback; + + return $clone; + } + + /** + * This setting will be removed in a future major version, as a replacement + * the following methods should be used: + * + * - @see allowScalarValueCasting() + * - @see allowNonSequentialList() + * - @see allowUndefinedValues() + */ + public function enableFlexibleCasting(): self + { + return $this + ->allowScalarValueCasting() + ->allowNonSequentialList() + ->allowUndefinedValues(); + } + + /** + * With this setting enabled, scalar types will accept castable values: + * + * - Integer types will accept any valid numeric value, for instance the + * string value "42". + * + * - Float types will accept any valid numeric value, for instance the + * string value "1337.42". + * + * - String types will accept any integer, float or object implementing the + * `Stringable` interface. + * + * - Boolean types will accept any truthy or falsy value: + * - "true" (string), "1" (string) and 1 (int) will be cast to `true` + * - "false" (string), "0" (string) and 0 (int) will be cast to `false` + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->allowScalarValueCasting() + * ->mapper() + * ->map('array{id: string, price: float, active: bool}', [ + * 'id' => 549465210, // Will be cast to string + * 'price' => '42.39', // Will be cast to float + * 'active' => 1, // Will be cast to bool + * ]); + * ``` + */ + public function allowScalarValueCasting(): self + { + $clone = clone $this; + $clone->settings->allowScalarValueCasting = true; + + return $clone; + } + + /** + * By default, list types will only accept sequential keys starting from 0. + * + * This setting allows the mapper to convert associative arrays to a list + * with sequential keys. + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->allowNonSequentialList() + * ->mapper() + * ->map('list', [ + * 'foo' => 42, + * 'bar' => 1337, + * ]); + * + * // => [0 => 42, 1 => 1337] + * ``` + */ + public function allowNonSequentialList(): self + { + $clone = clone $this; + $clone->settings->allowNonSequentialList = true; + + return $clone; + } + + /** + * Allows the mapper to accept undefined values (missing from the input), by + * converting them to `null` (if the current type is nullable) or an empty + * array (if the current type is an object or an iterable). + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->allowUndefinedValues() + * ->mapper() + * ->map('array{name: string, age: int|null}', [ + * 'name' => 'John Doe', + * // 'age' is not defined + * ]); + * + * // => ['name' => 'John Doe', 'age' => null] + * ``` + */ + public function allowUndefinedValues(): self + { + $clone = clone $this; + $clone->settings->allowUndefinedValues = true; + + return $clone; + } + + /** + * By default, an error is raised when a source array contains keys that + * do not match a class property/parameter or a shaped array element. + * + * This setting allows the mapper to ignore these superfluous keys. + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->allowSuperfluousKeys() + * ->mapper() + * ->map('array{name: string, age: int}', [ + * 'name' => 'John Doe', + * 'age' => 42, + * 'city' => 'Paris', // Will be ignored + * ]); + * ``` + */ + public function allowSuperfluousKeys(): self + { + $clone = clone $this; + $clone->settings->allowSuperfluousKeys = true; + + return $clone; + } + + /** + * Allows permissive types `mixed` and `object` to be used during mapping. + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->allowPermissiveTypes() + * ->mapper() + * ->map('array{name: string, data: mixed}', [ + * 'name' => 'some_product', + * 'data' => 42, // Could be any value + * ]); + * ``` + */ + public function allowPermissiveTypes(): self + { + $clone = clone $this; + $clone->settings->allowPermissiveTypes = true; + + return $clone; + } + + /** + * Filters which userland exceptions are allowed during the mapping. + * + * It is advised to use this feature with caution: userland exceptions may + * contain sensible information — for instance an SQL exception showing a + * part of a query should never be allowed. Therefore, only an exhaustive + * list of carefully chosen exceptions should be filtered. + * + * ```php + * final class SomeClass + * { + * public function __construct(string $value) + * { + * \Webmozart\Assert\Assert::startsWith($value, 'foo_'); + * } + * } + * + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->filterExceptions(function (Throwable $exception) { + * if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) { + * return \OCA\Talk\Vendor\CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::from($exception); + * } + * + * // If the exception should not be caught by this library, it must + * // be thrown again. + * throw $exception; + * }) + * ->mapper() + * ->map(SomeClass::class, [ + * // … + * ]); + * ``` + * + * @param callable(Throwable): ErrorMessage $filter + */ + public function filterExceptions(callable $filter): self + { + $clone = clone $this; + $clone->settings->exceptionFilter = $filter; + + return $clone; + } + + /** + * A transformer is responsible for transforming specific values during a + * normalization process. + * + * Transformers can be chained, the last registered one will take precedence + * over the previous ones. + * + * By specifying the type of its first parameter, the given callable will + * determine when the transformer is used. Advanced type annotations like + * `non-empty-string` can be used to target a more specific type. + * + * A second `callable` parameter may be declared, allowing to call the next + * transformer in the chain and get the modified value from it, before + * applying its own transformations. + * + * A priority can be given to a transformer, to make sure it is called + * before or after another one. The higher the priority, the sooner the + * transformer will be called. Default priority is 0. + * + * An attribute on a property or a class can act as a transformer if: + * 1. It defines a `normalize` or `normalizeKey` method. + * 2. It is registered using either the `registerTransformer()` method or + * the following attribute: @see \OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\AsTransformer + * + * Example: + * + * ```php + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * + * // The type of the first parameter of the transformer will determine + * // when it will be used by the normalizer. + * ->registerTransformer( + * fn (string $value, callable $next) => strtoupper($next()) + * ) + * + * // Transformers can be chained, the last registered one will take + * // precedence over the previous ones, which can be called using the + * // `$next` parameter. + * ->registerTransformer( + * fn (string $value, callable $next) => $next() . '!' + * ) + * + * // A priority can be given to a transformer, to make sure it is + * // called before or after another one. + * ->registerTransformer( + * fn (string $value, callable $next) => $next() . '?', + * priority: -100 // Negative priority: transformer is called early + * ) + * + * // External transformer attributes must be registered before they are + * // used by the normalizer. + * ->registerTransformer(\Some\External\TransformerAttribute::class) + * + * ->normalizer() + * ->normalize('Hello world'); // HELLO WORLD?! + * ``` + * + * @param callable|class-string $transformer + */ + public function registerTransformer(callable|string $transformer, int $priority = 0): self + { + $clone = clone $this; + + if (is_callable($transformer)) { + $clone->settings->transformers[$priority][] = $transformer; + } else { + $clone->settings->transformerAttributes[$transformer] = null; + } + + return $clone; + } + + /** + * Warms up the injected cache implementation with the provided class names. + * + * By passing a class which contains recursive objects, every nested object + * will be cached as well. + */ + public function warmup(string ...$signatures): void + { + $this->container()->cacheWarmupService()->warmup(...$signatures); + } + + public function mapper(): TreeMapper + { + return $this->container()->treeMapper(); + } + + public function argumentsMapper(): ArgumentsMapper + { + return $this->container()->argumentsMapper(); + } + + /** + * @template T of Normalizer + * + * @param Format $format + * @return T + */ + public function normalizer(Format $format): Normalizer + { + return $this->container()->normalizer($format); + } + + public function __clone() + { + $this->settings = clone $this->settings; + unset($this->container); + } + + private function container(): Container + { + return ($this->container ??= new Container($this->settings)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/ArrayNormalizer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/ArrayNormalizer.php new file mode 100644 index 0000000..2fb4daf --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/ArrayNormalizer.php @@ -0,0 +1,43 @@ +|scalar|null> + */ +final class ArrayNormalizer implements Normalizer +{ + public function __construct( + private Transformer $transformer, + ) {} + + public function normalize(mixed $value): mixed + { + /** @var array|scalar|null */ + return $this->format( + $this->transformer->transform($value), + ); + } + + private function format(mixed $value): mixed + { + if (is_iterable($value)) { + if (! is_array($value)) { + $value = iterator_to_array($value); + } + + $value = array_map($this->format(...), $value); + } elseif ($value instanceof EmptyObject) { + return []; + } + + return $value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/AsTransformer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/AsTransformer.php new file mode 100644 index 0000000..f7fdc15 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/AsTransformer.php @@ -0,0 +1,55 @@ +format($this->format); + * } + * } + * + * final readonly class Event + * { + * public function __construct( + * public string $eventName, + * #[\My\App\DateTimeFormat('Y/m/d')] + * public \DateTimeInterface $date, + * ) {} + * } + * + * (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->normalizer(\OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\Format::array()) + * ->normalize(new \My\App\Event( + * eventName: 'Release of legendary album', + * date: new \DateTimeImmutable('1971-11-08'), + * )); + * + * // [ + * // 'eventName' => 'Release of legendary album', + * // 'date' => '1971/11/08', + * // ] + * ``` + * + * @api + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class AsTransformer {} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/CircularReferenceFoundDuringNormalization.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/CircularReferenceFoundDuringNormalization.php new file mode 100644 index 0000000..9bcb631 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/CircularReferenceFoundDuringNormalization.php @@ -0,0 +1,21 @@ +parameters->count()} given for `$method->signature`.", + 1701701102, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/KeyTransformerParameterInvalidType.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/KeyTransformerParameterInvalidType.php new file mode 100644 index 0000000..83ae0b2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/KeyTransformerParameterInvalidType.php @@ -0,0 +1,20 @@ +parameters->at(0)->type->toString()} given for `$method->signature`.", + 1701706316, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasInvalidCallableParameter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasInvalidCallableParameter.php new file mode 100644 index 0000000..6cee39c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasInvalidCallableParameter.php @@ -0,0 +1,22 @@ +toString()}` given for `$method->signature`.", + 1695065710, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasNoParameter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasNoParameter.php new file mode 100644 index 0000000..768c925 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasNoParameter.php @@ -0,0 +1,21 @@ +signature`.", + 1695064946, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasTooManyParameters.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasTooManyParameters.php new file mode 100644 index 0000000..437ddcd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TransformerHasTooManyParameters.php @@ -0,0 +1,21 @@ +parameters->count()} given for `$method->signature`.", + 1695065433, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TypeUnhandledByNormalizer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TypeUnhandledByNormalizer.php new file mode 100644 index 0000000..b21ceed --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Exception/TypeUnhandledByNormalizer.php @@ -0,0 +1,23 @@ +normalizer(\OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\Format::array()); + * + * $userAsArray = $normalizer->normalize( + * new \My\App\User( + * name: 'John Doe', + * age: 42, + * country: new \My\App\Country( + * name: 'France', + * countryCode: 'FR', + * ), + * ) + * ); + * + * // `$userAsArray` is now an array and can be manipulated much more easily, for + * // instance to be serialized to the wanted data format. + * // + * // [ + * // 'name' => 'John Doe', + * // 'age' => 42, + * // 'country' => [ + * // 'name' => 'France', + * // 'countryCode' => 'FR', + * // ], + * // ]; + * ``` + * + * @return self + */ + public static function array(): self + { + return new self(ArrayNormalizer::class); + } + + /** + * Allows a normalizer to format an input to JSON syntax. + * + * ```php + * namespace My\App; + * + * $normalizer = (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->normalizer(\OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\Format::json()); + * + * $userAsJson = $normalizer->normalize( + * new \My\App\User( + * name: 'John Doe', + * age: 42, + * country: new \My\App\Country( + * name: 'France', + * code: 'FR', + * ), + * ) + * ); + * + * // `$userAsJson` is a valid JSON string representing the data: + * // {"name":"John Doe","age":42,"country":{"name":"France","code":"FR"}} + * ``` + * + * @return self + */ + public static function json(): self + { + return new self(JsonNormalizer::class); + } + + /** + * @param class-string $type + */ + private function __construct(private string $type) {} + + /** + * @return class-string + */ + public function type(): string + { + return $this->type; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Formatter/Exception/CannotFormatInvalidTypeToJson.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Formatter/Exception/CannotFormatInvalidTypeToJson.php new file mode 100644 index 0000000..d0d8b9d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Formatter/Exception/CannotFormatInvalidTypeToJson.php @@ -0,0 +1,21 @@ +prettyPrint = (bool)($this->jsonEncodingOptions & JSON_PRETTY_PRINT); + $this->forceObject = (bool)($this->jsonEncodingOptions & JSON_FORCE_OBJECT); + } + + /** + * @return resource + */ + public function format(mixed $value): mixed + { + $this->formatRecursively($value, 1); + + return $this->resource; + } + + private function formatRecursively(mixed $value, int $depth): void + { + if (is_null($value)) { + $this->write('null'); + } elseif (is_bool($value)) { + $this->write($value ? 'true' : 'false'); + } elseif (is_scalar($value)) { + /** + * @phpstan-ignore-next-line / Due to the new json encoding options feature, it is not possible to let SA + * tools understand that JSON_THROW_ON_ERROR is always set. + */ + $this->write(json_encode($value, $this->jsonEncodingOptions)); + } elseif ($value instanceof EmptyObject) { + $this->write('{}'); + } elseif (is_iterable($value)) { + // Note: when a generator is formatted, it is considered as a list + // if its first key is 0. This is done early because the first JSON + // character for an array differs from the one for an object, and we + // need to know that before actually looping on the generator. + // + // For generators having a first key of 0 and inconsistent keys + // afterward, this leads to a JSON array being written, while it + // should have been an object. This is a trade-off we accept, + // considering most generators starting at 0 are actually lists. + if ($this->forceObject) { + $isList = false; + } elseif ($value instanceof Generator) { + if (! $value->valid()) { + $this->write('[]'); + return; + } + + $isList = $value->key() === 0; + } else { + /** @var array $value At this point we know this is an array */ + $isList = array_is_list($value); + } + + $isFirst = true; + + $this->write($isList ? '[' : '{'); + + foreach ($value as $key => $val) { + $chunk = ''; + + if (! $isFirst) { + $chunk = ','; + } + + if ($this->prettyPrint) { + $chunk .= PHP_EOL . str_repeat(' ', $depth); + } + + $isFirst = false; + + if (! $isList) { + assert(is_scalar($key)); + + $key = json_encode((string)$key, $this->jsonEncodingOptions); + + $chunk .= $key . ':'; + + if ($this->prettyPrint) { + $chunk .= ' '; + } + } + + $this->write($chunk); + + $this->formatRecursively($val, $depth + 1); + } + + $chunk = ''; + + if ($this->prettyPrint) { + $chunk = PHP_EOL . str_repeat(' ', $depth - 1); + } + + $chunk .= $isList ? ']' : '}'; + + $this->write($chunk); + } else { + throw new CannotFormatInvalidTypeToJson($value); + } + } + + private function write(string $content): void + { + fwrite($this->resource, $content); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/JsonNormalizer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/JsonNormalizer.php new file mode 100644 index 0000000..1368ed0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/JsonNormalizer.php @@ -0,0 +1,159 @@ + + */ +final class JsonNormalizer implements Normalizer +{ + private const ACCEPTABLE_JSON_OPTIONS = JSON_FORCE_OBJECT + | JSON_HEX_QUOT + | JSON_HEX_TAG + | JSON_HEX_AMP + | JSON_HEX_APOS + | JSON_INVALID_UTF8_IGNORE + | JSON_INVALID_UTF8_SUBSTITUTE + | JSON_NUMERIC_CHECK + | JSON_PRETTY_PRINT + | JSON_PRESERVE_ZERO_FRACTION + | JSON_UNESCAPED_LINE_TERMINATORS + | JSON_UNESCAPED_SLASHES + | JSON_UNESCAPED_UNICODE + | JSON_THROW_ON_ERROR; + + private Transformer $transformer; + + private int $jsonEncodingOptions; + + /** + * Internal note + * ------------- + * + * We could use the `int-mask-of` annotation + * to let PHPStan infer the type of the accepted options, but some caveats + * were found: + * - SA tools are not able to infer that we end up having only accepted + * options. Might be fixed with https://github.com/phpstan/phpstan/issues/9384 + * for PHPStan but Psalm does have some (not all) issues as well. + * - Using this annotation provokes *severe* performance issues when + * running PHPStan analysis, therefore it is preferable to avoid it. + */ + public function __construct( + Transformer $transformer, + int $jsonEncodingOptions = JSON_THROW_ON_ERROR, + ) { + $this->transformer = $transformer; + $this->jsonEncodingOptions = (self::ACCEPTABLE_JSON_OPTIONS & $jsonEncodingOptions) | JSON_THROW_ON_ERROR; + } + + /** + * By default, the JSON normalizer will only use `JSON_THROW_ON_ERROR` to + * encode non-boolean scalar values. There might be use-cases where projects + * will need flags like `JSON_JSON_PRESERVE_ZERO_FRACTION`. + * + * This can be achieved by passing these flags to this method: + * + * ```php + * $normalizer = (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->normalizer(\OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\Format::json()) + * ->withOptions(\JSON_PRESERVE_ZERO_FRACTION); + * + * $lowerManhattanAsJson = $normalizer->normalize( + * new \My\App\Coordinates( + * longitude: 40.7128, + * latitude: -74.0000 + * ) + * ); + * + * // `$lowerManhattanAsJson` is a valid JSON string representing the data: + * // {"longitude":40.7128,"latitude":-74.0000} + * ``` + */ + public function withOptions(int $options): self + { + return new self($this->transformer, $options); + } + + public function normalize(mixed $value): string + { + $result = $this->transformer->transform($value); + + /** @var resource $resource */ + $resource = fopen('php://memory', 'w'); + + $formatter = new JsonFormatter($resource, $this->jsonEncodingOptions); + $formatter->format($result); + + rewind($resource); + + /** @var string */ + $json = stream_get_contents($resource); + + fclose($resource); + + return $json; + } + + /** + * Returns a new normalizer that will write the JSON to the given resource + * instead of returning a string. + * + * A benefit of streaming the data to a PHP resource is that it may be more + * memory-efficient when using generators — for instance when querying a + * database: + * + * ```php + * // In this example, we assume that the result of the query below is a + * // generator, every entry will be yielded one by one, instead of + * // everything being loaded in memory at once. + * $users = $database->execute('SELECT * FROM users'); + * + * $file = fopen('path/to/some_file.json', 'w'); + * + * $normalizer = (new \OCA\Talk\Vendor\CuyZ\Valinor\MapperBuilder()) + * ->normalizer(\OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\Format::json()) + * ->streamTo($file); + * + * // Even if there are thousands of users, memory usage will be kept low + * // when writing JSON into the file. + * $normalizer->normalize($users); + * ``` + * + * @param resource $resource + */ + public function streamTo(mixed $resource): StreamNormalizer + { + // This check is there to help people that do not use static analyzers. + if (! is_resource($resource)) { + throw new RuntimeException('Expected a valid resource, got ' . get_debug_type($resource)); + } + + return new StreamNormalizer($this->transformer, new JsonFormatter($resource, $this->jsonEncodingOptions)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Normalizer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Normalizer.php new file mode 100644 index 0000000..819d1ec --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Normalizer.php @@ -0,0 +1,25 @@ + + */ +final class StreamNormalizer implements Normalizer +{ + public function __construct( + private Transformer $transformer, + private JsonFormatter $formatter, + ) {} + + public function normalize(mixed $value): mixed + { + $result = $this->transformer->transform($value); + + return $this->formatter->format($result); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/CacheTransformer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/CacheTransformer.php new file mode 100644 index 0000000..84a43cb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/CacheTransformer.php @@ -0,0 +1,162 @@ +, Transformer): Transformer> */ + private CacheInterface $cache, + /** @var list */ + private array $transformers, + ) {} + + public function transform(mixed $value): mixed + { + $type = $this->inferType($value, isSure: true); + + $key = "transformer-\0" . $type->toString(); + + $entry = $this->cache->get($key); + + if ($entry) { + $transformer = $entry($this->transformers, $this); + + return $transformer->transform($value); + } + + $transformer = new EvaluatedTransformer($this->compileFor($type)); + + // @phpstan-ignore argument.type (this is a temporary workaround, while waiting for the cache API to be refined) + $this->cache->set($key, $transformer); + + $entry = $this->cache->get($key); + + // @phpstan-ignore callable.nonCallable (this is a temporary workaround, while waiting for the cache API to be refined) + $transformer = $entry($this->transformers, $this); + + return $transformer->transform($value); + } + + private function compileFor(Type $type): string + { + $rootNode = new TransformerRootNode($this->definitionBuilder, $type); + + $node = Node::shortClosure($rootNode) + ->witParameters( + Node::parameterDeclaration('transformers', 'array'), + Node::parameterDeclaration('delegate', Transformer::class), + ); + + return (new Compiler())->compile($node)->code(); + } + + private function inferType(mixed $value, bool $isSure = false): Type + { + // @infection-ignore-all (mutation from `true` to `false` is useless) + return match (true) { + $value instanceof UnitEnum => EnumType::native($value::class), + is_object($value) && ! $value instanceof Closure && ! $value instanceof Generator => $this->inferObjectType($value), + is_iterable($value) => $this->inferIterableType($value), + is_scalar($value) && $isSure => ValueTypeFactory::from($value), + is_string($value) => NativeStringType::get(), + is_int($value) => NativeIntegerType::get(), + is_float($value) => NativeFloatType::get(), + is_bool($value) => NativeBooleanType::get(), + is_null($value) => NullType::get(), + default => throw new TypeUnhandledByNormalizer($value), + }; + } + + private function inferObjectType(object $value): NativeClassType + { + if (is_iterable($value)) { + $iterableType = $this->inferIterableType($value); + + return new NativeClassType($value::class, ['SubType' => $iterableType->subType()]); + } + + return new NativeClassType($value::class); + } + + /** + * This method contains a strongly opinionated rule: when normalizing an + * iterable, we assume that the iterable has a high probability of + * containing only one type of value, each iteration matching the type of + * the first value. + * + * This is a trade-off between performance and accuracy: the first value + * will always be normalized, so we can safely add its type to the compiling + * process. For other values, if their types match the first one, that is a + * big performance win; if they don't, the transformer will do its best to + * find an optimized way of dealing with it or, by default, fall back to + * the delegate transformer. + * + * @param iterable $value + */ + private function inferIterableType(iterable $value): CompositeTraversableType + { + if (is_array($value)) { + if ($value === []) { + return ArrayType::native(); + } + + $firstValueType = $this->inferType(reset($value)); + + if (array_is_list($value)) { + return new NonEmptyListType($firstValueType); + } + + return new NonEmptyArrayType(ArrayKeyType::default(), $firstValueType); + } + + if ($value instanceof IteratorAggregate) { + $value = $value->getIterator(); + } + + if ($value instanceof Iterator && $value->valid()) { + $firstValueType = $this->inferType($value->current()); + + return new IterableType(ArrayKeyType::default(), $firstValueType); + } + + return IterableType::native(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinition.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinition.php new file mode 100644 index 0000000..fd2e492 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinition.php @@ -0,0 +1,100 @@ + */ + private array $transformerAttributes = []; + + public function __construct( + public readonly Type $type, + /** @var array */ + private array $transformerTypes, + private TypeFormatter $typeFormatter, + ) {} + + public function typeFormatter(): TypeFormatter + { + $typeFormatter = $this->typeFormatter; + + if ($this->transformerTypes !== [] || $this->transformerAttributes !== []) { + $typeFormatter = new RegisteredTransformersFormatter( + $this->type, + $typeFormatter, + $this->transformerTypes, + $this->transformerAttributes, + ); + } + + if (! $this->isSure && ! $this->type instanceof UnresolvableType) { + return new UnsureTypeFormatter($typeFormatter, $this->type); + } + + return $typeFormatter; + } + + /** + * @param list $transformerAttributes + */ + public function withTransformerAttributes(array $transformerAttributes): self + { + $self = clone $this; + $self->transformerAttributes = [...$self->transformerAttributes, ...array_reverse($transformerAttributes)]; + + return $self; + } + + public function hasTransformers(): bool + { + return $this->transformerTypes !== [] || $this->transformerAttributes !== []; + } + + public function markAsSure(): self + { + $self = clone $this; + $self->isSure = true; + + return $self; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinitionBuilder.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinitionBuilder.php new file mode 100644 index 0000000..a9d9d5c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerDefinitionBuilder.php @@ -0,0 +1,127 @@ +transformerTypes($type), + typeFormatter: $this->typeFormatter($type), + ); + + if ($type instanceof ClassType) { + // A class may have transformer attributes, in which case they are + // added to the list of attributes for this definition. + $definition = $definition->withTransformerAttributes( + $this->classDefinitionRepository + ->for($type) + ->attributes + ->filter(TransformerContainer::filterTransformerAttributes(...)) + ->toArray(), + ); + } + + if ($type instanceof MixedType) { + $definition = $definition->markAsSure(); + } + + return $definition; + } + + /** + * @return array + */ + private function transformerTypes(Type $type): array + { + // If the type is a union type, we don't need to add the transformer + // because it will be added for each subtype anyway. This condition also + // prevents the transformers from being applied twice in these cases. + if ($type instanceof UnionType) { + return []; + } + + $types = []; + + foreach ($this->transformerContainer->transformers() as $key => $transformer) { + $function = $this->functionDefinitionRepository->for($transformer); + $transformerType = $function->parameters->at(0)->type; + + if (! $type->matches($transformerType)) { + continue; + } + + $types[$key] = $transformerType; + } + + return array_reverse($types, preserve_keys: true); + } + + private function typeFormatter(Type $type): TypeFormatter + { + // @infection-ignore-all (mutation from `true` to `false` is useless) + return match (true) { + $type instanceof CompositeTraversableType => new TraversableFormatter($type->subType()), + $type instanceof EnumType => new EnumFormatter($type), + $type instanceof InterfaceType => new InterfaceFormatter($type), + $type instanceof NativeClassType => match (true) { + $type->className() === UnitEnum::class => new UnitEnumFormatter(), + $type->className() === stdClass::class => new StdClassFormatter(), + is_a($type->className(), DateTimeInterface::class, true) => new DateTimeFormatter(), + is_a($type->className(), DateTimeZone::class, true) => new DateTimeZoneFormatter(), + is_a($type->className(), Traversable::class, true) => new TraversableFormatter($type->generics()['SubType'] ?? MixedType::get()), + default => new ClassFormatter($this->classDefinitionRepository->for($type)), + }, + $type instanceof NullType => new NullFormatter(), + $type instanceof ScalarType => new ScalarFormatter(), + $type instanceof ShapedArrayType => new ShapedArrayFormatter($type), + $type instanceof UnionType => new UnionFormatter($type), + default => new MixedFormatter(), + }; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerRootNode.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerRootNode.php new file mode 100644 index 0000000..41146ad --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TransformerRootNode.php @@ -0,0 +1,73 @@ +definitionBuilder->for($this->type); + $definition = $definition->markAsSure(); + + $classNode = $this->transformerClassNode($definition); + $classNode = $definition->typeFormatter()->manipulateTransformerClass($classNode, $this->definitionBuilder); + + return $classNode->compile($compiler); + } + + private function transformerClassNode(TransformerDefinition $definition): AnonymousClassNode + { + return Node::anonymousClass() + ->implements(Transformer::class) + ->withArguments( + Node::variable('transformers'), + Node::variable('delegate'), + ) + ->withProperties( + Node::propertyDeclaration('transformers', 'array'), + Node::propertyDeclaration('delegate', Transformer::class), + ) + ->withMethods( + MethodNode::constructor() + ->withVisibility('public') + ->witParameters( + Node::parameterDeclaration('transformers', 'array'), + Node::parameterDeclaration('delegate', Transformer::class), + ) + ->withBody( + Node::property('transformers')->assign(Node::variable('transformers'))->asExpression(), + Node::property('delegate')->assign(Node::variable('delegate'))->asExpression(), + ), + Node::method('transform') + ->withVisibility('public') + ->witParameters( + Node::parameterDeclaration('value', 'mixed'), + ) + ->withReturnType('mixed') + ->withBody( + Node::variable('references')->assign(Node::newClass(WeakMap::class))->asExpression(), + Node::return( + $definition->typeFormatter()->formatValueNode( + Node::variable('value'), + ), + ), + ), + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ClassFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ClassFormatter.php new file mode 100644 index 0000000..7aa62f8 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ClassFormatter.php @@ -0,0 +1,212 @@ +callMethod( + method: $this->methodName(), + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + $methodName = $this->methodName(); + + if ($class->hasMethod($methodName)) { + return $class; + } + + // This is a placeholder method to avoid circular references coming from + // the class properties. + $class = $class->withMethods(Node::method($methodName)); + + $valuesNode = $this->valuesNode(Node::variable('value')); + + $nodes = [ + ...$this->checkCircularObjectReference(), + Node::variable('values')->assign($valuesNode)->asExpression(), + ]; + + $transformedNodes = [ + Node::variable('transformed')->assign(Node::array())->asExpression(), + ]; + + $shouldUseTransformedNodes = false; + + foreach ($this->class->properties as $property) { + $propertyDefinition = $definitionBuilder->for($property->type); + + if (! $property->nativeType instanceof MixedType) { + $propertyDefinition = $propertyDefinition->markAsSure(); + } + + if (! $property->type instanceof UnresolvableType) { + $propertyDefinition = $propertyDefinition->withTransformerAttributes( + $property->attributes + ->filter(TransformerContainer::filterTransformerAttributes(...)) + ->filter( + static fn (AttributeDefinition $attribute): bool => $property->type->matches( + $attribute->class->methods->get('normalize')->parameters->at(0)->type, + ), + )->toArray(), + ); + } + + $typeFormatter = $propertyDefinition->typeFormatter(); + + $keyTransformerAttributes = $property->attributes + ->filter(TransformerContainer::filterKeyTransformerAttributes(...)) + ->toArray(); + + if ($keyTransformerAttributes === []) { + $key = Node::value($property->name); + } else { + $transformedNodes[] = Node::variable('key')->assign(Node::value($property->name))->asExpression(); + + foreach ($keyTransformerAttributes as $attribute) { + $transformedNodes[] = Node::variable('key')->assign( + (new NewAttributeNode($attribute))->wrap()->callMethod( + method: 'normalizeKey', + arguments: [Node::variable('key')], + ), + )->asExpression(); + } + + $key = Node::variable('key'); + } + + $transformedNodes[] = Node::variable('transformed') + ->key($key) + ->assign($typeFormatter->formatValueNode( + Node::variable('values')->key(Node::value($property->name)), + ))->asExpression(); + + $class = $typeFormatter->manipulateTransformerClass($class, $definitionBuilder); + + $shouldUseTransformedNodes = $shouldUseTransformedNodes + || $propertyDefinition->hasTransformers() + || $keyTransformerAttributes !== [] + || $property->type instanceof UnresolvableType + || ! $property->nativeType instanceof ScalarType; + } + + if ($shouldUseTransformedNodes) { + $nodes = [ + ...$nodes, + ...$transformedNodes, + Node::return(Node::variable('transformed')) + ]; + } else { + $nodes[] = Node::return(Node::variable('values')); + } + + return $class->withMethods( + Node::method($methodName) + ->witParameters( + Node::parameterDeclaration('value', str_contains($this->class->name, '@anonymous') ? 'object' : $this->class->name), + Node::parameterDeclaration('references', WeakMap::class), + ) + ->withReturnType('array') + ->withBody(...$nodes), + ); + } + + /** + * @return list + */ + private function checkCircularObjectReference(): array + { + return [ + Node::if( + condition: Node::functionCall('isset', [Node::variable('references')->key(Node::variable('value'))]), + body: Node::throw( + Node::newClass(CircularReferenceFoundDuringNormalization::class, Node::variable('value')), + )->asExpression(), + ), + Node::variable('references')->assign(Node::variable('references')->clone())->asExpression(), + Node::variable('references')->key(Node::variable('value'))->assign(Node::variable('value'))->asExpression(), + ]; + } + + /** + * This method returns a node responsible for getting the properties' values + * of an object. + * + * First scenario: if all properties are public, we can extract them easily + * by accessing the properties directly, resulting in a code like this: + * + * ``` + * [ + * 'someProperty' => $value->someProperty, + * 'anotherProperty' => $value->anotherProperty, + * ] + * ``` + * + * Second scenario: at least one property is protected/private. In this + * case, we need to "extract" the properties using `get_object_vars`: + * + * ``` + * (fn () => get_object_vars($this))->call($value) + * ``` + */ + private function valuesNode(ComplianceNode $valueNode): Node + { + $allPropertiesArePublic = true; + $assignments = []; + + foreach ($this->class->properties as $property) { + $assignments[$property->name] = $valueNode->access($property->name); + + $allPropertiesArePublic = $allPropertiesArePublic && $property->isPublic; + } + + if ($allPropertiesArePublic) { + return Node::array($assignments); + } + + return Node::shortClosure( + return: Node::functionCall( + name: 'get_object_vars', + arguments: [Node::this()], + ), + )->wrap()->callMethod('call', [$valueNode]); + } + + /** + * @return non-empty-string + */ + private function methodName(): string + { + $slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->class->type->toString())); + + return "transform_object_{$slug}_" . hash('xxh128', $this->class->type->toString()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeFormatter.php new file mode 100644 index 0000000..14f9af9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeFormatter.php @@ -0,0 +1,26 @@ +callMethod('format', [ + Node::value('Y-m-d\\TH:i:s.uP'), // RFC 3339 + ]); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + return $class; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeZoneFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeZoneFormatter.php new file mode 100644 index 0000000..dac776d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/DateTimeZoneFormatter.php @@ -0,0 +1,24 @@ +callMethod('getName'); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + return $class; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/EnumFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/EnumFormatter.php new file mode 100644 index 0000000..24289a2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/EnumFormatter.php @@ -0,0 +1,32 @@ +type->className(), BackedEnum::class, true) + ? $valueNode->access('value') + : $valueNode->access('name'); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + return $class; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/InterfaceFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/InterfaceFormatter.php new file mode 100644 index 0000000..e63e4df --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/InterfaceFormatter.php @@ -0,0 +1,36 @@ +type->className() === DateTimeInterface::class) { + return (new DateTimeFormatter())->formatValueNode($valueNode); + } + + return Node::this() + ->access('delegate') + ->callMethod('transform', [$valueNode]); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + return $class; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/MixedFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/MixedFormatter.php new file mode 100644 index 0000000..961e35d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/MixedFormatter.php @@ -0,0 +1,90 @@ +callMethod( + method: 'transform_mixed', + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + if ($class->hasMethod('transform_mixed')) { + return $class; + } + + // This is a placeholder method to avoid infinite loops. + $class = $class->withMethods(Node::method('transform_mixed')); + + $nodes = []; + + $types = [ + NativeBooleanType::get(), + NativeFloatType::get(), + NativeIntegerType::get(), + NativeStringType::get(), + NullType::get(), + new NativeClassType(UnitEnum::class), + new NativeClassType(DateTime::class), + new NativeClassType(DateTimeZone::class), + IterableType::native(), + ]; + + foreach ($types as $type) { + $definition = $definitionBuilder->for($type)->markAsSure(); + + $class = $definition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + $nodes[] = Node::if( + condition: new TypeAcceptNode(Node::variable('value'), $definition->type), + body: Node::return($definition->typeFormatter()->formatValueNode(Node::variable('value'))), + ); + } + + $nodes[] = Node::return( + Node::this() + ->access('delegate') + ->callMethod('transform', [ + Node::variable('value'), + ]), + ); + + return $class->withMethods( + Node::method('transform_mixed') + ->witParameters( + Node::parameterDeclaration('value', 'mixed'), + Node::parameterDeclaration('references', WeakMap::class), + ) + ->withReturnType('mixed') + ->withBody(...$nodes), + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/NullFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/NullFormatter.php new file mode 100644 index 0000000..dfeb2a5 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/NullFormatter.php @@ -0,0 +1,24 @@ + */ + private array $transformerTypes, + /** @var list */ + private array $transformerAttributes = [], + ) {} + + public function formatValueNode(ComplianceNode $valueNode): Node + { + return Node::this()->callMethod( + method: $this->methodName(), + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + $methodName = $this->methodName(); + + if ($class->hasMethod($methodName)) { + return $class; + } + + $class = $this->delegate->manipulateTransformerClass($class, $definitionBuilder); + + $nodes = [ + Node::variable('next')->assign( + Node::shortClosure( + $this->delegate->formatValueNode(Node::variable('value')), + ), + )->asExpression(), + + ...array_map( + fn (int $key, Type $transformerType) => Node::if( + condition: new TypeAcceptNode(Node::variable('value'), $transformerType), + body: Node::variable('next')->assign( + Node::shortClosure( + return: Node::this() + ->access('transformers') + ->key(Node::value($key)) + ->call(arguments: [ + Node::variable('value'), + Node::variable('next'), + ]), + ), + )->asExpression(), + ), + array_keys($this->transformerTypes), + $this->transformerTypes, + ), + + ...array_map( + fn (AttributeDefinition $attribute) => Node::variable('next')->assign( + Node::shortClosure( + return: (new NewAttributeNode($attribute)) + ->wrap() + ->callMethod( + method: 'normalize', + arguments: [ + Node::variable('value'), + Node::variable('next'), + ], + ), + ), + )->asExpression(), + $this->transformerAttributes, + ), + + Node::return( + Node::variable('next')->call(), + ), + ]; + + return $class->withMethods( + Node::method($methodName) + ->witParameters( + Node::parameterDeclaration('value', 'mixed'), + Node::parameterDeclaration('references', WeakMap::class), + ) + ->withReturnType('mixed') + ->withBody(...$nodes), + ); + } + + /** + * @return non-empty-string + */ + private function methodName(): string + { + $slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->type->toString())); + + return "transform_{$slug}_" . sha1(serialize([$this->type->toString(), $this->transformerAttributes])); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ScalarFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ScalarFormatter.php new file mode 100644 index 0000000..ba77598 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/ScalarFormatter.php @@ -0,0 +1,24 @@ +callMethod( + method: $this->methodName(), + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + $methodName = $this->methodName(); + + if ($class->hasMethod($methodName)) { + return $class; + } + + if ($this->type->isUnsealed()) { + $defaultDefinition = $definitionBuilder->for($this->type->unsealedType()->subType()); + } else { + $defaultDefinition = $definitionBuilder->for(MixedType::get()); + } + + $class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + $elementsDefinitions = []; + + foreach ($this->type->elements() as $element) { + $elementDefinition = $definitionBuilder->for($element->type()); + + $class = $elementDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + $elementsDefinitions[$element->key()->value()] = $elementDefinition; + } + + return $class->withMethods( + Node::method($methodName) + ->witParameters( + Node::parameterDeclaration('value', 'array'), + Node::parameterDeclaration('references', WeakMap::class), + ) + ->withReturnType('array') + ->withBody( + Node::variable('result')->assign(Node::array())->asExpression(), + Node::forEach( + value: Node::variable('value'), + key: 'key', + item: 'item', + body: Node::variable('result')->key(Node::variable('key'))->assign( + (function () use ($defaultDefinition, $elementsDefinitions) { + $match = Node::match(Node::variable('key')); + + foreach ($elementsDefinitions as $name => $definition) { + $match = $match->withCase( + condition: Node::value($name), + body: $definition->typeFormatter()->formatValueNode(Node::variable('item')), + ); + } + + return $match->withDefaultCase( + $defaultDefinition->typeFormatter()->formatValueNode(Node::variable('item')), + ); + })(), + )->asExpression(), + ), + Node::return(Node::variable('result')), + ), + ); + } + + /** + * @return non-empty-string + */ + private function methodName(): string + { + return 'transform_shaped_array_' . hash('xxh128', $this->type->toString()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/StdClassFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/StdClassFormatter.php new file mode 100644 index 0000000..493a134 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/StdClassFormatter.php @@ -0,0 +1,68 @@ +hasMethod('transform_stdclass')) { + return $class; + } + + $defaultDefinition = $definitionBuilder->for(MixedType::get()); + + $class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + return $class->withMethods( + Node::method('transform_stdclass') + ->witParameters( + Node::parameterDeclaration('value', stdClass::class), + Node::parameterDeclaration('references', \WeakMap::class), + ) + ->withReturnType('mixed') + ->withBody( + Node::variable('values')->assign(Node::variable('value')->castToArray())->asExpression(), + Node::if( + condition: Node::variable('values')->equals(Node::array()), + body: Node::return( + Node::class(EmptyObject::class)->callStaticMethod('get'), + ), + ), + Node::return( + Node::functionCall( + name: 'array_map', + arguments: [ + Node::shortClosure( + return: $defaultDefinition->typeFormatter()->formatValueNode(Node::variable('value')), + )->witParameters(Node::parameterDeclaration('value', 'mixed')), + Node::variable('value')->castToArray(), + ], + ) + ), + ), + ); + } + + public function formatValueNode(ComplianceNode $valueNode): Node + { + return Node::this()->callMethod( + method: 'transform_stdclass', + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TraversableFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TraversableFormatter.php new file mode 100644 index 0000000..d2c3d54 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TraversableFormatter.php @@ -0,0 +1,114 @@ +callMethod( + method: $this->methodName(), + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + /** + * If the input is an array, we use `array_map` to format all sub-values + * easily. If the input is not an array, we return a generator that will + * yield all transformed values one at a time. + * + * Generated code should look like: + * + * ```php + * if (is_array($value)) { + * return array_map( + * fn ($item) => $this->some_function($item), + * $value, + * ); + * } + * + * return (function () use ($value) { + * foreach ($value as $key => $item) { + * yield $key => $this->some_function($item); + * } + * })(); + * ``` + */ + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + $methodName = $this->methodName(); + + if ($class->hasMethod($methodName)) { + return $class; + } + + $subDefinition = $definitionBuilder->for($this->subType); + + $class = $subDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + return $class->withMethods( + Node::method($methodName) + ->witParameters( + Node::parameterDeclaration('value', 'iterable'), + Node::parameterDeclaration('references', WeakMap::class), + ) + ->withReturnType('iterable') + ->withBody( + Node::if( + condition: Node::functionCall('is_array', [Node::variable('value')]), + body: Node::return( + Node::functionCall( + name: 'array_map', + arguments: [ + Node::shortClosure( + return: $subDefinition->typeFormatter()->formatValueNode(Node::variable('item')), + )->witParameters(Node::parameterDeclaration('item', 'mixed')), + Node::variable('value') + ], + ), + ) + ), + Node::return( + Node::closure( + Node::forEach( + value: Node::variable('value'), + key: 'key', + item: 'item', + body: Node::yield( + key: Node::variable('key'), + value: $subDefinition->typeFormatter()->formatValueNode(Node::variable('item')), + )->asExpression(), + ) + )->uses('value', 'references')->wrap()->call(), + ), + ), + ); + } + + /** + * @return non-empty-string + */ + private function methodName(): string + { + $slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->subType->toString())); + + return "transform_iterable_{$slug}_" . hash('xxh128', $this->subType->toString()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TypeFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TypeFormatter.php new file mode 100644 index 0000000..19378fb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/TypeFormatter.php @@ -0,0 +1,18 @@ +callMethod( + method: $this->methodName(), + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + $methodName = $this->methodName(); + + if ($class->hasMethod($methodName)) { + return $class; + } + + $defaultDefinition = $definitionBuilder->for(MixedType::get()); + + $class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + $nodes = []; + + foreach ($this->type->types() as $subType) { + $definition = $definitionBuilder->for($subType)->markAsSure(); + + $class = $definition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + $nodes[] = Node::if( + condition: new TypeAcceptNode(Node::variable('value'), $definition->type), + body: Node::return( + $definition->typeFormatter()->formatValueNode(Node::variable('value')), + ), + ); + } + + $nodes[] = Node::return( + $defaultDefinition->typeFormatter()->formatValueNode(Node::variable('value')), + ); + + return $class->withMethods( + Node::method($methodName) + ->witParameters( + Node::parameterDeclaration('value', 'mixed'), + Node::parameterDeclaration('references', WeakMap::class), + ) + ->withReturnType('mixed') + ->withBody(...$nodes), + ); + } + + /** + * @return non-empty-string + */ + private function methodName(): string + { + return 'transform_union_' . hash('xxh128', $this->type->toString()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnitEnumFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnitEnumFormatter.php new file mode 100644 index 0000000..3357896 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnitEnumFormatter.php @@ -0,0 +1,29 @@ +instanceOf(BackedEnum::class), + ifTrue: $valueNode->access('value'), + ifFalse: $valueNode->access('name'), + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + return $class; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnsureTypeFormatter.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnsureTypeFormatter.php new file mode 100644 index 0000000..050eae6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Compiler/TypeFormatter/UnsureTypeFormatter.php @@ -0,0 +1,78 @@ +callMethod( + method: $this->methodName(), + arguments: [ + $valueNode, + Node::variable('references'), + ], + ); + } + + public function manipulateTransformerClass(AnonymousClassNode $class, TransformerDefinitionBuilder $definitionBuilder): AnonymousClassNode + { + $methodName = $this->methodName(); + + if ($class->hasMethod($methodName)) { + return $class; + } + + $class = $this->delegate->manipulateTransformerClass($class, $definitionBuilder); + + $defaultDefinition = $definitionBuilder->for(MixedType::get()); + + $class = $defaultDefinition->typeFormatter()->manipulateTransformerClass($class, $definitionBuilder); + + return $class->withMethods( + Node::method($methodName) + ->witParameters( + Node::parameterDeclaration('value', 'mixed'), + Node::parameterDeclaration('references', \WeakMap::class), + ) + ->withReturnType('mixed') + ->withBody( + Node::if( + condition: Node::negate( + (new TypeAcceptNode(Node::variable('value'), $this->unsureType->nativeType()))->wrap(), + ), + body: Node::return($defaultDefinition->typeFormatter()->formatValueNode(Node::variable('value'))), + ), + Node::return( + $this->delegate->formatValueNode(Node::variable('value')), + ), + ), + ); + } + + /** + * @return non-empty-string + */ + private function methodName(): string + { + $slug = preg_replace('/[^a-z0-9]+/', '_', strtolower($this->unsureType->toString())); + + return "transform_unsure_{$slug}_" . hash('xxh128', $this->unsureType->toString()); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/EmptyObject.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/EmptyObject.php new file mode 100644 index 0000000..6f6c2eb --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/EmptyObject.php @@ -0,0 +1,13 @@ +doTransform($value, new WeakMap()); // @phpstan-ignore-line + } + + /** + * @param WeakMap $references + * @param list $attributes + */ + private function doTransform(mixed $value, WeakMap $references, array $attributes = []): mixed + { + if (is_object($value)) { + if (isset($references[$value])) { + throw new CircularReferenceFoundDuringNormalization($value); + } + + $references = clone $references; + $references[$value] = $value; + + $type = $value instanceof UnitEnum + ? EnumType::native($value::class) + : new NativeClassType($value::class); + + $classAttributes = $this->classDefinitionRepository->for($type)->attributes->toArray(); + + $attributes = [...$attributes, ...$classAttributes]; + } + + if (! $this->transformerContainer->hasTransformers() && $attributes === []) { + return $this->defaultTransformer($value, $references); + } + + if ($attributes !== []) { + $attributes = (new Attributes(...$attributes)) + ->filter(TransformerContainer::filterTransformerAttributes(...)) + ->toArray(); + } + + $transformers = [ + // First chunk of transformers to be used: attributes, coming from + // class or property. + ...array_map( + fn (AttributeDefinition $attribute) => $attribute->instantiate()->normalize(...), // @phpstan-ignore-line / We know the method exists + $attributes, + ), + // Second chunk of transformers to be used: registered transformers. + ...$this->transformerContainer->transformers(), + // Last one: default transformer. + fn (mixed $value) => $this->defaultTransformer($value, $references), + ]; + + return call_user_func($this->nextTransformer($transformers, $value)); + } + + /** + * @param non-empty-list $transformers + */ + private function nextTransformer(array $transformers, mixed $value): callable + { + $transformer = array_shift($transformers); + + if ($transformers === []) { + return fn () => $transformer($value); + } + + $function = $this->functionDefinitionRepository->for($transformer); + + if (! $function->parameters->at(0)->type->accepts($value)) { + return $this->nextTransformer($transformers, $value); + } + + return fn () => $transformer($value, fn () => call_user_func($this->nextTransformer($transformers, $value))); + } + + /** + * @param WeakMap $references + * @return iterable|scalar|null + */ + private function defaultTransformer(mixed $value, WeakMap $references): mixed + { + if ($value === null) { + return null; + } + + if (is_scalar($value)) { + return $value; + } + + if (is_iterable($value)) { + if (is_array($value)) { + return array_map( + fn (mixed $value) => $this->doTransform($value, $references), + $value + ); + } + + return (function () use ($value, $references) { + foreach ($value as $key => $item) { + yield $key => $this->doTransform($item, $references); + } + })(); + } + + if (is_object($value) && ! $value instanceof Closure) { + if ($value instanceof UnitEnum) { + return $value instanceof BackedEnum ? $value->value : $value->name; + } + + if ($value instanceof DateTimeInterface) { + return $value->format('Y-m-d\\TH:i:s.uP'); // RFC 3339 + } + + if ($value instanceof DateTimeZone) { + return $value->getName(); + } + + if ($value::class === stdClass::class) { + $result = (array)$value; + + if ($result === []) { + return EmptyObject::get(); + } + + return array_map( + fn (mixed $value) => $this->doTransform($value, $references), + $result, + ); + } + + $values = (fn () => get_object_vars($this))->call($value); + + $transformed = []; + + $class = $this->classDefinitionRepository->for(new NativeClassType($value::class)); + + foreach ($values as $key => $subValue) { + $property = $class->properties->get($key); + + $keyTransformersAttributes = $property->attributes->filter(TransformerContainer::filterKeyTransformerAttributes(...)); + + foreach ($keyTransformersAttributes as $attribute) { + $method = $attribute->class->methods->get('normalizeKey'); + + if ($method->parameters->count() === 0 || $method->parameters->at(0)->type->accepts($key)) { + $key = $attribute->instantiate()->normalizeKey($key); // @phpstan-ignore-line / We know the method exists + } + } + + $transformed[$key] = $this->doTransform($subValue, $references, $property->attributes->toArray()); + } + + return $transformed; + } + + throw new TypeUnhandledByNormalizer($value); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Transformer.php b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Transformer.php new file mode 100644 index 0000000..deb9a8f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Normalizer/Transformer/Transformer.php @@ -0,0 +1,11 @@ + */ + private array $transformers, + ) {} + + public function hasTransformers(): bool + { + return $this->transformers !== []; + } + + /** + * @return list + */ + public function transformers(): array + { + if (! $this->transformersCallablesWereChecked) { + $this->transformersCallablesWereChecked = true; + + foreach ($this->transformers as $transformer) { + $function = $this->functionDefinitionRepository->for($transformer); + + self::checkTransformer($function); + } + } + + return $this->transformers; + } + + public static function filterTransformerAttributes(AttributeDefinition $attribute): bool + { + return $attribute->class->methods->has('normalize') + && self::checkTransformer($attribute->class->methods->get('normalize')); + } + + public static function filterKeyTransformerAttributes(AttributeDefinition $attribute): bool + { + return $attribute->class->methods->has('normalizeKey') + && self::checkKeyTransformer($attribute->class->methods->get('normalizeKey')); + } + + private static function checkTransformer(MethodDefinition|FunctionDefinition $method): bool + { + $parameters = $method->parameters; + + if ($parameters->count() === 0) { + throw new TransformerHasNoParameter($method); + } + + if ($parameters->count() > 2) { + throw new TransformerHasTooManyParameters($method); + } + + if ($parameters->count() > 1 && ! $parameters->at(1)->nativeType instanceof CallableType) { + throw new TransformerHasInvalidCallableParameter($method, $parameters->at(1)->nativeType); + } + + return true; + } + + private static function checkKeyTransformer(MethodDefinition $method): bool + { + $parameters = $method->parameters; + + if ($parameters->count() > 1) { + throw new KeyTransformerHasTooManyParameters($method); + } + + if ($parameters->count() > 0) { + $type = $parameters->at(0)->type; + + if (! $type instanceof StringType) { + throw new KeyTransformerParameterInvalidType($method); + } + } + + return true; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/BooleanType.php b/lib/Vendor/CuyZ/Valinor/Type/BooleanType.php new file mode 100644 index 0000000..dbc8c31 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/BooleanType.php @@ -0,0 +1,11 @@ + + */ + public function types(): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/CompositeTraversableType.php b/lib/Vendor/CuyZ/Valinor/Type/CompositeTraversableType.php new file mode 100644 index 0000000..f29ca43 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/CompositeTraversableType.php @@ -0,0 +1,15 @@ + + */ + public function traverse(): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/FixedType.php b/lib/Vendor/CuyZ/Valinor/Type/FixedType.php new file mode 100644 index 0000000..378e09d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/FixedType.php @@ -0,0 +1,9 @@ + + */ + public function generics(): array; +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/IntegerType.php b/lib/Vendor/CuyZ/Valinor/Type/IntegerType.php new file mode 100644 index 0000000..833054c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/IntegerType.php @@ -0,0 +1,11 @@ + */ + private array $types = []; + + public function __construct(private TypeParser $delegate) {} + + public function parse(string $raw): Type + { + return $this->types[$raw] ??= $this->delegate->parse($raw); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php new file mode 100644 index 0000000..0e8693b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php @@ -0,0 +1,24 @@ + $enumName + */ + public function __construct(string $enumName) + { + parent::__construct( + "Missing case name for enum `$enumName::?`.", + 1653468431 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Enum/MissingSpecificEnumCase.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Enum/MissingSpecificEnumCase.php new file mode 100644 index 0000000..0bc2c27 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Enum/MissingSpecificEnumCase.php @@ -0,0 +1,24 @@ + $enumName + */ + public function __construct(string $enumName) + { + parent::__construct( + "Missing specific case for enum `$enumName::?` (cannot be `*`).", + 1653468438 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/AssignedGenericNotFound.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/AssignedGenericNotFound.php new file mode 100644 index 0000000..b56bf0c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/AssignedGenericNotFound.php @@ -0,0 +1,24 @@ + $type->toString(), $generics)); + + parent::__construct( + "Could not find a template to assign the generic(s) `$list` for the class `$className`.", + 1604660485 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericClosingBracketMissing.php new file mode 100644 index 0000000..3e28dad --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericClosingBracketMissing.php @@ -0,0 +1,30 @@ + $type->toString(), $generics)) . '>'; + + parent::__construct( + "The closing bracket is missing for the generic `$signature`.", + 1604333677 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericCommaMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericCommaMissing.php new file mode 100644 index 0000000..77a63d4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/GenericCommaMissing.php @@ -0,0 +1,30 @@ + $type->toString(), $generics)) . ', ?>'; + + parent::__construct( + "A comma is missing for the generic `$signature`.", + 1615829484 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/InvalidAssignedGeneric.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/InvalidAssignedGeneric.php new file mode 100644 index 0000000..4b28d89 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/InvalidAssignedGeneric.php @@ -0,0 +1,25 @@ +toString()}` is not a subtype of `{$template->toString()}` for " . + "the template `$name` of the class `$className`.", + 1604613633 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/MissingGenerics.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/MissingGenerics.php new file mode 100644 index 0000000..6d608b1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Generic/MissingGenerics.php @@ -0,0 +1,38 @@ + $templates + */ + public function __construct(string $className, array $generics, array $templates) + { + /** @var positive-int $missing */ + $missing = count($templates) - count($generics); + $generics = array_map(fn (Type $type) => $type->toString(), $generics); + $generics += array_fill(count($generics), $missing, '?'); + + $signature = $className . '<' . implode(', ', $generics) . '>'; + + parent::__construct( + "There are $missing missing generics for `$signature`.", + 1618054357 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidIntersectionType.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidIntersectionType.php new file mode 100644 index 0000000..5297416 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidIntersectionType.php @@ -0,0 +1,20 @@ +toString()}`, it must be a class name or an interface name.", + 1608030163 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidType.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidType.php new file mode 100644 index 0000000..8683b18 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/InvalidType.php @@ -0,0 +1,10 @@ +toString()}`.", + 1606483975 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ArrayCommaMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ArrayCommaMissing.php new file mode 100644 index 0000000..3e91ade --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ArrayCommaMissing.php @@ -0,0 +1,32 @@ + $arrayType + */ + public function __construct(string $arrayType, Type $type) + { + $signature = "array<{$type->toString()}, ?>"; + + if ($arrayType === NonEmptyArrayType::class) { + $signature = "non-empty-array<{$type->toString()}, ?>"; + } + + parent::__construct( + "A comma is missing for `$signature`.", + 1606483614 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidArrayKey.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidArrayKey.php new file mode 100644 index 0000000..578f082 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidArrayKey.php @@ -0,0 +1,21 @@ +toString()}`, it must be a valid string or integer.", + 1604335007 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidIterableKey.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidIterableKey.php new file mode 100644 index 0000000..2a1c83d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/InvalidIterableKey.php @@ -0,0 +1,22 @@ +toString()}` for `iterable<{$keyType->toString()}, {$subType->toString()}>`. " . + "It must be one of `array-key`, `int` or `string`.", + 1618994708 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableClosingBracketMissing.php new file mode 100644 index 0000000..f433456 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableClosingBracketMissing.php @@ -0,0 +1,21 @@ +toString()}, {$subtype->toString()}>`.", + 1618994728 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableCommaMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableCommaMissing.php new file mode 100644 index 0000000..94e2208 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/IterableCommaMissing.php @@ -0,0 +1,21 @@ +toString()}, ?>`.", + 1618994669 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ListClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ListClosingBracketMissing.php new file mode 100644 index 0000000..460b602 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ListClosingBracketMissing.php @@ -0,0 +1,22 @@ +toString()}`.", + 1634035071 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayClosingBracketMissing.php new file mode 100644 index 0000000..f56b41c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayClosingBracketMissing.php @@ -0,0 +1,35 @@ + $element->toString(), $elements)); + + if ($unsealedType === false) { + $signature .= ', ...'; + } elseif ($unsealedType instanceof Type) { + $signature .= ', ...' . $unsealedType->toString(); + } + + parent::__construct( + "Missing closing curly bracket in shaped array signature `$signature`.", + 1631283658 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayColonTokenMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayColonTokenMissing.php new file mode 100644 index 0000000..e7bf863 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayColonTokenMissing.php @@ -0,0 +1,35 @@ + $element->toString(), $elements)); + + if (! empty($elements)) { + $signature .= ', '; + } + + $signature .= "{$type->toString()}?"; + + parent::__construct( + "A colon symbol is missing in shaped array signature `$signature`.", + 1631283847 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayCommaMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayCommaMissing.php new file mode 100644 index 0000000..d86b877 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayCommaMissing.php @@ -0,0 +1,28 @@ + $element->toString(), $elements)); + + parent::__construct( + "Comma missing in shaped array signature `$signature`.", + 1631286589 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayElementDuplicatedKey.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayElementDuplicatedKey.php new file mode 100644 index 0000000..c88d3e7 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayElementDuplicatedKey.php @@ -0,0 +1,20 @@ + $element->toString(), $elements)); + + if (! empty($elements)) { + $signature .= ', '; + } + + $signature .= $key->value(); + + if ($optional) { + $signature .= '?'; + } + + $signature .= ':'; + + parent::__construct( + "Missing element type in shaped array signature `$signature`.", + 1631286250 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayEmptyElements.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayEmptyElements.php new file mode 100644 index 0000000..5a0a725 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayEmptyElements.php @@ -0,0 +1,20 @@ + $element->toString(), $elements)); + $signature .= ', ...' . $unsealedType->toString(); + $signature .= '}'; + + parent::__construct( + "Invalid unsealed type `{$unsealedType->toString()}` in shaped array signature `$signature`, it should be a valid array.", + 1711618899, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayUnexpectedTokenAfterSealedType.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayUnexpectedTokenAfterSealedType.php new file mode 100644 index 0000000..d22b70b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayUnexpectedTokenAfterSealedType.php @@ -0,0 +1,36 @@ + $elements + * @param list $unexpectedTokens + */ + public function __construct(array $elements, Type $unsealedType, array $unexpectedTokens) + { + $unexpected = implode('', array_map(fn (Token $token) => $token->symbol(), $unexpectedTokens)); + + $signature = 'array{'; + $signature .= implode(', ', array_map(fn (ShapedArrayElement $element) => $element->toString(), $elements)); + $signature .= ', ...' . $unsealedType->toString(); + $signature .= $unexpected; + + parent::__construct( + "Unexpected `$unexpected` after sealed type in shaped array signature `$signature`, expected a `}`.", + 1711618958, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayWithoutElementsWithSealedType.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayWithoutElementsWithSealedType.php new file mode 100644 index 0000000..f6d877a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/ShapedArrayWithoutElementsWithSealedType.php @@ -0,0 +1,23 @@ +toString()}}"; + + parent::__construct( + "Missing elements in shaped array signature `$signature`.", + 1711629845, + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/SimpleArrayClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/SimpleArrayClosingBracketMissing.php new file mode 100644 index 0000000..4896c2d --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Iterable/SimpleArrayClosingBracketMissing.php @@ -0,0 +1,21 @@ +toString()}[]`.", + 1606474266 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfClosingBracketMissing.php new file mode 100644 index 0000000..d04e082 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfClosingBracketMissing.php @@ -0,0 +1,21 @@ +toString()}>`.", + 1717702289 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfIncorrectSubType.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfIncorrectSubType.php new file mode 100644 index 0000000..8d9baf1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfIncorrectSubType.php @@ -0,0 +1,21 @@ +toString()}>`, it should be a `BackedEnum`.", + 1717702683 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfOpeningBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfOpeningBracketMissing.php new file mode 100644 index 0000000..21f3860 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Magic/ValueOfOpeningBracketMissing.php @@ -0,0 +1,20 @@ +`.", + 1717702268 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/MissingClosingQuoteChar.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/MissingClosingQuoteChar.php new file mode 100644 index 0000000..7d42aa9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/MissingClosingQuoteChar.php @@ -0,0 +1,19 @@ +toString()}&?`.", + 1631612575 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/RightUnionTypeMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/RightUnionTypeMissing.php new file mode 100644 index 0000000..87fc5c3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/RightUnionTypeMissing.php @@ -0,0 +1,20 @@ +toString()}|?`.", + 1631294715 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ClassStringClosingBracketMissing.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ClassStringClosingBracketMissing.php new file mode 100644 index 0000000..5eb74bc --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ClassStringClosingBracketMissing.php @@ -0,0 +1,21 @@ +toString()}>`.", + 1606484169 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMaxValue.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMaxValue.php new file mode 100644 index 0000000..74bdd39 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMaxValue.php @@ -0,0 +1,22 @@ +toString()}` for max value of integer range `int<{$min->value()}, ?>`, it must be either `max` or an integer value.", + 1638788172 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMinValue.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMinValue.php new file mode 100644 index 0000000..e135ba4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeInvalidMinValue.php @@ -0,0 +1,21 @@ +toString()}` for min value of integer range, it must be either `min` or an integer value.", + 1638787807 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingClosingBracket.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingClosingBracket.php new file mode 100644 index 0000000..4efabc1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingClosingBracket.php @@ -0,0 +1,21 @@ +value()}, {$max->value()}>`.", + 1638788306 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingComma.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingComma.php new file mode 100644 index 0000000..f4931a2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingComma.php @@ -0,0 +1,21 @@ +value()}, ?>`.", + 1638787915 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMaxValue.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMaxValue.php new file mode 100644 index 0000000..9ed5f2a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMaxValue.php @@ -0,0 +1,21 @@ +value()}, max>`.", + 1638788092 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMinValue.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMinValue.php new file mode 100644 index 0000000..bc3aaea --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/IntegerRangeMissingMinValue.php @@ -0,0 +1,20 @@ +`.', + 1638787061 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/InvalidClassStringSubType.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/InvalidClassStringSubType.php new file mode 100644 index 0000000..7f13d50 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/InvalidClassStringSubType.php @@ -0,0 +1,21 @@ +toString()}`, it must be a class name or an interface name.", + 1608034138 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ReversedValuesForIntegerRange.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ReversedValuesForIntegerRange.php new file mode 100644 index 0000000..60ed9b6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/ReversedValuesForIntegerRange.php @@ -0,0 +1,20 @@ +`.", + 1638787061 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/SameValueForIntegerRange.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/SameValueForIntegerRange.php new file mode 100644 index 0000000..0225c87 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/Scalar/SameValueForIntegerRange.php @@ -0,0 +1,20 @@ +getMessage()}", + 1630092678, + $exception + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/UnknownSymbol.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/UnknownSymbol.php new file mode 100644 index 0000000..fadd10a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Exception/UnknownSymbol.php @@ -0,0 +1,16 @@ +buildTypeParser( + new ClassContextSpecification($className), + ); + } + + public function buildAdvancedTypeParserForClass(ObjectType $type, array $aliases = []): TypeParser + { + $specifications = [ + new ClassContextSpecification($type->className()), + new AliasSpecification(Reflection::class($type->className())), + ]; + + if ($aliases === [] && $type instanceof GenericType) { + $aliases = $type->generics(); + } + + if ($aliases !== []) { + $specifications[] = new TypeAliasAssignerSpecification($aliases); + } + + $parser = $this->buildTypeParser(...$specifications); + + return new GenericCheckerParser($parser, $this); + } + + public function buildNativeTypeParserForFunction(ReflectionFunction $reflection): TypeParser + { + $class = $reflection->getClosureScopeClass(); + + if ($class) { + return $this->buildNativeTypeParserForClass($class->name); + } + + return $this->buildDefaultTypeParser(); + } + + public function buildAdvancedTypeParserForFunction(ReflectionFunction $reflection): TypeParser + { + $class = $reflection->getClosureScopeClass(); + + $specifications = [ + new AliasSpecification($reflection), + ]; + + if ($class === null) { + return $this->buildTypeParser(...$specifications); + } + + $specifications[] = new ClassContextSpecification($class->name); + + $parser = $this->buildTypeParser(...$specifications); + + return new GenericCheckerParser($parser, $this); + } + + public function buildDefaultTypeParser(): TypeParser + { + return $this->nativeParser ??= new CachedParser($this->buildTypeParser()); + } + + private function buildTypeParser(TypeParserSpecification ...$specifications): TypeParser + { + $lexer = new SpecificationsLexer($specifications); + $lexer = new NativeLexer($lexer); + + return new LexingParser($lexer); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/AliasSpecification.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/AliasSpecification.php new file mode 100644 index 0000000..b91219e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/AliasSpecification.php @@ -0,0 +1,108 @@ +|ReflectionFunction */ + private Reflector $reflection, + ) {} + + public function manipulateToken(TraversingToken $token): TraversingToken + { + $symbol = $token->symbol(); + + // Matches the case where a class extends a class with the same name but + // in a different namespace. + if ($symbol === $this->reflection->getShortName() && Reflection::classOrInterfaceExists($symbol)) { + return $token; + } + + $alias = $this->resolveAlias($symbol); + + if (strtolower($alias) !== strtolower($symbol)) { + /** @var class-string $alias */ + return new ObjectToken($alias); + } + + $namespaced = $this->resolveNamespaced($symbol); + + if ($namespaced !== $symbol) { + /** @var class-string $namespaced */ + return new ObjectToken($namespaced); + } + + return $token; + } + + private function resolveAlias(string $symbol): string + { + $aliases = PhpParser::parseUseStatements($this->reflection); + + if (in_array($symbol, $aliases, true)) { + return $symbol; + } + + $namespaceParts = explode('\\', $symbol); + + $alias = strtolower(array_shift($namespaceParts)); + + if (! isset($aliases[$alias])) { + return $symbol; + } + + if ($namespaceParts === []) { + return $aliases[$alias]; + } + + return $aliases[$alias] . '\\' . implode('\\', $namespaceParts); + } + + private function resolveNamespaced(string $symbol): string + { + $reflection = $this->reflection; + + if ($reflection instanceof ReflectionFunction) { + $classReflection = $reflection->getClosureScopeClass(); + + if ($classReflection && $classReflection->getFileName() === $reflection->getFileName()) { + $reflection = $classReflection; + } + } + + if ($reflection->inNamespace()) { + $namespace = $reflection->getNamespaceName(); + } elseif ($reflection instanceof ReflectionFunction) { + $namespace = PhpParser::parseNamespace($reflection); + } + + if (! isset($namespace)) { + return $symbol; + } + + $full = "$namespace\\$symbol"; + + if (Reflection::classOrInterfaceExists($full)) { + return $full; + } + + return $symbol; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/ClassContextSpecification.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/ClassContextSpecification.php new file mode 100644 index 0000000..1534a7b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/ClassContextSpecification.php @@ -0,0 +1,26 @@ +symbol() === 'self' || $token->symbol() === 'static') { + return new ObjectToken($this->className); + } + + return $token; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php new file mode 100644 index 0000000..096f2e4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php @@ -0,0 +1,29 @@ + */ + private array $aliases, + ) {} + + public function manipulateToken(TraversingToken $token): TraversingToken + { + $symbol = $token->symbol(); + + if (isset($this->aliases[$symbol])) { + return TypeToken::withSymbol($this->aliases[$symbol], $symbol); + } + + return $token; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeParserSpecification.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeParserSpecification.php new file mode 100644 index 0000000..6aa5725 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Factory/Specifications/TypeParserSpecification.php @@ -0,0 +1,13 @@ + $aliases + */ + public function buildAdvancedTypeParserForClass(ObjectType $type, array $aliases = []): TypeParser; + + public function buildNativeTypeParserForFunction(ReflectionFunction $reflection): TypeParser; + + public function buildAdvancedTypeParserForFunction(ReflectionFunction $reflection): TypeParser; +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/GenericCheckerParser.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/GenericCheckerParser.php new file mode 100644 index 0000000..30d4e47 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/GenericCheckerParser.php @@ -0,0 +1,94 @@ +delegate->parse($raw); + + $this->checkGenerics($type); + + return $type; + } + + private function checkGenerics(Type $type): void + { + if ($type instanceof CompositeTraversableType) { + foreach ($type->traverse() as $subType) { + $this->checkGenerics($subType); + } + } + + if (! $type instanceof GenericType) { + return; + } + + $templates = (new ClassTemplatesResolver())->resolveTemplatesFrom($type->className()); + + if ($templates === []) { + return; + } + + $generics = $type->generics(); + + $parser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type); + + foreach ($templates as $templateName => $template) { + if (! isset($generics[$templateName])) { + throw new AssignedGenericNotFound($type->className(), ...array_keys($templates)); + } + + array_shift($templates); + + if ($template === null) { + // If no template is provided, it defaults to mixed type. + continue; + } + + $genericType = $generics[$templateName]; + + try { + $templateType = $parser->parse($template); + } catch (InvalidType $invalidType) { + throw new InvalidClassTemplate($type->className(), $templateName, $invalidType); + } + + if ($templateType instanceof ArrayKeyType && $genericType instanceof StringType) { + $genericType = ArrayKeyType::string(); + } + + if ($templateType instanceof ArrayKeyType && $genericType instanceof IntegerType) { + $genericType = ArrayKeyType::integer(); + } + + if (! $genericType->matches($templateType)) { + throw new InvalidAssignedGeneric($genericType, $templateType, $templateName, $type->className()); + } + } + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Annotations.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Annotations.php new file mode 100644 index 0000000..863fb71 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Annotations.php @@ -0,0 +1,108 @@ + */ + private array $annotations = []; + + public function __construct(string $docBlock) + { + $docBlock = $this->sanitizeDocComment($docBlock); + + $tokens = (new TokensExtractor($docBlock))->all(); + + $current = []; + + while (($token = array_pop($tokens)) !== null) { + if (str_starts_with($token, '@')) { + $current = $this->trimArrayTips($current); + + if ($current !== []) { + array_unshift($this->annotations, new TokenizedAnnotation($token, $current)); + } + + $current = []; + } else { + array_unshift($current, $token); + } + } + } + + public function firstOf(string ...$annotations): ?TokenizedAnnotation + { + foreach ($annotations as $annotation) { + foreach ($this->annotations as $tokenizedAnnotation) { + if ($tokenizedAnnotation->name() === $annotation) { + return $tokenizedAnnotation; + } + } + } + + return null; + } + + /** + * @return array + */ + public function filteredInOrder(string ...$annotations): array + { + return array_filter( + $this->annotations, + static fn (TokenizedAnnotation $tokenizedAnnotation) => in_array($tokenizedAnnotation->name(), $annotations, true), + ); + } + + /** + * @return list + */ + public function filteredByPriority(string ...$annotations): array + { + $result = []; + + foreach ($annotations as $annotation) { + $filtered = array_filter( + $this->annotations, + static fn (TokenizedAnnotation $tokenizedAnnotation) => $tokenizedAnnotation->name() === $annotation, + ); + + $result = array_merge($result, $filtered); + } + + return $result; + } + + private function sanitizeDocComment(string $value): string + { + $value = preg_replace('#^\s*/\*\*([^/]+)\*/\s*$#', '$1', $value); + + return preg_replace('/^\s*\*\s*(\S*)/mU', '$1', $value); // @phpstan-ignore-line / We know the regex is correct + } + + /** + * @param list $array + * @return list + */ + private function trimArrayTips(array $array): array + { + if ($array !== [] && trim(current($array)) === '') { + array_shift($array); + } + + if ($array !== [] && trim(end($array)) === '') { + array_pop($array); + } + + return $array; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/NativeLexer.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/NativeLexer.php new file mode 100644 index 0000000..7218232 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/NativeLexer.php @@ -0,0 +1,110 @@ + UnionToken::get(), + '&' => IntersectionToken::get(), + '<' => OpeningBracketToken::get(), + '>' => ClosingBracketToken::get(), + '[' => OpeningSquareBracketToken::get(), + ']' => ClosingSquareBracketToken::get(), + '{' => OpeningCurlyBracketToken::get(), + '}' => ClosingCurlyBracketToken::get(), + '::' => DoubleColonToken::get(), + ':' => ColonToken::get(), + '?' => NullableToken::get(), + ',' => CommaToken::get(), + '...' => TripleDotsToken::get(), + + 'int', 'integer' => IntegerToken::get(), + 'array' => ArrayToken::array(), + 'non-empty-array' => ArrayToken::nonEmptyArray(), + 'list' => ListToken::list(), + 'non-empty-list' => ListToken::nonEmptyList(), + 'iterable' => IterableToken::get(), + 'class-string' => ClassStringToken::get(), + 'callable' => CallableToken::get(), + 'value-of' => ValueOfToken::get(), + + 'null' => new TypeToken(NullType::get()), + 'true' => new TypeToken(BooleanValueType::true()), + 'false' => new TypeToken(BooleanValueType::false()), + 'mixed' => new TypeToken(MixedType::get()), + 'float' => new TypeToken(NativeFloatType::get()), + 'positive-int' => new TypeToken(PositiveIntegerType::get()), + 'negative-int' => new TypeToken(NegativeIntegerType::get()), + 'non-positive-int' => new TypeToken(NonPositiveIntegerType::get()), + 'non-negative-int' => new TypeToken(NonNegativeIntegerType::get()), + 'string' => new TypeToken(NativeStringType::get()), + 'non-empty-string' => new TypeToken(NonEmptyStringType::get()), + 'numeric-string' => new TypeToken(NumericStringType::get()), + 'bool', 'boolean' => new TypeToken(NativeBooleanType::get()), + 'array-key' => new TypeToken(ArrayKeyType::default()), + 'object' => new TypeToken(UndefinedObjectType::get()), + 'scalar' => new TypeToken(ScalarConcreteType::get()), + + default => match (true) { + str_starts_with($symbol, "'") || str_starts_with($symbol, '"') => new StringValueToken($symbol), + filter_var($symbol, FILTER_VALIDATE_INT) !== false => new IntegerValueToken((int)$symbol), + is_numeric($symbol) => new FloatValueToken((float)$symbol), + default => $this->delegate->tokenize($symbol), + }, + }; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/SpecificationsLexer.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/SpecificationsLexer.php new file mode 100644 index 0000000..0551cc7 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/SpecificationsLexer.php @@ -0,0 +1,23 @@ + */ + private array $specifications, + ) {} + + public function tokenize(string $symbol): Token + { + return (new VacantToken($symbol, $this->specifications)); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ArrayToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ArrayToken.php new file mode 100644 index 0000000..c839b2b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ArrayToken.php @@ -0,0 +1,236 @@ + */ + private string $arrayType, + private string $symbol + ) {} + + public static function array(): self + { + return self::$array ??= new self(ArrayType::class, 'array'); + } + + public static function nonEmptyArray(): self + { + return self::$nonEmptyArray ??= new self(NonEmptyArrayType::class, 'non-empty-array'); + } + + public function traverse(TokenStream $stream): Type + { + if ($stream->done()) { + return ($this->arrayType)::native(); + } + + if ($stream->next() instanceof OpeningBracketToken) { + return $this->arrayType($stream); + } + + if ($this->arrayType === ArrayType::class && $stream->next() instanceof OpeningCurlyBracketToken) { + return $this->shapedArrayType($stream); + } + + return ($this->arrayType)::native(); + } + + public function symbol(): string + { + return $this->symbol; + } + + private function arrayType(TokenStream $stream): CompositeTraversableType + { + $stream->forward(); + $type = $stream->read(); + $token = $stream->forward(); + + if ($token instanceof ClosingBracketToken) { + return new ($this->arrayType)(ArrayKeyType::default(), $type); + } + + if (! $token instanceof CommaToken) { + throw new ArrayCommaMissing($this->arrayType, $type); + } + + $keyType = ArrayKeyType::from($type); + $subType = $stream->read(); + + $arrayType = new ($this->arrayType)($keyType, $subType); + + if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) { + throw new ArrayClosingBracketMissing($arrayType); + } + + return $arrayType; + } + + private function shapedArrayType(TokenStream $stream): ShapedArrayType + { + $stream->forward(); + + $elements = []; + $index = 0; + $isUnsealed = false; + $unsealedType = null; + + while (! $stream->done()) { + if ($stream->next() instanceof ClosingCurlyBracketToken) { + $stream->forward(); + break; + } + + if (! empty($elements) && ! $stream->forward() instanceof CommaToken) { + throw new ShapedArrayCommaMissing($elements); + } + + if ($stream->done()) { + throw new ShapedArrayClosingBracketMissing($elements); + } + + if ($stream->next() instanceof ClosingCurlyBracketToken) { + $stream->forward(); + break; + } + + $optional = false; + + if ($stream->next() instanceof TripleDotsToken) { + $isUnsealed = true; + $stream->forward(); + } + + if ($stream->done()) { + throw new ShapedArrayClosingBracketMissing($elements, unsealedType: false); + } + + if ($stream->next() instanceof VacantToken) { + $type = new StringValueType($stream->forward()->symbol()); + } elseif ($isUnsealed && ($stream->next() instanceof ClosingCurlyBracketToken || $stream->next() instanceof CommaToken)) { + $stream->forward(); + break; + } else { + $type = $stream->read(); + } + + if ($isUnsealed) { + $unsealedType = $type; + + if ($elements === []) { + throw new ShapedArrayWithoutElementsWithSealedType($unsealedType); + } + + if (! $unsealedType instanceof ArrayType) { + throw new ShapedArrayInvalidUnsealedType($elements, $unsealedType); + } + + if ($stream->done()) { + throw new ShapedArrayClosingBracketMissing($elements, $unsealedType); + } elseif (! $stream->next() instanceof ClosingCurlyBracketToken) { + $unexpected = []; + + while (! $stream->done() && ! $stream->next() instanceof ClosingCurlyBracketToken) { + $unexpected[] = $stream->forward(); + } + + throw new ShapedArrayUnexpectedTokenAfterSealedType($elements, $unsealedType, $unexpected); + } + + continue; + } + + if ($stream->done()) { + $elements[] = new ShapedArrayElement(new IntegerValueType($index), $type); + + throw new ShapedArrayClosingBracketMissing($elements); + } + + if ($stream->next() instanceof NullableToken) { + $stream->forward(); + $optional = true; + + if ($stream->done()) { + throw new ShapedArrayColonTokenMissing($elements, $type); + } + } + + if ($stream->next() instanceof ColonToken) { + $stream->forward(); + + $key = $type; + $type = null; + + if (! $key instanceof StringValueType && ! $key instanceof IntegerValueType) { + $key = new StringValueType($key->toString()); + } + + if ($key instanceof IntegerValueType) { + $index++; + } + } else { + if ($optional) { + throw new ShapedArrayColonTokenMissing($elements, $type); + } + + $key = new IntegerValueType($index++); + } + + if (! $type) { + if ($stream->done()) { + throw new ShapedArrayElementTypeMissing($elements, $key, $optional); + } + + $type = $stream->read(); + } + + $elements[] = new ShapedArrayElement($key, $type, $optional); + + if ($stream->done()) { + throw new ShapedArrayClosingBracketMissing($elements); + } + } + + if ($elements === []) { + throw new ShapedArrayEmptyElements(); + } + + if ($unsealedType) { + return ShapedArrayType::unsealed($unsealedType, ...$elements); + } elseif ($isUnsealed) { + return ShapedArrayType::unsealedWithoutType(...$elements); + } + + return new ShapedArrayType(...$elements); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/CallableToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/CallableToken.php new file mode 100644 index 0000000..8268165 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/CallableToken.php @@ -0,0 +1,26 @@ + */ + private array $cases + ) {} + + /** + * @param list $tokens + * @return array + */ + public function matching(array $tokens): array + { + $matches = []; + + foreach ($this->cases as $name => $value) { + if ($this->matches($name, $tokens)) { + $matches[$name] = $value; + } + } + + return $matches; + } + + /** + * @param list $tokens + */ + private function matches(string $name, array $tokens): bool + { + $offset = 0; + + while (($token = current($tokens)) !== false) { + $next = next($tokens); + + if ($token === '') { + if ($next === false) { + return true; + } + + continue; + } + + $position = strpos($name, $token, $offset); + + if ($position === false) { + return false; + } + + $offset = $position + strlen($token); + } + + return $offset === strlen($name); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassNameToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassNameToken.php new file mode 100644 index 0000000..a4b609b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassNameToken.php @@ -0,0 +1,167 @@ + */ + private ReflectionClass $reflection; + + /** + * @param class-string $className + */ + public function __construct(string $className) + { + $this->reflection = Reflection::class($className); + } + + public function traverse(TokenStream $stream): Type + { + $constant = $this->classConstant($stream); + + if ($constant) { + return $constant; + } + + $templates = (new ClassTemplatesResolver())->resolveTemplateNamesFrom($this->reflection->name); + + $generics = $this->generics($stream, $this->reflection->name, $templates); + $generics = $this->assignGenerics($this->reflection->name, $templates, $generics); + + if ($this->reflection->isInterface()) { + return new InterfaceType($this->reflection->name, $generics); + } + + return new NativeClassType($this->reflection->name, $generics); + } + + public function symbol(): string + { + return $this->reflection->name; + } + + private function classConstant(TokenStream $stream): ?Type + { + if ($stream->done() || ! $stream->next() instanceof DoubleColonToken) { + return null; + } + + $stream->forward(); + + if ($stream->done()) { + throw new MissingClassConstantCase($this->reflection->name); + } + + $symbol = $stream->forward()->symbol(); + + $cases = []; + + if (! preg_match('/\*\s*\*/', $symbol)) { + $finder = new CaseFinder($this->reflection->getConstants(ReflectionClassConstant::IS_PUBLIC)); + $cases = $finder->matching(explode('*', $symbol)); + } + + if (empty($cases)) { + throw new ClassConstantCaseNotFound($this->reflection->name, $symbol); + } + + $cases = array_map(static fn ($value) => ValueTypeFactory::from($value), $cases); + + if (count($cases) > 1) { + return new UnionType(...array_values($cases)); + } + + return reset($cases); + } + + /** + * @param array $templates + * @param class-string $className + * @return list + */ + private function generics(TokenStream $stream, string $className, array $templates): array + { + if ($stream->done() || ! $stream->next() instanceof OpeningBracketToken) { + return []; + } + + $generics = []; + + $stream->forward(); + + while (true) { + if ($stream->done()) { + throw new MissingGenerics($className, $generics, $templates); + } + + $generics[] = $stream->read(); + + if ($stream->done()) { + throw new GenericClosingBracketMissing($className, $generics); + } + + $next = $stream->forward(); + + if ($next instanceof ClosingBracketToken) { + break; + } + + if (! $next instanceof CommaToken) { + throw new GenericCommaMissing($className, $generics); + } + } + + return $generics; + } + + /** + * @param class-string $className + * @param array $templates + * @param list $generics + * @return array + */ + private function assignGenerics(string $className, array $templates, array $generics): array + { + $assignedGenerics = []; + + foreach ($templates as $template) { + $generic = array_shift($generics); + + if ($generic) { + $assignedGenerics[$template] = $generic; + } + } + + if (! empty($generics)) { + throw new CannotAssignGeneric($className, ...$generics); + } + + return $assignedGenerics; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassStringToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassStringToken.php new file mode 100644 index 0000000..dfab2c4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClassStringToken.php @@ -0,0 +1,46 @@ +done() || ! $stream->next() instanceof OpeningBracketToken) { + return new ClassStringType(); + } + + $stream->forward(); + + $type = $stream->read(); + + if (! $type instanceof ObjectType && ! $type instanceof UnionType) { + throw new InvalidClassStringSubType($type); + } + + if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) { + throw new ClassStringClosingBracketMissing($type); + } + + return new ClassStringType($type); + } + + public function symbol(): string + { + return 'class-string'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingBracketToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingBracketToken.php new file mode 100644 index 0000000..5309191 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingBracketToken.php @@ -0,0 +1,18 @@ +'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingCurlyBracketToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingCurlyBracketToken.php new file mode 100644 index 0000000..d1ed0b4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ClosingCurlyBracketToken.php @@ -0,0 +1,18 @@ + */ + private string $enumName + ) {} + + public function traverse(TokenStream $stream): Type + { + return $this->findPatternEnumType($stream) + ?? EnumType::native($this->enumName); + } + + private function findPatternEnumType(TokenStream $stream): ?Type + { + if ($stream->done() || ! $stream->next() instanceof DoubleColonToken) { + return null; + } + + $stream->forward(); + + if ($stream->done()) { + throw new MissingEnumCase($this->enumName); + } + + $symbol = $stream->forward()->symbol(); + + if ($symbol === '*') { + throw new MissingSpecificEnumCase($this->enumName); + } + + return EnumType::fromPattern($this->enumName, $symbol); + } + + public function symbol(): string + { + return $this->enumName; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/FloatValueToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/FloatValueToken.php new file mode 100644 index 0000000..de72300 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/FloatValueToken.php @@ -0,0 +1,25 @@ +value); + } + + public function symbol(): string + { + return (string)$this->value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerToken.php new file mode 100644 index 0000000..8139045 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerToken.php @@ -0,0 +1,78 @@ +done() || ! $stream->next() instanceof OpeningBracketToken) { + return NativeIntegerType::get(); + } + + $stream->forward(); + + if ($stream->done()) { + throw new IntegerRangeMissingMinValue(); + } + + if ($stream->next()->symbol() === 'min') { + $min = new IntegerValueType(PHP_INT_MIN); + $stream->forward(); + } else { + $min = $stream->read(); + } + + if (! $min instanceof IntegerValueType) { + throw new IntegerRangeInvalidMinValue($min); + } + + if ($stream->done() || ! $stream->forward() instanceof CommaToken) { + throw new IntegerRangeMissingComma($min); + } + + if ($stream->done()) { + throw new IntegerRangeMissingMaxValue($min); + } + + if ($stream->next()->symbol() === 'max') { + $max = new IntegerValueType(PHP_INT_MAX); + $stream->forward(); + } else { + $max = $stream->read(); + } + + if (! $max instanceof IntegerValueType) { + throw new IntegerRangeInvalidMaxValue($min, $max); + } + + if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) { + throw new IntegerRangeMissingClosingBracket($min, $max); + } + + return new IntegerRangeType($min->value(), $max->value()); + } + + public function symbol(): string + { + return 'int'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerValueToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerValueToken.php new file mode 100644 index 0000000..2452e38 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntegerValueToken.php @@ -0,0 +1,25 @@ +value); + } + + public function symbol(): string + { + return (string)$this->value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntersectionToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntersectionToken.php new file mode 100644 index 0000000..dfd8fad --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IntersectionToken.php @@ -0,0 +1,47 @@ +done()) { + throw new RightIntersectionTypeMissing($type); + } + + $rightType = $stream->read(); + + if ($rightType instanceof IntersectionType) { + return new IntersectionType($type, ...$rightType->types()); + } + + if (! $rightType instanceof ObjectType) { + throw new InvalidIntersectionType($rightType); + } + + return new IntersectionType($type, $rightType); + } + + public function symbol(): string + { + return '&'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IterableToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IterableToken.php new file mode 100644 index 0000000..5245287 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/IterableToken.php @@ -0,0 +1,64 @@ +done() || ! $stream->next() instanceof OpeningBracketToken) { + return IterableType::native(); + } + + $stream->forward(); + $type = $stream->read(); + $token = $stream->forward(); + + if ($token instanceof ClosingBracketToken) { + return new IterableType(ArrayKeyType::default(), $type); + } + + if (! $token instanceof CommaToken) { + throw new IterableCommaMissing($type); + } + + $subType = $stream->read(); + + if ($type instanceof ArrayKeyType) { + $iterableType = new IterableType($type, $subType); + } elseif ($type instanceof IntegerType) { + $iterableType = new IterableType(ArrayKeyType::integer(), $subType); + } elseif ($type instanceof StringType) { + $iterableType = new IterableType(ArrayKeyType::string(), $subType); + } else { + throw new InvalidIterableKey($type, $subType); + } + + if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) { + throw new IterableClosingBracketMissing($type, $subType); + } + + return $iterableType; + } + + public function symbol(): string + { + return 'iterable'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/LeftTraversingToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/LeftTraversingToken.php new file mode 100644 index 0000000..77748db --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/LeftTraversingToken.php @@ -0,0 +1,14 @@ + */ + private string $listType, + private string $symbol + ) {} + + public static function list(): self + { + return self::$list ??= new self(ListType::class, 'list'); + } + + public static function nonEmptyList(): self + { + return self::$nonEmptyList ??= new self(NonEmptyListType::class, 'non-empty-list'); + } + + public function traverse(TokenStream $stream): Type + { + if (! $stream->done() && $stream->next() instanceof OpeningBracketToken) { + $stream->forward(); + + $subType = $stream->read(); + + $listType = new ($this->listType)($subType); + + if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) { + throw new ListClosingBracketMissing($listType); + } + + return $listType; + } + + return ($this->listType)::native(); + } + + public function symbol(): string + { + return $this->symbol; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/NullableToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/NullableToken.php new file mode 100644 index 0000000..31867d0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/NullableToken.php @@ -0,0 +1,27 @@ +read()); + } + + public function symbol(): string + { + return '?'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ObjectToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ObjectToken.php new file mode 100644 index 0000000..8971a57 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ObjectToken.php @@ -0,0 +1,30 @@ +className) + ? (new EnumNameToken($this->className))->traverse($stream) + : (new ClassNameToken($this->className))->traverse($stream); + } + + public function symbol(): string + { + return $this->className; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/OpeningBracketToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/OpeningBracketToken.php new file mode 100644 index 0000000..805dca9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/OpeningBracketToken.php @@ -0,0 +1,18 @@ +done() || ! $stream->forward() instanceof ClosingSquareBracketToken) { + throw new SimpleArrayClosingBracketMissing($type); + } + + return ArrayType::simple($type); + } + + public function symbol(): string + { + return '['; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/StringValueToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/StringValueToken.php new file mode 100644 index 0000000..01bd5dc --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/StringValueToken.php @@ -0,0 +1,32 @@ +value[0]; + + if ($this->value[-1] !== $quoteType) { + throw new MissingClosingQuoteChar($this->value); + } + + return StringValueType::from($this->value); + } + + public function symbol(): string + { + return $this->value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/Token.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/Token.php new file mode 100644 index 0000000..0a4a58e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/Token.php @@ -0,0 +1,11 @@ +type = $type; + $this->symbol = $type->toString(); + } + + public static function withSymbol(Type $type, string $symbol): self + { + $self = new self($type); + $self->symbol = $symbol; + + return $self; + } + + public function traverse(TokenStream $stream): Type + { + return $this->type; + } + + public function symbol(): string + { + return $this->symbol; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/UnionToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/UnionToken.php new file mode 100644 index 0000000..807a7f9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/UnionToken.php @@ -0,0 +1,31 @@ +done()) { + throw new RightUnionTypeMissing($type); + } + + return new UnionType($type, $stream->read()); + } + + public function symbol(): string + { + return '|'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/VacantToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/VacantToken.php new file mode 100644 index 0000000..999876c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/VacantToken.php @@ -0,0 +1,49 @@ + */ + private array $specifications, + ) {} + + public function traverse(TokenStream $stream): Type + { + $token = $this; + + foreach ($this->specifications as $specification) { + $new = $specification->manipulateToken($token); + + if ($new !== $token) { + return $new->traverse($stream); + } + } + + if (Reflection::enumExists($this->symbol)) { + return (new EnumNameToken($this->symbol))->traverse($stream); + } + + if (Reflection::classOrInterfaceExists($this->symbol)) { + return (new ClassNameToken($this->symbol))->traverse($stream); + } + + throw new UnknownSymbol($this->symbol); + } + + public function symbol(): string + { + return $this->symbol; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ValueOfToken.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ValueOfToken.php new file mode 100644 index 0000000..10b6430 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/Token/ValueOfToken.php @@ -0,0 +1,65 @@ +done() || !$stream->forward() instanceof OpeningBracketToken) { + throw new ValueOfOpeningBracketMissing(); + } + + $subType = $stream->read(); + + if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) { + throw new ValueOfClosingBracketMissing($subType); + } + + if (! $subType instanceof EnumType) { + throw new ValueOfIncorrectSubType($subType); + } + + if (! is_a($subType->className(), BackedEnum::class, true)) { + throw new ValueOfIncorrectSubType($subType); + } + + $cases = array_map( + // @phpstan-ignore-next-line / We know it's a BackedEnum + fn (BackedEnum $case) => ValueTypeFactory::from($case->value), + array_values($subType->cases()), + ); + + if (count($cases) > 1) { + return new UnionType(...$cases); + } + + return $cases[0]; + } + + public function symbol(): string + { + return 'value-of'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenStream.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenStream.php new file mode 100644 index 0000000..fbdfab0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenStream.php @@ -0,0 +1,69 @@ +tokens = $tokens; + } + + /** @phpstan-impure */ + public function read(): Type + { + assert(! $this->done()); + + $token = $this->forward(); + + assert($token instanceof TraversingToken); + + $type = $token->traverse($this); + + while (! $this->done()) { + $token = $this->next(); + + if (! $token instanceof LeftTraversingToken) { + break; + } + + $this->forward(); + + $type = $token->traverse($type, $this); + } + + return $type; + } + + public function next(): Token + { + return $this->tokens[$this->peek + 1]; + } + + /** @phpstan-impure */ + public function forward(): Token + { + return $this->tokens[++$this->peek]; + } + + /** @phpstan-impure */ + public function done(): bool + { + return $this->peek === count($this->tokens) - 1; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenizedAnnotation.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenizedAnnotation.php new file mode 100644 index 0000000..dbb1a3e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokenizedAnnotation.php @@ -0,0 +1,62 @@ + */ + private array $tokens, + ) {} + + /** + * @return non-empty-string + */ + public function name(): string + { + return $this->name; + } + + public function splice(int $length): string + { + return implode('', array_splice($this->tokens, 0, $length)); + } + + /** + * @return non-empty-string + */ + public function allAfter(int $offset): string + { + /** @var non-empty-string */ + return implode('', array_splice($this->tokens, $offset)); + } + + /** + * @return non-empty-array + */ + public function filtered(): array + { + /** @var non-empty-array / We can force the type as we know for sure it contains at least one non-empty-string */ + return array_filter( + array_map(trim(...), $this->tokens), + static fn ($value) => $value !== '', + ); + } + + /** + * @return non-empty-string + */ + public function raw(): string + { + /** @var non-empty-string / We can force the type as we know for sure it contains at least one non-empty-string */ + return implode('', $this->tokens); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokensExtractor.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokensExtractor.php new file mode 100644 index 0000000..ddeb1ce --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TokensExtractor.php @@ -0,0 +1,65 @@ + '[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++', + 'Simple quoted string' => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n])*+\'?', + 'Double quoted string' => '"(?:\\\\[^\\r\\n]|[^"\\r\\n])*+"?', + 'Double colons' => '\:\:', + 'Triple dots' => '\.\.\.', + 'Dollar sign' => '\$', + 'Whitespace' => '\s+', + 'Union' => '\|', + 'Intersection' => '&', + 'Opening bracket' => '\<', + 'Closing bracket' => '\>', + 'Opening square bracket' => '\[', + 'Closing square bracket' => '\]', + 'Opening curly bracket' => '\{', + 'Closing curly bracket' => '\}', + 'Colon' => '\:', + 'Question mark' => '\?', + 'Comma' => ',', + 'Single quote' => "'", + 'Double quote' => '"', + ]; + + /** @var non-empty-list */ + private array $symbols; + + public function __construct(string $raw) + { + $pattern = '/(' . implode('|', self::TOKEN_PATTERNS) . ')' . '/'; + + // @phpstan-ignore-next-line / We know the pattern is valid and the returned array contains at least one string + $this->symbols = preg_split($pattern, $raw, flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + } + + /** + * @return non-empty-list + */ + public function all(): array + { + return $this->symbols; + } + + /** + * @return array + */ + public function filtered(): array + { + return array_filter( + array_map(trim(...), $this->symbols), + static fn ($value) => $value !== '', + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TypeLexer.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TypeLexer.php new file mode 100644 index 0000000..22fc1d1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/Lexer/TypeLexer.php @@ -0,0 +1,13 @@ + $this->lexer->tokenize($symbol), + (new TokensExtractor($raw))->filtered() + ); + + return (new TokenStream(...$tokens))->read(); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Parser/TypeParser.php b/lib/Vendor/CuyZ/Valinor/Type/Parser/TypeParser.php new file mode 100644 index 0000000..444ecef --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Parser/TypeParser.php @@ -0,0 +1,17 @@ + */ + private array $types; + + private string $signature; + + private function __construct(Type $type) + { + $types = $type instanceof UnionType + ? [...$type->types()] + : [$type]; + + foreach ($types as $subType) { + if (! $subType instanceof IntegerType && ! $subType instanceof StringType) { + throw new InvalidArrayKey($subType); + } + } + + /** @var non-empty-list $types */ + $this->types = $types; + $this->signature = $type->toString(); + } + + public static function default(): self + { + if (!isset(self::$default)) { + self::$default = new self(new UnionType(NativeIntegerType::get(), NativeStringType::get())); + self::$default->signature = 'array-key'; + } + + return self::$default; + } + + public static function integer(): self + { + return self::$integer ??= new self(NativeIntegerType::get()); + } + + public static function string(): self + { + return self::$string ??= new self(NativeStringType::get()); + } + + public static function from(Type $type): self + { + return match (true) { + $type instanceof self => $type, + $type instanceof NativeIntegerType => self::integer(), + $type instanceof NativeStringType => self::string(), + default => new self($type), + }; + } + + public function accepts(mixed $value): bool + { + foreach ($this->types as $type) { + // If an array key can be evaluated as an integer, it will always be + // cast to an integer, even if the actual key is a string. + if (is_int($value) && $type instanceof NativeStringType) { + return true; + } elseif ($type->accepts($value)) { + return true; + } + } + + return false; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $conditions = []; + + foreach ($this->types as $type) { + $condition = $type->compiledAccept($node); + + if ($type instanceof NativeStringType) { + $condition = $condition->or(Node::functionCall('is_int', [$node])); + } + + $conditions[] = $condition; + } + + return Node::logicalOr(...$conditions); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof ScalarConcreteType) { + return true; + } + + if ($other instanceof UnionType) { + return $this->isMatchedBy($other); + } + + if (! $other instanceof self) { + return false; + } + + foreach ($this->types as $type) { + foreach ($other->types as $otherType) { + if ($type->matches($otherType)) { + continue 2; + } + } + + return false; + } + + return true; + } + + public function isMatchedBy(Type $other): bool + { + foreach ($this->types as $type) { + if ($other->matches($type)) { + return true; + } + } + + return false; + } + + public function canCast(mixed $value): bool + { + foreach ($this->types as $type) { + if ($type->canCast($value)) { + return true; + } + } + + return false; + } + + public function cast(mixed $value): string|int + { + foreach ($this->types as $type) { + if ($type->canCast($value)) { + return $type->cast($value); + } + } + + throw new LogicException(); + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid array key.')->build(); + } + + public function nativeType(): Type + { + $types = []; + + foreach ($this->types as $type) { + $types[$type->nativeType()->toString()] = $type->nativeType(); + } + + if (count($types) === 1) { + return array_values($types)[0]; + } + + return new UnionType(...array_values($types)); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/ArrayType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/ArrayType.php new file mode 100644 index 0000000..90b1dfc --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/ArrayType.php @@ -0,0 +1,139 @@ +keyType = $keyType; + $this->subType = $subType; + $this->signature = $keyType === ArrayKeyType::default() + ? "array<{$subType->toString()}>" + : "array<{$keyType->toString()}, {$subType->toString()}>"; + } + + /** + * @codeCoverageIgnore + * @infection-ignore-all + */ + public static function native(): self + { + if (! isset(self::$native)) { + self::$native = new self(ArrayKeyType::default(), MixedType::get()); + self::$native->signature = 'array'; + } + + return self::$native; + } + + public static function simple(Type $type): self + { + $instance = new self(ArrayKeyType::default(), $type); + $instance->signature = $instance->subType->toString() . '[]'; + + return $instance; + } + + public function accepts(mixed $value): bool + { + if (! is_array($value)) { + return false; + } + + return Polyfill::array_all( + $value, + fn (mixed $item, mixed $key) => $this->keyType->accepts($key) && $this->subType->accepts($item), + ); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $condition = Node::functionCall('is_array', [$node]); + + if ($this === self::native()) { + return $condition; + } + + return $condition->and(Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ + $node, + Node::shortClosure( + Node::logicalAnd( + $this->keyType->compiledAccept(Node::variable('key'))->wrap(), + $this->subType->compiledAccept(Node::variable('item'))->wrap(), + ), + )->witParameters( + Node::parameterDeclaration('item', 'mixed'), + Node::parameterDeclaration('key', 'mixed'), + ), + ])); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if (! $other instanceof self && ! $other instanceof IterableType) { + return false; + } + + return $this->keyType->matches($other->keyType()) + && $this->subType->matches($other->subType()); + } + + public function keyType(): ArrayKeyType + { + return $this->keyType; + } + + public function subType(): Type + { + return $this->subType; + } + + public function traverse(): array + { + if ($this->subType instanceof CompositeType) { + return [$this->subType, ...$this->subType->traverse()]; + } + + return [$this->subType]; + } + + public function nativeType(): ArrayType + { + return self::native(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/BooleanValueType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/BooleanValueType.php new file mode 100644 index 0000000..11767ee --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/BooleanValueType.php @@ -0,0 +1,102 @@ +value; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->equals(Node::value($this->value)); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other === $this + || $other instanceof NativeBooleanType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + if ($value === $this->value) { + return true; + } + + if ($this->value === true) { + return $value === '1' || $value === 1 || $value === 'true'; + } + + return $value === '0' || $value === 0 || $value === 'false'; + } + + public function cast(mixed $value): bool + { + assert($this->canCast($value)); + + return $this->value; + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} does not match boolean value {expected_value}.') + ->withParameter('expected_value', $this->toString()) + ->build(); + } + + public function value(): bool + { + return $this->value; + } + + public function nativeType(): NativeBooleanType + { + return NativeBooleanType::get(); + } + + public function toString(): string + { + return $this->value ? 'true' : 'false'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/CallableType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/CallableType.php new file mode 100644 index 0000000..13136f1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/CallableType.php @@ -0,0 +1,48 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof MixedType; + } + + public function nativeType(): Type + { + return $this; + } + + public function toString(): string + { + return 'callable'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/ClassStringType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/ClassStringType.php new file mode 100644 index 0000000..45db583 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/ClassStringType.php @@ -0,0 +1,184 @@ +types() as $type) { + if (! $type instanceof ObjectType) { + throw new InvalidUnionOfClassString($subType); + } + } + } + + $this->subType = $subType; + $this->signature = $this->subType + ? "class-string<{$this->subType->toString()}>" + : 'class-string'; + } + + public function accepts(mixed $value): bool + { + if (! is_string($value)) { + return false; + } + + if (! $this->subType) { + return Reflection::classOrInterfaceExists($value); + } + + if ($this->subType instanceof ObjectType) { + return is_a($value, $this->subType->className(), true); + } + + foreach ($this->subType->types() as $type) { + /** @var ObjectType $type */ + if (is_a($value, $type->className(), true)) { + return true; + } + } + + return false; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $condition = Node::functionCall('is_string', [$node]); + + if (! $this->subType) { + return $condition->and(Node::functionCall(Reflection::class . '::classOrInterfaceExists', [$node])); + } + + if ($this->subType instanceof ObjectType) { + return $condition->and( + Node::functionCall('is_a', [ + $node, + Node::value($this->subType->className()), + Node::value(true), + ]) + ); + } + + $conditions = array_map( + // @phpstan-ignore argument.type (We know it's an ObjectType) + static fn (ObjectType $type) => Node::functionCall('is_a', [ + $node, + Node::value($type->className()), + Node::value(true), + ]), + $this->subType->types() + ); + + return $condition->and(Node::logicalOr(...$conditions)->wrap()); + } + + public function matches(Type $other): bool + { + if ($other instanceof NativeStringType + || $other instanceof NonEmptyStringType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType + ) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if (! $other instanceof self) { + return false; + } + + if (! $this->subType) { + return true; + } + + if (! $other->subType) { + return true; + } + + return $this->subType->matches($other->subType); + } + + public function canCast(mixed $value): bool + { + return (is_string($value) || $value instanceof Stringable) + && $this->accepts((string)$value); + } + + public function cast(mixed $value): string + { + assert($this->canCast($value)); + + return (string)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + if ($this->subType) { + return MessageBuilder::newError('Value {source_value} is not a valid class string of `{expected_class_type}`.') + ->withParameter('expected_class_type', $this->subType->toString()) + ->build(); + } + + return MessageBuilder::newError('Value {source_value} is not a valid class string.')->build(); + } + + public function subType(): ObjectType|UnionType|null + { + return $this->subType; + } + + public function traverse(): array + { + if (! $this->subType) { + return []; + } + + if ($this->subType instanceof CompositeType) { + return [$this->subType, ...$this->subType->traverse()]; + } + + return [$this->subType]; + } + + public function nativeType(): NativeStringType + { + return NativeStringType::get(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/EnumType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/EnumType.php new file mode 100644 index 0000000..a6ceed7 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/EnumType.php @@ -0,0 +1,148 @@ + */ + private string $enumName; + + private string $pattern; + + /** @var non-empty-array */ + private array $cases; + + /** + * @param class-string $enumName + * @param array $cases + */ + public function __construct(string $enumName, string $pattern, array $cases) + { + // @phpstan-ignore assign.propertyType (it is still an enum class-string) + $this->enumName = ltrim($enumName, '\\'); + $this->pattern = $pattern; + + if (empty($cases)) { + throw new EnumCaseNotFound($this->enumName, $pattern); + } + + foreach ($cases as $case) { + $this->cases[$case instanceof BackedEnum ? $case->value : $case->name] = $case; + } + } + + /** + * @param class-string $enumName + */ + public static function native(string $enumName): self + { + return new self($enumName, '', $enumName::cases()); + } + + /** + * @param class-string $enumName + */ + public static function fromPattern(string $enumName, string $pattern): self + { + $namedCases = []; + + foreach ($enumName::cases() as $case) { + /** @var UnitEnum $case */ + $namedCases[$case->name] = $case; + } + + $cases = (new CaseFinder($namedCases))->matching(explode('*', $pattern)); + + return new self($enumName, $pattern, $cases); + } + + /** + * @return class-string + */ + public function className(): string + { + return $this->enumName; + } + + /** + * @return array + */ + public function cases(): array + { + return $this->cases; + } + + public function pattern(): string + { + return $this->pattern; + } + + public function accepts(mixed $value): bool + { + return in_array($value, $this->cases, true); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->instanceOf($this->enumName); + } + + public function matches(Type $other): bool + { + if ($other instanceof CombiningType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof self) { + if ($other->enumName !== $this->enumName) { + return false; + } + + foreach ($this->cases as $case) { + if (! in_array($case, $other->cases, true)) { + return false; + } + } + + return true; + } + + return $other instanceof UndefinedObjectType + || $other instanceof MixedType; + } + + /** + * @return non-empty-string + */ + public function readableSignature(): string + { + return implode('|', array_keys($this->cases)); + } + + public function nativeType(): EnumType + { + return self::native($this->enumName); + } + + public function toString(): string + { + return $this->pattern === '' + ? $this->enumName + : "$this->enumName::$this->pattern"; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/Exception/ForbiddenMixedType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/Exception/ForbiddenMixedType.php new file mode 100644 index 0000000..5dadcce --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/Exception/ForbiddenMixedType.php @@ -0,0 +1,20 @@ +toString()}` contains invalid class string element(s).", + 1648830951 + ); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/Factory/CannotBuildTypeFromValue.php b/lib/Vendor/CuyZ/Valinor/Type/Types/Factory/CannotBuildTypeFromValue.php new file mode 100644 index 0000000..49c842e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/Factory/CannotBuildTypeFromValue.php @@ -0,0 +1,23 @@ +name); + } + + if (is_string($value)) { + if (Reflection::classOrInterfaceExists($value)) { + return new ClassStringType(new NativeClassType($value)); + } + + if (str_contains($value, "'") && str_contains($value, '"')) { + $value = "'" . str_replace("'", "\'", $value) . "'"; + } elseif (str_contains($value, "'")) { + $value = '"' . $value . '"'; + } else { + $value = "'" . $value . "'"; + } + + return StringValueType::from($value); + } + + if (is_array($value)) { + $elements = []; + + foreach ($value as $key => $child) { + $keyType = is_string($key) ? new StringValueType($key) : new IntegerValueType($key); + + $elements[] = new ShapedArrayElement($keyType, self::from($child)); + } + + return new ShapedArrayType(...$elements); + } + + throw new CannotBuildTypeFromValue($value); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/FloatValueType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/FloatValueType.php new file mode 100644 index 0000000..6d7d8a3 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/FloatValueType.php @@ -0,0 +1,80 @@ +value; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->equals(Node::value($this->value)); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof self) { + return $this->value === $other->value; + } + + return $other instanceof NativeFloatType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return is_numeric($value) && (float)$value === $this->value; + } + + public function cast(mixed $value): float + { + assert($this->canCast($value)); + + return (float)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} does not match float value {expected_value}.') + ->withParameter('expected_value', (string)$this->value) + ->build(); + } + + public function value(): float + { + return $this->value; + } + + public function nativeType(): NativeFloatType + { + return NativeFloatType::get(); + } + + public function toString(): string + { + return (string)$this->value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/IntegerRangeType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/IntegerRangeType.php new file mode 100644 index 0000000..b28f2d1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/IntegerRangeType.php @@ -0,0 +1,140 @@ +min = $min; + $this->max = $max; + $this->signature = sprintf( + 'int<%s, %s>', + $min > PHP_INT_MIN ? $min : 'min', + $max < PHP_INT_MAX ? $max : 'max' + ); + + if ($min > $max) { + throw new ReversedValuesForIntegerRange($min, $max); + } + + if ($min === $max) { + throw new SameValueForIntegerRange($min); + } + } + + public function accepts(mixed $value): bool + { + return is_int($value) + && $value >= $this->min + && $value <= $this->max; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return Node::functionCall('is_int', [$node]) + ->and($node->isGreaterOrEqualsTo(Node::value($this->min))) + ->and($node->isLessOrEqualsTo(Node::value($this->max))); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof NativeIntegerType || $other instanceof ScalarConcreteType || $other instanceof MixedType) { + return true; + } + + if ($other instanceof IntegerValueType && $this->accepts($other->value())) { + return true; + } + + if ($other instanceof NegativeIntegerType && $this->max < 0) { + return true; + } + + if ($other instanceof PositiveIntegerType && $this->min > 0) { + return true; + } + + if ($other instanceof self) { + return $other->min === $this->min && $other->max === $this->max; + } + + return false; + } + + public function canCast(mixed $value): bool + { + if (is_string($value)) { + $value = preg_match('/^0+$/', $value) + ? '0' + : ltrim($value, '0'); + } + + return ! is_bool($value) + && filter_var($value, FILTER_VALIDATE_INT) !== false + && $value >= $this->min + && $value <= $this->max; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid integer between {min} and {max}.') + ->withParameter('min', (string)$this->min) + ->withParameter('max', (string)$this->max) + ->build(); + } + + public function min(): int + { + return $this->min; + } + + public function max(): int + { + return $this->max; + } + + public function nativeType(): NativeIntegerType + { + return NativeIntegerType::get(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/IntegerValueType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/IntegerValueType.php new file mode 100644 index 0000000..1f521c2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/IntegerValueType.php @@ -0,0 +1,86 @@ +value; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->equals(Node::value($this->value)); + } + + public function matches(Type $other): bool + { + return $other->accepts($this->value); + } + + public function canCast(mixed $value): bool + { + if (is_string($value)) { + $value = preg_match('/^0+$/', $value) + ? '0' + : ltrim($value, '0'); + } + + return ! is_bool($value) + && filter_var($value, FILTER_VALIDATE_INT) !== false + && (int)$value === $this->value; // @phpstan-ignore-line; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line; + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} does not match integer value {expected_value}.') + ->withParameter('expected_value', (string)$this->value) + ->build(); + } + + public function value(): int + { + return $this->value; + } + + public function nativeType(): NativeIntegerType + { + return NativeIntegerType::get(); + } + + /** + * @return non-empty-string + */ + public function toString(): string + { + return (string)$this->value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/InterfaceType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/InterfaceType.php new file mode 100644 index 0000000..826ea57 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/InterfaceType.php @@ -0,0 +1,89 @@ + */ + private array $generics = [] + ) {} + + public function className(): string + { + return $this->interfaceName; + } + + public function generics(): array + { + return $this->generics; + } + + public function accepts(mixed $value): bool + { + return $value instanceof $this->interfaceName; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->instanceOf($this->interfaceName); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType || $other instanceof UndefinedObjectType) { + return true; + } + + if ($other instanceof CombiningType) { + return $other->isMatchedBy($this); + } + + if (! $other instanceof ObjectType) { + return false; + } + + return is_a($this->interfaceName, $other->className(), true); + } + + public function traverse(): array + { + $types = []; + + foreach ($this->generics as $type) { + $types[] = $type; + + if ($type instanceof CompositeType) { + $types = [...$types, ...$type->traverse()]; + } + } + + return $types; + } + + public function nativeType(): InterfaceType + { + return new self($this->interfaceName); + } + + public function toString(): string + { + return empty($this->generics) + ? $this->interfaceName + : $this->interfaceName . '<' . implode(', ', array_map(fn (Type $type) => $type->toString(), $this->generics)) . '>'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/IntersectionType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/IntersectionType.php new file mode 100644 index 0000000..09cd2e7 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/IntersectionType.php @@ -0,0 +1,117 @@ + */ + private array $types; + + private string $signature; + + public function __construct(ObjectType $type, ObjectType $otherType, ObjectType ...$otherTypes) + { + $this->types = [$type, $otherType, ...array_values($otherTypes)]; + $this->signature = implode('&', array_map(fn (Type $type) => $type->toString(), $this->types)); + } + + public function accepts(mixed $value): bool + { + foreach ($this->types as $type) { + if (! $type->accepts($value)) { + return false; + } + } + + return true; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return Node::logicalAnd(...array_map( + fn (ObjectType $type) => $type->compiledAccept($node), + $this->types, + )); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + foreach ($this->types as $type) { + if (! $type->matches($other)) { + return false; + } + } + + return true; + } + + public function isMatchedBy(Type $other): bool + { + foreach ($this->types as $type) { + if (! $other->matches($type)) { + return false; + } + } + + return true; + } + + public function traverse(): array + { + $types = []; + + foreach ($this->types as $type) { + $types[] = $type; + + if ($type instanceof CompositeType) { + $types = [...$types, ...$type->traverse()]; + } + } + + return $types; + } + + /** + * @return non-empty-list + */ + public function types(): array + { + return $this->types; + } + + public function nativeType(): IntersectionType + { + return new self( + ...array_map( + static fn (ObjectType $type) => $type->nativeType(), + $this->types, + ), + ); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/IterableType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/IterableType.php new file mode 100644 index 0000000..ae2d53f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/IterableType.php @@ -0,0 +1,143 @@ +keyType = $keyType; + $this->subType = $subType; + $this->signature = $keyType === ArrayKeyType::default() + ? "iterable<{$this->subType->toString()}>" + : "iterable<{$this->keyType->toString()}, {$this->subType->toString()}>"; + } + + public static function native(): self + { + if (! isset(self::$native)) { + self::$native = new self(ArrayKeyType::default(), MixedType::get()); + self::$native->signature = 'iterable'; + } + + return self::$native; + } + + public function accepts(mixed $value): bool + { + if (! is_iterable($value)) { + return false; + } + + foreach ($value as $key => $item) { + if (! $this->keyType->accepts($key)) { + return false; + } + + if (! $this->subType->accepts($item)) { + return false; + } + } + + return true; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $condition = Node::logicalAnd( + Node::functionCall('is_iterable', [$node]), + Node::negate($node->instanceOf(Generator::class)), + ); + + if ($this === self::native()) { + return $condition; + } + + // @infection-ignore-all + $iteratorToArray = PHP_VERSION_ID >= 8_02_00 + ? Node::functionCall('iterator_to_array', [$node]) + : Node::ternary( + condition: Node::functionCall('is_array', [$node]), + ifTrue: $node, + ifFalse: Node::functionCall('iterator_to_array', [$node]), + ); + + return $condition->and(Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ + $iteratorToArray, + Node::shortClosure( + Node::logicalAnd( + $this->keyType->compiledAccept(Node::variable('key'))->wrap(), + $this->subType->compiledAccept(Node::variable('item'))->wrap(), + ), + )->witParameters( + Node::parameterDeclaration('item', 'mixed'), + Node::parameterDeclaration('key', 'mixed'), + ), + ])); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof CompositeTraversableType + && $this->keyType->matches($other->keyType()) + && $this->subType->matches($other->subType()); + } + + public function keyType(): ArrayKeyType + { + return $this->keyType; + } + + public function subType(): Type + { + return $this->subType; + } + + public function traverse(): array + { + if ($this->subType instanceof CompositeType) { + return [$this->subType, ...$this->subType->traverse()]; + } + + return [$this->subType]; + } + + public function nativeType(): IterableType + { + return self::native(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/ListType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/ListType.php new file mode 100644 index 0000000..df87f27 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/ListType.php @@ -0,0 +1,131 @@ +signature = "list<{$this->subType->toString()}>"; + } + + /** + * @codeCoverageIgnore + * @infection-ignore-all + */ + public static function native(): self + { + if (! isset(self::$native)) { + self::$native = new self(MixedType::get()); + self::$native->signature = 'list'; + } + + return self::$native; + } + + public function accepts(mixed $value): bool + { + if (! is_array($value)) { + return false; + } + + if (! array_is_list($value)) { + return false; + } + + return Polyfill::array_all( + $value, + fn (mixed $item) => $this->subType->accepts($item), + ); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $condition = Node::logicalAnd( + Node::functionCall('is_array', [$node]), + Node::functionCall('array_is_list', [$node]), + ); + + if ($this === self::native()) { + return $condition; + } + + return $condition->and(Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ + $node, + Node::shortClosure( + $this->subType->compiledAccept(Node::variable('item'))->wrap(), + )->witParameters( + Node::parameterDeclaration('item', 'mixed'), + ), + ])); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof ArrayType || $other instanceof IterableType) { + return $other->keyType() !== ArrayKeyType::string() + && $this->subType->matches($other->subType()); + } + + if ($other instanceof self) { + return $this->subType->matches($other->subType); + } + + return false; + } + + public function keyType(): ArrayKeyType + { + return ArrayKeyType::integer(); + } + + public function subType(): Type + { + return $this->subType; + } + + public function traverse(): array + { + if ($this->subType instanceof CompositeType) { + return [$this->subType, ...$this->subType->traverse()]; + } + + return [$this->subType]; + } + + public function nativeType(): ArrayType + { + return ArrayType::native(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/MixedType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/MixedType.php new file mode 100644 index 0000000..b40dce9 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/MixedType.php @@ -0,0 +1,41 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return is_bool($value) + || $value === '1' + || $value === '0' + || $value === 1 + || $value === 0 + || $value === 'true' + || $value === 'false'; + } + + public function cast(mixed $value): bool + { + assert($this->canCast($value)); + + if ($value === 'false') { + return false; + } + + return (bool)$value; + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid boolean.')->build(); + } + + public function nativeType(): NativeBooleanType + { + return $this; + } + + public function toString(): string + { + return 'bool'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NativeClassType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeClassType.php new file mode 100644 index 0000000..a04d1be --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeClassType.php @@ -0,0 +1,92 @@ + */ + private array $generics = [], + ) { + $this->className = ltrim($this->className, '\\'); + } + + public function className(): string + { + return $this->className; + } + + public function generics(): array + { + return $this->generics; + } + + public function accepts(mixed $value): bool + { + return $value instanceof $this->className; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->instanceOf($this->className); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType || $other instanceof UndefinedObjectType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if (! $other instanceof ObjectType) { + return false; + } + + return is_a($this->className, $other->className(), true); + } + + public function traverse(): array + { + $types = []; + + foreach ($this->generics as $type) { + $types[] = $type; + + if ($type instanceof CompositeType) { + $types = [...$types, ...$type->traverse()]; + } + } + + return $types; + } + + public function nativeType(): NativeClassType + { + return new self($this->className); + } + + public function toString(): string + { + return empty($this->generics) + ? $this->className + : $this->className . '<' . implode(', ', array_map(fn (Type $type) => $type->toString(), $this->generics)) . '>'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NativeFloatType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeFloatType.php new file mode 100644 index 0000000..07bdd3b --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeFloatType.php @@ -0,0 +1,71 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return is_numeric($value); + } + + public function cast(mixed $value): float + { + assert($this->canCast($value)); + + return (float)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid float.')->build(); + } + + public function nativeType(): NativeFloatType + { + return $this; + } + + public function toString(): string + { + return 'float'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NativeIntegerType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeIntegerType.php new file mode 100644 index 0000000..1b553d2 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeIntegerType.php @@ -0,0 +1,78 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + if (is_string($value)) { + $value = ltrim($value, '0') . '0'; + } + + return ! is_bool($value) && filter_var($value, FILTER_VALIDATE_INT) !== false; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid integer.')->build(); + } + + public function nativeType(): NativeIntegerType + { + return $this; + } + + public function toString(): string + { + return 'int'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NativeStringType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeStringType.php new file mode 100644 index 0000000..ba02fbd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NativeStringType.php @@ -0,0 +1,74 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return is_string($value) + || is_numeric($value) + || $value instanceof Stringable; + } + + public function cast(mixed $value): string + { + assert($this->canCast($value)); + + return (string)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid string.')->build(); + } + + public function nativeType(): NativeStringType + { + return $this; + } + + public function toString(): string + { + return 'string'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NegativeIntegerType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NegativeIntegerType.php new file mode 100644 index 0000000..d5cd02f --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NegativeIntegerType.php @@ -0,0 +1,75 @@ +and($node->isLessThan(Node::value(0))); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof NativeIntegerType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return ! is_bool($value) + && filter_var($value, FILTER_VALIDATE_INT) !== false + && $value < 0; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid negative integer.')->build(); + } + + public function nativeType(): NativeIntegerType + { + return NativeIntegerType::get(); + } + + public function toString(): string + { + return 'negative-int'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyArrayType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyArrayType.php new file mode 100644 index 0000000..46c4fbd --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyArrayType.php @@ -0,0 +1,138 @@ +keyType = $keyType; + $this->subType = $subType; + $this->signature = $keyType === ArrayKeyType::default() + ? "non-empty-array<{$subType->toString()}>" + : "non-empty-array<{$keyType->toString()}, {$subType->toString()}>"; + } + + /** + * @codeCoverageIgnore + * @infection-ignore-all + */ + public static function native(): self + { + if (! isset(self::$native)) { + self::$native = new self(ArrayKeyType::default(), MixedType::get()); + self::$native->signature = 'non-empty-array'; + } + + return self::$native; + } + + public function accepts(mixed $value): bool + { + if (! is_array($value)) { + return false; + } + + if ($value === []) { + return false; + } + + return Polyfill::array_all( + $value, + fn (mixed $item, mixed $key) => $this->keyType->accepts($key) && $this->subType->accepts($item), + ); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $condition = Node::logicalAnd( + $node->different(Node::value([])), + Node::functionCall('is_array', [$node]), + ); + + if ($this === self::native()) { + return $condition; + } + + return $condition->and(Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ + $node, + Node::shortClosure( + Node::logicalAnd( + $this->keyType->compiledAccept(Node::variable('key'))->wrap(), + $this->subType->compiledAccept(Node::variable('item'))->wrap(), + ), + )->witParameters( + Node::parameterDeclaration('item', 'mixed'), + Node::parameterDeclaration('key', 'mixed'), + ), + ])); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if (! $other instanceof CompositeTraversableType) { + return false; + } + + return $this->keyType->matches($other->keyType()) + && $this->subType->matches($other->subType()); + } + + public function keyType(): ArrayKeyType + { + return $this->keyType; + } + + public function subType(): Type + { + return $this->subType; + } + + public function traverse(): array + { + if ($this->subType instanceof CompositeType) { + return [$this->subType, ...$this->subType->traverse()]; + } + + return [$this->subType]; + } + + public function nativeType(): ArrayType + { + return ArrayType::native(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyListType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyListType.php new file mode 100644 index 0000000..b1207d5 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyListType.php @@ -0,0 +1,141 @@ +subType = $subType; + $this->signature = "non-empty-list<{$this->subType->toString()}>"; + } + + /** + * @codeCoverageIgnore + * @infection-ignore-all + */ + public static function native(): self + { + if (! isset(self::$native)) { + self::$native = new self(MixedType::get()); + self::$native->signature = 'non-empty-list'; + } + + return self::$native; + } + + public function accepts(mixed $value): bool + { + if ($value === []) { + return false; + } + + if (! is_array($value)) { + return false; + } + + if (! array_is_list($value)) { + return false; + } + + return Polyfill::array_all( + $value, + fn (mixed $item) => $this->subType->accepts($item), + ); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $condition = Node::logicalAnd( + $node->different(Node::value([])), + Node::functionCall('is_array', [$node]), + Node::functionCall('array_is_list', [$node]), + ); + + if ($this === self::native()) { + return $condition; + } + + return $condition->and(Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ + $node, + Node::shortClosure( + $this->subType->compiledAccept(Node::variable('item'))->wrap() + )->witParameters( + Node::parameterDeclaration('item', 'mixed'), + ), + ])); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof ArrayType + || $other instanceof NonEmptyArrayType + || $other instanceof IterableType + ) { + return $other->keyType() !== ArrayKeyType::string() + && $this->subType->matches($other->subType()); + } + + if ($other instanceof self || $other instanceof ListType) { + return $this->subType->matches($other->subType()); + } + + return false; + } + + public function keyType(): ArrayKeyType + { + return ArrayKeyType::integer(); + } + + public function subType(): Type + { + return $this->subType; + } + + public function traverse(): array + { + if ($this->subType instanceof CompositeType) { + return [$this->subType, ...$this->subType->traverse()]; + } + + return [$this->subType]; + } + + public function nativeType(): ArrayType + { + return ArrayType::native(); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyStringType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyStringType.php new file mode 100644 index 0000000..8c33327 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NonEmptyStringType.php @@ -0,0 +1,74 @@ +and($node->different(Node::value(''))); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof NativeStringType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return (is_string($value) || is_numeric($value) || $value instanceof Stringable) + && (string)$value !== ''; + } + + public function cast(mixed $value): string + { + assert($this->canCast($value)); + + return (string)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid non-empty string.')->build(); + } + + public function nativeType(): NativeStringType + { + return NativeStringType::get(); + } + + public function toString(): string + { + return 'non-empty-string'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NonNegativeIntegerType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NonNegativeIntegerType.php new file mode 100644 index 0000000..b12bf84 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NonNegativeIntegerType.php @@ -0,0 +1,77 @@ += 0; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return Node::functionCall('is_int', [$node])->and($node->isGreaterOrEqualsTo(Node::value(0))); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof NativeIntegerType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + if (is_string($value)) { + $value = ltrim($value, '0') . '0'; + } + + return ! is_bool($value) + && filter_var($value, FILTER_VALIDATE_INT) !== false + && $value >= 0; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid non-negative integer.')->build(); + } + + public function nativeType(): NativeIntegerType + { + return NativeIntegerType::get(); + } + + public function toString(): string + { + return 'non-negative-int'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NonPositiveIntegerType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NonPositiveIntegerType.php new file mode 100644 index 0000000..e3bc53a --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NonPositiveIntegerType.php @@ -0,0 +1,70 @@ +and($node->isLessOrEqualsTo(Node::value(0))); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof NativeIntegerType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return ! is_bool($value) + && filter_var($value, FILTER_VALIDATE_INT) !== false + && $value <= 0; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid non-positive integer.')->build(); + } + + public function nativeType(): NativeIntegerType + { + return NativeIntegerType::get(); + } + + public function toString(): string + { + return 'non-positive-int'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NullType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NullType.php new file mode 100644 index 0000000..3b643ac --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NullType.php @@ -0,0 +1,46 @@ +equals(Node::value(null)); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof MixedType; + } + + public function nativeType(): Type + { + return $this; + } + + public function toString(): string + { + return 'null'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/NumericStringType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/NumericStringType.php new file mode 100644 index 0000000..6da3b36 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/NumericStringType.php @@ -0,0 +1,78 @@ +and(Node::functionCall('is_numeric', [$node])); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof NativeStringType + || $other instanceof NonEmptyStringType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + if ($value instanceof Stringable) { + $value = (string)$value; + } + + return is_numeric($value); + } + + public function cast(mixed $value): string + { + assert($this->canCast($value)); + + return (string)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid numeric string.')->build(); + } + + public function nativeType(): NativeStringType + { + return NativeStringType::get(); + } + + public function toString(): string + { + return 'numeric-string'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/PositiveIntegerType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/PositiveIntegerType.php new file mode 100644 index 0000000..9743fb6 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/PositiveIntegerType.php @@ -0,0 +1,81 @@ + 0; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return Node::functionCall('is_int', [$node])->and($node->isGreaterThan(Node::value(0))); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof NativeIntegerType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + if (is_string($value)) { + $value = ltrim($value, '0'); + } + + return ! is_bool($value) + && filter_var($value, FILTER_VALIDATE_INT) !== false + && $value > 0; + } + + public function cast(mixed $value): int + { + assert($this->canCast($value)); + + return (int)$value; // @phpstan-ignore-line + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid positive integer.')->build(); + } + + public function nativeType(): NativeIntegerType + { + return NativeIntegerType::get(); + } + + public function toString(): string + { + return 'positive-int'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/ScalarConcreteType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/ScalarConcreteType.php new file mode 100644 index 0000000..9682c70 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/ScalarConcreteType.php @@ -0,0 +1,87 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof IntegerType + || $other instanceof FloatType + || $other instanceof StringType + || $other instanceof BooleanType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return is_scalar($value) || $value instanceof Stringable; + } + + public function cast(mixed $value): bool|string|int|float + { + assert($this->canCast($value)); + + if ($value instanceof Stringable) { + return (string)$value; + } + + return $value; // @phpstan-ignore return.type (must be scalar) + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} is not a valid scalar.')->build(); + } + + public function nativeType(): UnionType + { + return new UnionType( + new NativeIntegerType(), + new NativeFloatType(), + new NativeStringType(), + new NativeBooleanType(), + ); + } + + public function toString(): string + { + return 'scalar'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayElement.php b/lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayElement.php new file mode 100644 index 0000000..2a80870 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayElement.php @@ -0,0 +1,39 @@ +key; + } + + public function type(): Type + { + return $this->type; + } + + public function isOptional(): bool + { + return $this->optional; + } + + public function toString(): string + { + return $this->isOptional() + ? "{$this->key->toString()}?: {$this->type->toString()}" + : "{$this->key->toString()}: {$this->type->toString()}"; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayType.php new file mode 100644 index 0000000..f678d62 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/ShapedArrayType.php @@ -0,0 +1,265 @@ + */ + private array $elements; + + private bool $isUnsealed = false; + + private ?ArrayType $unsealedType = null; + + /** + * @no-named-arguments + */ + public function __construct(ShapedArrayElement ...$elements) + { + $this->elements = $elements; + + $keys = []; + + foreach ($this->elements as $elem) { + $key = $elem->key()->value(); + + if (in_array($key, $keys, true)) { + throw new ShapedArrayElementDuplicatedKey((string)$key, $this->toString()); + } + + $keys[] = $key; + } + } + + /** + * @no-named-arguments + */ + public static function unsealed(ArrayType $unsealedType, ShapedArrayElement ...$elements): self + { + $self = new self(...$elements); + $self->isUnsealed = true; + $self->unsealedType = $unsealedType; + + return $self; + } + + /** + * @no-named-arguments + */ + public static function unsealedWithoutType(ShapedArrayElement ...$elements): self + { + $self = new self(...$elements); + $self->isUnsealed = true; + + return $self; + } + + public function isUnsealed(): bool + { + return $this->isUnsealed; + } + + public function hasUnsealedType(): bool + { + return $this->unsealedType !== null; + } + + public function unsealedType(): ArrayType + { + assert($this->isUnsealed); + + return $this->unsealedType ?? ArrayType::native(); + } + + public function accepts(mixed $value): bool + { + if (! is_array($value)) { + return false; + } + + $elements = []; + + foreach ($this->elements as $element) { + $key = $element->key()->value(); + + $elements[$key] = null; + + if (array_key_exists($key, $value) ? ! $element->type()->accepts($value[$key]) : ! $element->isOptional()) { + return false; + } + } + + if ($this->isUnsealed) { + return $this->unsealedType()->accepts(array_diff_key($value, $elements)); + } + + return count($value) <= count($elements); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + $conditions = [ + Node::functionCall('is_array', [$node]), + ...array_map(function (ShapedArrayElement $element) use ($node) { + $key = Node::value($element->key()->value()); + + return Node::ternary( + condition: Node::functionCall('array_key_exists', [$key, $node]), + ifTrue: $element->type()->compiledAccept($node->key($key)), + ifFalse: Node::value($element->isOptional()) + )->wrap(); + }, $this->elements) + ]; + + if ($this->isUnsealed) { + $unsealedType = $this->unsealedType(); + + $elementsKeys = array_flip( + array_map(fn (ShapedArrayElement $element) => $element->key()->value(), $this->elements) + ); + + $conditions[] = Node::functionCall(function_exists('array_all') ? 'array_all' : Polyfill::class . '::array_all', [ + Node::functionCall('array_diff_key', [$node, Node::value($elementsKeys)]), + Node::shortClosure( + Node::logicalAnd( + $unsealedType->keyType()->compiledAccept(Node::variable('key'))->wrap(), + $unsealedType->subType()->compiledAccept(Node::variable('item'))->wrap(), + ), + )->witParameters( + Node::parameterDeclaration('item', 'mixed'), + Node::parameterDeclaration('key', 'mixed'), + ), + ]); + } + + return Node::logicalAnd(...$conditions); + } + + public function matches(Type $other): bool + { + if ($other instanceof MixedType) { + return true; + } + + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof CompositeTraversableType) { + $keyType = $other->keyType(); + $subType = $other->subType(); + + foreach ($this->elements as $element) { + if (! $element->key()->matches($keyType)) { + return false; + } + + if (! $element->type()->matches($subType)) { + return false; + } + } + + if ($this->isUnsealed && ! $this->unsealedType()->matches($other)) { + return false; + } + + return true; + } + + if (! $other instanceof self) { + return false; + } + + foreach ($this->elements as $element) { + foreach ($other->elements as $otherElement) { + if ($element->key()->matches($otherElement->key()) + && $element->type()->matches($otherElement->type()) + ) { + continue 2; + } + } + + if (! $element->isOptional()) { + return false; + } + } + + if ($other->isUnsealed) { + return $this->isUnsealed + && $this->unsealedType()->matches($other->unsealedType()); + } + + return true; + } + + public function traverse(): array + { + $types = []; + + foreach ($this->elements as $element) { + $types[] = $type = $element->type(); + + if ($type instanceof CompositeType) { + $types = [...$types, ...$type->traverse()]; + } + } + + if ($this->isUnsealed) { + $types = [...$types, $this->unsealedType(), ...$this->unsealedType()->traverse()]; + } + + return $types; + } + + /** + * @return ShapedArrayElement[] + */ + public function elements(): array + { + return $this->elements; + } + + public function nativeType(): ArrayType + { + return ArrayType::native(); + } + + public function toString(): string + { + $signature = 'array{'; + $signature .= implode(', ', array_map(fn (ShapedArrayElement $element) => $element->toString(), $this->elements)); + + if ($this->isUnsealed) { + $signature .= ', ...'; + + if ($this->unsealedType) { + $signature .= $this->unsealedType->toString(); + } + } + + $signature .= '}'; + + return $signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/StringValueType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/StringValueType.php new file mode 100644 index 0000000..0ed8797 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/StringValueType.php @@ -0,0 +1,108 @@ +quoteChar = $value[0]; + + return $instance; + } + + public function accepts(mixed $value): bool + { + return $value === $this->value; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return $node->equals(Node::value($this->value)); + } + + public function matches(Type $other): bool + { + if ($other instanceof UnionType) { + return $other->isMatchedBy($this); + } + + if ($other instanceof self) { + return $this->value === $other->value; + } + + if ($other instanceof ArrayKeyType) { + return $other->isMatchedBy($this); + } + + return $other instanceof StringType + || $other instanceof ScalarConcreteType + || $other instanceof MixedType; + } + + public function canCast(mixed $value): bool + { + return (is_string($value) || is_numeric($value) || $value instanceof Stringable) + && (string)$value === $this->value; + } + + public function cast(mixed $value): string + { + assert($this->canCast($value)); + + return $this->value; + } + + public function value(): string + { + return $this->value; + } + + public function errorMessage(): ErrorMessage + { + return MessageBuilder::newError('Value {source_value} does not match string value {expected_value}.') + ->withParameter('expected_value', ValueDumper::dump($this->value)) + ->build(); + } + + public function nativeType(): NativeStringType + { + return NativeStringType::get(); + } + + public function toString(): string + { + if (isset($this->quoteChar)) { + return $this->quoteChar . $this->value . $this->quoteChar; + } + + return $this->value; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/UndefinedObjectType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/UndefinedObjectType.php new file mode 100644 index 0000000..efa67be --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/UndefinedObjectType.php @@ -0,0 +1,51 @@ +isMatchedBy($this); + } + + return $other instanceof self + || $other instanceof ObjectType + || $other instanceof IntersectionType + || $other instanceof MixedType; + } + + public function nativeType(): UndefinedObjectType + { + return $this; + } + + public function toString(): string + { + return 'object'; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/UnionType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/UnionType.php new file mode 100644 index 0000000..c1b13b4 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/UnionType.php @@ -0,0 +1,143 @@ + */ + private array $types; + + private string $signature; + + public function __construct(Type $type, Type $otherType, Type ...$otherTypes) + { + $types = [$type, $otherType, ...$otherTypes]; + $filteredTypes = []; + + foreach ($types as $subType) { + if ($subType instanceof self) { + foreach ($subType->types as $anotherSubType) { + $filteredTypes[] = $anotherSubType; + } + + continue; + } + + if ($subType instanceof MixedType) { + throw new ForbiddenMixedType(); + } + + $filteredTypes[] = $subType; + } + + $this->types = $filteredTypes; + $this->signature = implode('|', array_map(fn (Type $type) => $type->toString(), $this->types)); + } + + public function accepts(mixed $value): bool + { + foreach ($this->types as $type) { + if ($type->accepts($value)) { + return true; + } + } + + return false; + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + return Node::logicalOr( + ...array_map( + fn (Type $type) => $type->compiledAccept($node), + $this->types, + ) + ); + } + + public function matches(Type $other): bool + { + if ($other instanceof self) { + foreach ($this->types as $type) { + if (! $other->isMatchedBy($type)) { + return false; + } + } + + return true; + } + + foreach ($this->types as $type) { + if ($type->matches($other)) { + return true; + } + } + + return false; + } + + public function isMatchedBy(Type $other): bool + { + foreach ($this->types as $type) { + if ($other->matches($type)) { + return true; + } + } + + return false; + } + + public function traverse(): array + { + $types = []; + + foreach ($this->types as $type) { + $types[] = $type; + + if ($type instanceof CompositeType) { + $types = [...$types, ...$type->traverse()]; + } + } + + return $types; + } + + public function types(): array + { + return $this->types; + } + + public function nativeType(): UnionType + { + $subNativeTypes = []; + + foreach ($this->types as $type) { + if (isset($subNativeTypes[$type->toString()])) { + continue; + } + + $subNativeTypes[$type->toString()] = $type->nativeType(); + } + + return new self(...array_values($subNativeTypes)); + } + + public function toString(): string + { + return $this->signature; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Type/Types/UnresolvableType.php b/lib/Vendor/CuyZ/Valinor/Type/Types/UnresolvableType.php new file mode 100644 index 0000000..cf79d9e --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Type/Types/UnresolvableType.php @@ -0,0 +1,143 @@ +rawType, + "The type `$this->rawType` for property `$signature` could not be resolved: $this->message", + ); + } + + public function forParameter(string $signature): self + { + return new self( + $this->rawType, + "The type `$this->rawType` for parameter `$signature` could not be resolved: $this->message", + ); + } + + public function forFunctionReturnType(string $signature): self + { + return new self( + $this->rawType, + "The return type `$this->rawType` of function `$signature` could not be resolved: $this->message", + ); + } + + public function forMethodReturnType(string $signature): self + { + return new self( + $this->rawType, + "The return type `$this->rawType` of method `$signature` could not be resolved: $this->message", + ); + } + + public static function forInvalidPropertyDefaultValue(string $signature, Type $type, mixed $defaultValue): self + { + $value = ValueDumper::dump($defaultValue); + + return new self( + $type->toString(), + "Property `$signature` of type `{$type->toString()}` has invalid default value $value.", + ); + } + + public static function forInvalidParameterDefaultValue(string $signature, Type $type, mixed $defaultValue): self + { + $value = ValueDumper::dump($defaultValue); + + return new self( + $type->toString(), + "Parameter `$signature` of type `{$type->toString()}` has invalid default value $value.", + ); + } + + public static function forNonMatchingPropertyTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Types for property `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } + + public static function forNonMatchingParameterTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Types for parameter `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } + + public static function forNonMatchingFunctionReturnTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Return types for function `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } + + public static function forNonMatchingMethodReturnTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Return types for method `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } + + public static function forLocalAlias(string $raw, string $name, ObjectType $type, InvalidType $exception): self + { + return new self( + $raw, + "The type `$raw` for local alias `$name` of the class `{$type->className()}` could not be resolved: {$exception->getMessage()}", + ); + } + + public function message(): string + { + return $this->message; + } + + public function accepts(mixed $value): bool + { + throw new LogicException(); + } + + public function matches(Type $other): bool + { + throw new LogicException(); + } + + public function compiledAccept(ComplianceNode $node): ComplianceNode + { + throw new LogicException(); + } + + public function nativeType(): Type + { + throw new LogicException(); + } + + public function toString(): string + { + return $this->rawType; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/IsSingleton.php b/lib/Vendor/CuyZ/Valinor/Utility/IsSingleton.php new file mode 100644 index 0000000..ae50e03 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/IsSingleton.php @@ -0,0 +1,16 @@ + $array + */ + public static function array_all(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if (! $callback($value, $key)) { + return false; + } + } + + return true; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/Priority/HasPriority.php b/lib/Vendor/CuyZ/Valinor/Utility/Priority/HasPriority.php new file mode 100644 index 0000000..6fa5988 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/Priority/HasPriority.php @@ -0,0 +1,19 @@ + + */ +final class PrioritizedList implements IteratorAggregate +{ + /** @var array */ + private array $objects = []; + + /** + * @param T ...$objects + */ + public function __construct(object ...$objects) + { + foreach ($objects as $object) { + $this->objects[$this->priority($object)][] = $object; + } + + krsort($this->objects, SORT_NUMERIC); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + foreach ($this->objects as $priority => $objects) { + foreach ($objects as $object) { + yield $priority => $object; + } + } + } + + private function priority(object $object): int + { + return $object instanceof HasPriority + ? $object->priority() + : 0; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/Reflection/NamespaceFinder.php b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/NamespaceFinder.php new file mode 100644 index 0000000..90b196c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/NamespaceFinder.php @@ -0,0 +1,34 @@ +is(T_NAMESPACE)) { + /* @infection-ignore-all Unneeded because of the nature of namespace-related token */ + if ($pointer === 0) { + return null; + } + + $pointer--; + } + + while (! $tokens[$pointer]->is([T_NAME_QUALIFIED, T_STRING])) { + $pointer++; + } + + return (string)$tokens[$pointer]; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/Reflection/PhpParser.php b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/PhpParser.php new file mode 100644 index 0000000..735d310 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/PhpParser.php @@ -0,0 +1,100 @@ +> */ + private static array $statements = []; + + /** + * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection + * @return array + */ + public static function parseUseStatements(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): array + { + $signature = "{$reflection->getFileName()}:{$reflection->getStartLine()}"; + + // @infection-ignore-all + return self::$statements[$signature] ??= self::fetchUseStatements($reflection); + } + + public static function parseNamespace(ReflectionFunction $reflection): ?string + { + $content = self::getFileContent($reflection); + + if ($content === null) { + return null; + } + + return (new NamespaceFinder())->findNamespace($content); + } + + /** + * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection + * @return array + */ + private static function fetchUseStatements(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): array + { + if ($reflection instanceof ReflectionMethod) { + $namespaceName = $reflection->getDeclaringClass()->getNamespaceName(); + } elseif ($reflection instanceof ReflectionFunction && $reflection->getClosureScopeClass()) { + $namespaceName = $reflection->getClosureScopeClass()->getNamespaceName(); + } else { + $namespaceName = $reflection->getNamespaceName(); + } + + $content = self::getFileContent($reflection); + + if ($content === null) { + return []; + } + + return (new TokenParser($content))->parseUseStatements($namespaceName); + } + + /** + * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection + */ private static function getFileContent(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): ?string + { + $filename = $reflection->getFileName(); + $startLine = $reflection->getStartLine(); + + // @infection-ignore-all these values will never be `true` + if ($filename === false || $startLine === false) { + return null; + } + + if (! is_file($filename)) { + return null; + } + + // @infection-ignore-all no need to test with `-1` + $lineCnt = 0; + $content = ''; + $file = new SplFileObject($filename); + + while (! $file->eof()) { + if ($lineCnt++ === $startLine) { + break; + } + + $content .= $file->fgets(); + } + + return $content; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/Reflection/Reflection.php b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/Reflection.php new file mode 100644 index 0000000..2796ab0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/Reflection.php @@ -0,0 +1,69 @@ +> */ + private static array $classReflection = []; + + /** @var array */ + private static array $functionReflection = []; + + /** @var array */ + private static array $classOrInterfaceExists = []; + + /** @var array */ + private static array $enumExists = []; + + /** + * Case-sensitive implementation of `class_exists` and `interface_exists`. + * + * @phpstan-assert-if-true class-string $name + */ + public static function classOrInterfaceExists(string $name): bool + { + // @infection-ignore-all / We don't need to test the cache + return self::$classOrInterfaceExists[$name] ??= (class_exists($name) || interface_exists($name)) + && self::class($name)->name === ltrim($name, '\\'); + } + + /** + * @phpstan-assert-if-true class-string $name + */ + public static function enumExists(string $name): bool + { + // @infection-ignore-all / We don't need to test the cache + return self::$enumExists[$name] ??= enum_exists($name); + } + + /** + * @param class-string $className + * @return ReflectionClass + */ + public static function class(string $className): ReflectionClass + { + return self::$classReflection[$className] ??= new ReflectionClass($className); + } + + public static function function(callable $function): ReflectionFunction + { + $closure = Closure::fromCallable($function); + + return self::$functionReflection[spl_object_hash($closure)] ??= new ReflectionFunction($closure); + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/Reflection/TokenParser.php b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/TokenParser.php new file mode 100644 index 0000000..4a924a1 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/Reflection/TokenParser.php @@ -0,0 +1,132 @@ + */ + private array $tokens; + + private int $numTokens; + + private int $pointer = 0; + + public function __construct(string $content) + { + $this->tokens = PhpToken::tokenize($content); + $this->numTokens = count($this->tokens); + } + + /** + * @return array + */ + public function parseUseStatements(string $namespaceName): array + { + $currentNamespace = ''; + $statements = []; + + while ($token = $this->next()) { + if ($currentNamespace === $namespaceName && $token->is(T_USE)) { + $statements = [...$statements, ...$this->parseUseStatement()]; + continue; + } + + if (! $token->is(T_NAMESPACE)) { + continue; + } + + $currentNamespace = $this->parseNamespace(); + + // Get fresh array for new namespace. This is to prevent the parser + // to collect the use statements for a previous namespace with the + // same name (this is the case if a namespace is defined twice). + $statements = []; + } + + return $statements; + } + + /** + * @return array + */ + private function parseUseStatement(): array + { + $groupRoot = ''; + $class = ''; + $alias = ''; + $statements = []; + $explicitAlias = false; + + while ($token = $this->next()) { + $name = (string)$token; + + if (! $explicitAlias && $token->is(T_STRING)) { + $class = $alias = $name; + } elseif ($explicitAlias && $token->is(T_STRING)) { + $alias = $name; + } elseif ($token->is([T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED])) { + $class = $name; + $classSplit = explode('\\', $name); + $alias = $classSplit[count($classSplit) - 1]; + } elseif ($token->is(T_NS_SEPARATOR)) { + $class .= '\\'; + $alias = ''; + } elseif ($token->is(T_AS)) { + $explicitAlias = true; + $alias = ''; + } elseif ($name === ',') { + $statements[strtolower($alias)] = $groupRoot . $class; + $class = $alias = ''; + $explicitAlias = false; + } elseif ($name === ';') { + if ($alias !== '') { + $statements[strtolower($alias)] = $groupRoot . $class; + } + break; + } elseif ($name === '{') { + $groupRoot = $class; + $class = ''; + } + } + + return $statements; + } + + private function next(): ?PhpToken + { + for ($i = $this->pointer; $i < $this->numTokens; $i++) { + $this->pointer++; + + if (! $this->tokens[$i]->isIgnorable()) { + return $this->tokens[$i]; + } + } + + return null; + } + + private function parseNamespace(): string + { + while ($token = $this->next()) { + if ($token->is([T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_STRING])) { + return (string)$token; + } + } + + /** @infection-ignore-all */ + // @codeCoverageIgnoreStart + throw new LogicException('Namespace not found.'); + // @codeCoverageIgnoreEnd + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/String/StringCutter.php b/lib/Vendor/CuyZ/Valinor/Utility/String/StringCutter.php new file mode 100644 index 0000000..039d98c --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/String/StringCutter.php @@ -0,0 +1,51 @@ += 0); + + return match (true) { + default => substr($s, 0, $cur), + // U+0080 - U+07FF + $cnt === 1 && (ord($s[$cur]) & 0b1110_0000) === 0b1100_0000, + // U+0800 - U+FFFF + $cnt === 2 && (ord($s[$cur]) & 0b1111_0000) === 0b1110_0000, + // U+10000 - U+10FFFF + $cnt === 3 && (ord($s[$cur]) & 0b1111_1000) === 0b1111_0000 => $s + }; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatter.php b/lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatter.php new file mode 100644 index 0000000..2c26454 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatter.php @@ -0,0 +1,74 @@ + $parameters + */ + public static function format(string $locale, string $body, array $parameters = []): string + { + return class_exists(MessageFormatter::class) + ? self::formatWithIntl($locale, $body, $parameters) + : self::formatWithRegex($body, $parameters); + } + + public static function for(HasParameters $message): string + { + return self::formatWithRegex($message->body(), $message->parameters()); + } + + /** + * @param array $parameters + */ + private static function formatWithIntl(string $locale, string $body, array $parameters): string + { + try { + $formatted = MessageFormatter::formatMessage($locale, $body, $parameters); + + if ($formatted === false) { + throw new StringFormatterError($body, intl_get_error_message()); + } + + return $formatted; + } catch (IntlException $e) { + throw new StringFormatterError($body, $e->getMessage(), $e); + } + } + + /** + * @param array $parameters + */ + private static function formatWithRegex(string $body, array $parameters): string + { + $message = $body; + + if (preg_match('/{\s*[^}]*[^}a-z_]+\s*}?/', $body)) { + throw new StringFormatterError($body); + } + + foreach ($parameters as $name => $value) { + $name = preg_quote($name, '/'); + + /** @var string $message */ + $message = preg_replace("/{\s*$name\s*}/", $value, $message); + } + + return $message; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatterError.php b/lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatterError.php new file mode 100644 index 0000000..57d7cf0 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/String/StringFormatterError.php @@ -0,0 +1,19 @@ + 4, + $type instanceof FloatType => 3, + $type instanceof StringType => 2, + $type instanceof BooleanType => 1, + default => 0, + }; + } + + public static function dump(Type $type, bool $surround = true): string + { + if ($type instanceof EnumType) { + $text = $type->readableSignature(); + } elseif ($type instanceof FixedType) { + return $type->toString(); + } elseif (self::containsObject($type)) { + $text = '?'; + } else { + $text = $type->toString(); + } + + return $surround ? "`$text`" : $text; + } + + public static function dumpArguments(Arguments $arguments): string + { + if (count($arguments) === 0) { + return 'array'; + } + + if (count($arguments) === 1) { + return self::dump($arguments->at(0)->type()); + } + + $parameters = array_map( + function (Argument $argument) { + $name = $argument->name(); + $type = $argument->type(); + + $signature = self::dump($type, false); + + return $argument->isRequired() ? "$name: $signature" : "$name?: $signature"; + }, + [...$arguments], + ); + + return '`array{' . implode(', ', $parameters) . '}`'; + } + + public static function containsObject(Type $type): bool + { + if ($type instanceof CompositeType) { + foreach ($type->traverse() as $subType) { + if (self::containsObject($subType)) { + return true; + } + } + } + + return $type instanceof ObjectType; + } +} diff --git a/lib/Vendor/CuyZ/Valinor/Utility/ValueDumper.php b/lib/Vendor/CuyZ/Valinor/Utility/ValueDumper.php new file mode 100644 index 0000000..1b17bc5 --- /dev/null +++ b/lib/Vendor/CuyZ/Valinor/Utility/ValueDumper.php @@ -0,0 +1,146 @@ +value) + ? "'$value->value'" + : (string)$value->value; + } + + if ($value instanceof UnitEnum) { + return "'$value->name'"; + } + + if ($value instanceof DateTimeInterface) { + return $value->format(self::DATE_FORMAT); + } + + if (is_iterable($value) && ! $value instanceof Generator) { + /** @var iterable $value */ + if (is_array($value)) { + $type = 'array'; + } else { + $type = 'iterable'; + } + + $values = self::listValues($value); + + if (empty($values)) { + return "$type (empty)"; + } + + if (! $goDeeper) { + return "$type{…}"; + } + + return "$type{" . implode(', ', $values) . '}'; + } + + if (is_object($value)) { + return 'object(' . $value::class . ')'; + } + + // @codeCoverageIgnoreStart + return 'unknown'; + // @codeCoverageIgnoreEnd + } + + /** + * @param iterable $iterable + * @return array + */ + private static function listValues(iterable $iterable): array + { + $values = []; + $index = 0; + + foreach ($iterable as $key => $value) { + $values[] = "$key: " . self::doDump($value, false); + + if ($index++ >= self::MAX_ARRAY_ENTRIES) { + $values[] = '…'; + break; + } + } + + return $values; + } + + private static function crop(string $string): string + { + if (strlen($string) <= self::MAX_STRING_LENGTH) { + return $string; + } + + $string = StringCutter::cut($string, self::MAX_STRING_LENGTH + 1); + + for ($i = strlen($string) - 1; $i > 10; $i--) { + if ($string[$i] === ' ') { + return StringCutter::cut($string, $i) . '…'; + } + } + + return $string . '…'; + } +} diff --git a/lib/Vendor/Firebase/JWT/BeforeValidException.php b/lib/Vendor/Firebase/JWT/BeforeValidException.php new file mode 100644 index 0000000..db23ef0 --- /dev/null +++ b/lib/Vendor/Firebase/JWT/BeforeValidException.php @@ -0,0 +1,18 @@ +payload = $payload; + } + + public function getPayload(): object + { + return $this->payload; + } +} diff --git a/lib/Vendor/Firebase/JWT/CachedKeySet.php b/lib/Vendor/Firebase/JWT/CachedKeySet.php new file mode 100644 index 0000000..73ec4e0 --- /dev/null +++ b/lib/Vendor/Firebase/JWT/CachedKeySet.php @@ -0,0 +1,275 @@ + + */ +class CachedKeySet implements ArrayAccess +{ + /** + * @var string + */ + private $jwksUri; + /** + * @var ClientInterface + */ + private $httpClient; + /** + * @var RequestFactoryInterface + */ + private $httpFactory; + /** + * @var CacheItemPoolInterface + */ + private $cache; + /** + * @var ?int + */ + private $expiresAfter; + /** + * @var ?CacheItemInterface + */ + private $cacheItem; + /** + * @var array> + */ + private $keySet; + /** + * @var string + */ + private $cacheKey; + /** + * @var string + */ + private $cacheKeyPrefix = 'jwks'; + /** + * @var int + */ + private $maxKeyLength = 64; + /** + * @var bool + */ + private $rateLimit; + /** + * @var string + */ + private $rateLimitCacheKey; + /** + * @var int + */ + private $maxCallsPerMinute = 10; + /** + * @var string|null + */ + private $defaultAlg; + + public function __construct( + string $jwksUri, + ClientInterface $httpClient, + RequestFactoryInterface $httpFactory, + CacheItemPoolInterface $cache, + ?int $expiresAfter = null, + bool $rateLimit = false, + ?string $defaultAlg = null + ) { + $this->jwksUri = $jwksUri; + $this->httpClient = $httpClient; + $this->httpFactory = $httpFactory; + $this->cache = $cache; + $this->expiresAfter = $expiresAfter; + $this->rateLimit = $rateLimit; + $this->defaultAlg = $defaultAlg; + $this->setCacheKeys(); + } + + /** + * @param string $keyId + * @return Key + */ + public function offsetGet($keyId): Key + { + if (!$this->keyIdExists($keyId)) { + throw new OutOfBoundsException('Key ID not found'); + } + return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); + } + + /** + * @param string $keyId + * @return bool + */ + public function offsetExists($keyId): bool + { + return $this->keyIdExists($keyId); + } + + /** + * @param string $offset + * @param Key $value + */ + public function offsetSet($offset, $value): void + { + throw new LogicException('Method not implemented'); + } + + /** + * @param string $offset + */ + public function offsetUnset($offset): void + { + throw new LogicException('Method not implemented'); + } + + /** + * @return array + */ + private function formatJwksForCache(string $jwks): array + { + $jwks = json_decode($jwks, true); + + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + + $keys = []; + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + $keys[(string) $kid] = $v; + } + + return $keys; + } + + private function keyIdExists(string $keyId): bool + { + if (null === $this->keySet) { + $item = $this->getCacheItem(); + // Try to load keys from cache + if ($item->isHit()) { + // item found! retrieve it + $this->keySet = $item->get(); + // If the cached item is a string, the JWKS response was cached (previous behavior). + // Parse this into expected format array instead. + if (\is_string($this->keySet)) { + $this->keySet = $this->formatJwksForCache($this->keySet); + } + } + } + + if (!isset($this->keySet[$keyId])) { + if ($this->rateLimitExceeded()) { + return false; + } + $request = $this->httpFactory->createRequest('GET', $this->jwksUri); + $jwksResponse = $this->httpClient->sendRequest($request); + if ($jwksResponse->getStatusCode() !== 200) { + throw new UnexpectedValueException( + \sprintf( + 'HTTP Error: %d %s for URI "%s"', + $jwksResponse->getStatusCode(), + $jwksResponse->getReasonPhrase(), + $this->jwksUri, + ), + $jwksResponse->getStatusCode() + ); + } + $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); + + if (!isset($this->keySet[$keyId])) { + return false; + } + + $item = $this->getCacheItem(); + $item->set($this->keySet); + if ($this->expiresAfter) { + $item->expiresAfter($this->expiresAfter); + } + $this->cache->save($item); + } + + return true; + } + + private function rateLimitExceeded(): bool + { + if (!$this->rateLimit) { + return false; + } + + $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); + + $cacheItemData = []; + if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) { + $cacheItemData = $data; + } + + $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0; + $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC')); + + if (++$callsPerMinute > $this->maxCallsPerMinute) { + return true; + } + + $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]); + $cacheItem->expiresAt($expiry); + $this->cache->save($cacheItem); + return false; + } + + private function getCacheItem(): CacheItemInterface + { + if (\is_null($this->cacheItem)) { + $this->cacheItem = $this->cache->getItem($this->cacheKey); + } + + return $this->cacheItem; + } + + private function setCacheKeys(): void + { + if (empty($this->jwksUri)) { + throw new RuntimeException('JWKS URI is empty'); + } + + // ensure we do not have illegal characters + $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); + + // add prefix + $key = $this->cacheKeyPrefix . $key; + + // Hash keys if they exceed $maxKeyLength of 64 + if (\strlen($key) > $this->maxKeyLength) { + $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); + } + + $this->cacheKey = $key; + + if ($this->rateLimit) { + // add prefix + $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; + + // Hash keys if they exceed $maxKeyLength of 64 + if (\strlen($rateLimitKey) > $this->maxKeyLength) { + $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); + } + + $this->rateLimitCacheKey = $rateLimitKey; + } + } +} diff --git a/lib/Vendor/Firebase/JWT/ExpiredException.php b/lib/Vendor/Firebase/JWT/ExpiredException.php new file mode 100644 index 0000000..14bcf7e --- /dev/null +++ b/lib/Vendor/Firebase/JWT/ExpiredException.php @@ -0,0 +1,30 @@ +payload = $payload; + } + + public function getPayload(): object + { + return $this->payload; + } + + public function setTimestamp(int $timestamp): void + { + $this->timestamp = $timestamp; + } + + public function getTimestamp(): ?int + { + return $this->timestamp; + } +} diff --git a/lib/Vendor/Firebase/JWT/JWK.php b/lib/Vendor/Firebase/JWT/JWK.php new file mode 100644 index 0000000..e1c76ec --- /dev/null +++ b/lib/Vendor/Firebase/JWT/JWK.php @@ -0,0 +1,363 @@ + + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWK +{ + private const OID = '1.2.840.10045.2.1'; + private const ASN1_OBJECT_IDENTIFIER = 0x06; + private const ASN1_SEQUENCE = 0x10; // also defined in JWT + private const ASN1_BIT_STRING = 0x03; + private const EC_CURVES = [ + 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 + 'secp256k1' => '1.3.132.0.10', // Len: 64 + 'P-384' => '1.3.132.0.34', // Len: 96 + // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) + ]; + + // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype. + // This library supports the following subtypes: + private const OKP_SUBTYPES = [ + 'Ed25519' => true, // RFC 8037 + ]; + + /** + * Parse a set of JWK keys + * + * @param array $jwks The JSON Web Key Set as an associative array + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set + * + * @return array An associative array of key IDs (kid) to Key objects + * + * @throws InvalidArgumentException Provided JWK Set is empty + * @throws UnexpectedValueException Provided JWK Set was invalid + * @throws DomainException OpenSSL failure + * + * @uses parseKey + */ + public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null): array + { + $keys = []; + + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + if ($key = self::parseKey($v, $defaultAlg)) { + $keys[(string) $kid] = $key; + } + } + + if (0 === \count($keys)) { + throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + } + + return $keys; + } + + /** + * Parse a JWK key + * + * @param array $jwk An individual JWK + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set + * + * @return Key The key object for the JWK + * + * @throws InvalidArgumentException Provided JWK is empty + * @throws UnexpectedValueException Provided JWK was invalid + * @throws DomainException OpenSSL failure + * + * @uses createPemFromModulusAndExponent + */ + public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defaultAlg = null): ?Key + { + if (empty($jwk)) { + throw new InvalidArgumentException('JWK must not be empty'); + } + + if (!isset($jwk['kty'])) { + throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + } + + if (!isset($jwk['alg'])) { + if (\is_null($defaultAlg)) { + // The "alg" parameter is optional in a KTY, but an algorithm is required + // for parsing in this library. Use the $defaultAlg parameter when parsing the + // key set in order to prevent this error. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + } + $jwk['alg'] = $defaultAlg; + } + + switch ($jwk['kty']) { + case 'RSA': + if (!empty($jwk['d'])) { + throw new UnexpectedValueException('RSA private keys are not supported'); + } + if (!isset($jwk['n']) || !isset($jwk['e'])) { + throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + } + + $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); + $publicKey = \openssl_pkey_get_public($pem); + if (false === $publicKey) { + throw new DomainException( + 'OpenSSL error: ' . \openssl_error_string() + ); + } + return new Key($publicKey, $jwk['alg']); + case 'EC': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + + if (empty($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + + if (!isset(self::EC_CURVES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported EC curve'); + } + + if (empty($jwk['x']) || empty($jwk['y'])) { + throw new UnexpectedValueException('x and y not set'); + } + + $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); + return new Key($publicKey, $jwk['alg']); + case 'OKP': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + + if (!isset($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + + if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported OKP key subtype'); + } + + if (empty($jwk['x'])) { + throw new UnexpectedValueException('x not set'); + } + + // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. + $publicKey = JWT::convertBase64urlToBase64($jwk['x']); + return new Key($publicKey, $jwk['alg']); + case 'oct': + if (!isset($jwk['k'])) { + throw new UnexpectedValueException('k not set'); + } + + return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']); + default: + break; + } + + return null; + } + + /** + * Converts the EC JWK values to pem format. + * + * @param string $crv The EC curve (only P-256 & P-384 is supported) + * @param string $x The EC x-coordinate + * @param string $y The EC y-coordinate + * + * @return string + */ + private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string + { + $pem = + self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER( + self::ASN1_OBJECT_IDENTIFIER, + self::encodeOID(self::OID) + ) + . self::encodeDER( + self::ASN1_OBJECT_IDENTIFIER, + self::encodeOID(self::EC_CURVES[$crv]) + ) + ) . + self::encodeDER( + self::ASN1_BIT_STRING, + \chr(0x00) . \chr(0x04) + . JWT::urlsafeB64Decode($x) + . JWT::urlsafeB64Decode($y) + ) + ); + + return \sprintf( + "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", + wordwrap(base64_encode($pem), 64, "\n", true) + ); + } + + /** + * Create a public key represented in PEM format from RSA modulus and exponent information + * + * @param string $n The RSA modulus encoded in Base64 + * @param string $e The RSA exponent encoded in Base64 + * + * @return string The RSA public key represented in PEM format + * + * @uses encodeLength + */ + private static function createPemFromModulusAndExponent( + string $n, + string $e + ): string { + $mod = JWT::urlsafeB64Decode($n); + $exp = JWT::urlsafeB64Decode($e); + // Correct encoding for ASN1, as ints are represented as unsigned in jwk + // but signed in ASN1. Prepending null byte makes it unsigned. + if (\strlen($mod) > 0 && \ord($mod[0]) >= 128) { + $mod = \chr(0) . $mod; + } + if (\strlen($exp) > 0 && \ord($exp[0]) >= 128) { + $exp = \chr(0) . $exp; + } + + $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); + $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); + + $rsaPublicKey = \pack( + 'Ca*a*a*', + 48, + self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), + $modulus, + $publicExponent + ); + + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = \chr(0) . $rsaPublicKey; + $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; + + $rsaPublicKey = \pack( + 'Ca*a*', + 48, + self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), + $rsaOID . $rsaPublicKey + ); + + return "-----BEGIN PUBLIC KEY-----\r\n" . + \chunk_split(\base64_encode($rsaPublicKey), 64) . + '-----END PUBLIC KEY-----'; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @param int $length + * @return string + */ + private static function encodeLength(int $length): string + { + if ($length <= 0x7F) { + return \chr($length); + } + + $temp = \ltrim(\pack('N', $length), \chr(0)); + + return \pack('Ca*', 0x80 | \strlen($temp), $temp); + } + + /** + * Encodes a value into a DER object. + * Also defined in OCA\Talk\Vendor\Firebase\JWT\JWT + * + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object + */ + private static function encodeDER(int $type, string $value): string + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + + // Type + $der = \chr($tag_header | $type); + + // Length + $der .= \chr(\strlen($value)); + + return $der . $value; + } + + /** + * Encodes a string into a DER-encoded OID. + * + * @param string $oid the OID string + * @return string the binary DER-encoded OID + */ + private static function encodeOID(string $oid): string + { + $octets = explode('.', $oid); + + // Get the first octet + $first = (int) array_shift($octets); + $second = (int) array_shift($octets); + $oid = \chr($first * 40 + $second); + + // Iterate over subsequent octets + foreach ($octets as $octet) { + if ($octet == 0) { + $oid .= \chr(0x00); + continue; + } + $bin = ''; + + while ($octet) { + $bin .= \chr(0x80 | ($octet & 0x7f)); + $octet >>= 7; + } + $bin[0] = $bin[0] & \chr(0x7f); + + // Convert to big endian if necessary + if (pack('V', 65534) == pack('L', 65534)) { + $oid .= strrev($bin); + } else { + $oid .= $bin; + } + } + + return $oid; + } +} diff --git a/lib/Vendor/Firebase/JWT/JWT.php b/lib/Vendor/Firebase/JWT/JWT.php new file mode 100644 index 0000000..069d37c --- /dev/null +++ b/lib/Vendor/Firebase/JWT/JWT.php @@ -0,0 +1,745 @@ + + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWT +{ + private const ASN1_INTEGER = 0x02; + private const ASN1_SEQUENCE = 0x10; + private const ASN1_BIT_STRING = 0x03; + + private const RSA_KEY_MIN_LENGTH = 2048; + + /** + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. + * + * @var int + */ + public static $leeway = 0; + + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * Will default to PHP time() value if null. + * + * @var ?int + */ + public static $timestamp = null; + + /** + * @var array + */ + public static $supported_algs = [ + 'ES384' => ['openssl', 'SHA384'], + 'ES256' => ['openssl', 'SHA256'], + 'ES256K' => ['openssl', 'SHA256'], + 'HS256' => ['hash_hmac', 'SHA256'], + 'HS384' => ['hash_hmac', 'SHA384'], + 'HS512' => ['hash_hmac', 'SHA512'], + 'RS256' => ['openssl', 'SHA256'], + 'RS384' => ['openssl', 'SHA384'], + 'RS512' => ['openssl', 'SHA512'], + 'EdDSA' => ['sodium_crypto', 'EdDSA'], + ]; + + /** + * Decodes a JWT string into a PHP object. + * + * @param string $jwt The JWT + * @param Key|ArrayAccess|array $keyOrKeyArray The Key or associative array of key IDs + * (kid) to Key objects. + * If the algorithm used is asymmetric, this is + * the public key. + * Each Key object contains an algorithm and + * matching key. + * Supported algorithms are 'ES384','ES256', + * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384' + * and 'RS512'. + * @param stdClass $headers Optional. Populates stdClass with headers. + * + * @return stdClass The JWT's payload as a PHP object + * + * @throws InvalidArgumentException Provided key/key-array was empty or malformed + * @throws DomainException Provided JWT is malformed + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim + * + * @uses jsonDecode + * @uses urlsafeB64Decode + */ + public static function decode( + string $jwt, + #[\SensitiveParameter] $keyOrKeyArray, + ?stdClass &$headers = null + ): stdClass { + // Validate JWT + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + + if (empty($keyOrKeyArray)) { + throw new InvalidArgumentException('Key may not be empty'); + } + $tks = \explode('.', $jwt); + if (\count($tks) !== 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $bodyb64, $cryptob64) = $tks; + $headerRaw = static::urlsafeB64Decode($headb64); + if (null === ($header = static::jsonDecode($headerRaw))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if ($headers !== null) { + $headers = $header; + } + $payloadRaw = static::urlsafeB64Decode($bodyb64); + if (null === ($payload = static::jsonDecode($payloadRaw))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + if (\is_array($payload)) { + // prevent PHP Fatal Error in edge-cases when payload is empty array + $payload = (object) $payload; + } + if (!$payload instanceof stdClass) { + throw new UnexpectedValueException('Payload must be a JSON object'); + } + if (isset($payload->iat) && !\is_numeric($payload->iat)) { + throw new UnexpectedValueException('Payload iat must be a number'); + } + if (isset($payload->nbf) && !\is_numeric($payload->nbf)) { + throw new UnexpectedValueException('Payload nbf must be a number'); + } + if (isset($payload->exp) && !\is_numeric($payload->exp)) { + throw new UnexpectedValueException('Payload exp must be a number'); + } + + $sig = static::urlsafeB64Decode($cryptob64); + if (empty($header->alg)) { + throw new UnexpectedValueException('Empty algorithm'); + } + if (empty(static::$supported_algs[$header->alg])) { + throw new UnexpectedValueException('Algorithm not supported'); + } + + $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); + + // Check the algorithm + if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); + } + if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures + $sig = self::signatureToDER($sig); + } + if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { + throw new SignatureInvalidException('Signature verification failed'); + } + + // Check the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { + $ex = new BeforeValidException( + 'Cannot handle token with nbf prior to ' . \date(DateTime::ATOM, (int) floor($payload->nbf)) + ); + $ex->setPayload($payload); + throw $ex; + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { + $ex = new BeforeValidException( + 'Cannot handle token with iat prior to ' . \date(DateTime::ATOM, (int) floor($payload->iat)) + ); + $ex->setPayload($payload); + throw $ex; + } + + // Check if this token has expired. + if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { + $ex = new ExpiredException('Expired token'); + $ex->setPayload($payload); + $ex->setTimestamp($timestamp); + throw $ex; + } + + return $payload; + } + + /** + * Converts and signs a PHP array into a JWT string. + * + * @param array $payload PHP array + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $keyId + * @param array $head An array with header elements to attach + * + * @return string A signed JWT + * + * @uses jsonEncode + * @uses urlsafeB64Encode + */ + public static function encode( + array $payload, + #[\SensitiveParameter] $key, + string $alg, + ?string $keyId = null, + ?array $head = null + ): string { + $header = ['typ' => 'JWT']; + if (isset($head)) { + $header = \array_merge($header, $head); + } + $header['alg'] = $alg; + if ($keyId !== null) { + $header['kid'] = $keyId; + } + $segments = []; + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); + $signing_input = \implode('.', $segments); + + $signature = static::sign($signing_input, $key, $alg); + $segments[] = static::urlsafeB64Encode($signature); + + return \implode('.', $segments); + } + + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * + * @return string An encrypted message + * + * @throws DomainException Unsupported algorithm or bad key was specified + */ + public static function sign( + string $msg, + #[\SensitiveParameter] $key, + string $alg + ): string { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algorithm) = static::$supported_algs[$alg]; + switch ($function) { + case 'hash_hmac': + if (!\is_string($key)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } + self::validateHmacKeyLength($key, $algorithm); + return \hash_hmac($algorithm, $msg, $key, true); + case 'openssl': + $signature = ''; + if (!$key = openssl_pkey_get_private($key)) { + throw new DomainException('OpenSSL unable to validate key'); + } + if (str_starts_with($alg, 'RS')) { + self::validateRsaKeyLength($key); + } elseif (str_starts_with($alg, 'ES')) { + self::validateEcKeyLength($key, $alg); + } + $success = \openssl_sign($msg, $signature, $key, $algorithm); + if (!$success) { + throw new DomainException('OpenSSL unable to sign data'); + } + if ($alg === 'ES256' || $alg === 'ES256K') { + $signature = self::signatureFromDER($signature, 256); + } elseif ($alg === 'ES384') { + $signature = self::signatureFromDER($signature, 384); + } + return $signature; + case 'sodium_crypto': + try { + return sodium_crypto_sign_detached($msg, self::validateEdDSAKey($key)); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } + } + + throw new DomainException('Algorithm not supported'); + } + + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm + * + * @return bool + * + * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure + */ + private static function verify( + string $msg, + string $signature, + #[\SensitiveParameter] $keyMaterial, + string $alg + ): bool { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + + list($function, $algorithm) = static::$supported_algs[$alg]; + switch ($function) { + case 'openssl': + if (!$key = openssl_pkey_get_public($keyMaterial)) { + throw new DomainException('OpenSSL unable to validate key'); + } + if (str_starts_with($alg, 'RS')) { + self::validateRsaKeyLength($key); + } elseif (str_starts_with($alg, 'ES')) { + self::validateEcKeyLength($key, $alg); + } + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); + if ($success === 1) { + return true; + } + if ($success === 0) { + return false; + } + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException( + 'OpenSSL error: ' . \openssl_error_string() + ); + case 'sodium_crypto': + try { + $key = self::validateEdDSAKey($keyMaterial); + if (\strlen($signature) === 0) { + throw new DomainException('Signature cannot be empty string'); + } + return sodium_crypto_sign_verify_detached($signature, $msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } + case 'hash_hmac': + default: + if (!\is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } + self::validateHmacKeyLength($keyMaterial, $algorithm); + $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); + return self::constantTimeEquals($hash, $signature); + } + } + + /** + * Decode a JSON string into a PHP object. + * + * @param string $input JSON string + * + * @return mixed The decoded JSON string + * + * @throws DomainException Provided string was invalid JSON + */ + public static function jsonDecode(string $input) + { + $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + + if ($errno = \json_last_error()) { + self::handleJsonError($errno); + } elseif ($obj === null && $input !== 'null') { + throw new DomainException('Null result with non-null input'); + } + return $obj; + } + + /** + * Encode a PHP array into a JSON string. + * + * @param array $input A PHP array + * + * @return string JSON representation of the PHP array + * + * @throws DomainException Provided object could not be encoded to valid JSON + */ + public static function jsonEncode(array $input): string + { + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); + if ($errno = \json_last_error()) { + self::handleJsonError($errno); + } elseif ($json === 'null') { + throw new DomainException('Null result with non-null input'); + } + if ($json === false) { + throw new DomainException('Provided object could not be encoded to valid JSON'); + } + return $json; + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string A decoded string + * + * @throws InvalidArgumentException invalid base64 characters + */ + public static function urlsafeB64Decode(string $input): string + { + return \base64_decode(self::convertBase64UrlToBase64($input)); + } + + /** + * Convert a string in the base64url (URL-safe Base64) encoding to standard base64. + * + * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding) + * + * @return string A Base64 encoded string with standard characters (+/) and padding (=), when + * needed. + * + * @see https://www.rfc-editor.org/rfc/rfc4648 + */ + public static function convertBase64UrlToBase64(string $input): string + { + $remainder = \strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= \str_repeat('=', $padlen); + } + return \strtr($input, '-_', '+/'); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafeB64Encode(string $input): string + { + return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); + } + + /** + * Determine if an algorithm has been provided for each Key + * + * @param Key|ArrayAccess|array $keyOrKeyArray + * @param string|null $kid + * + * @throws UnexpectedValueException + * + * @return Key + */ + private static function getKey( + #[\SensitiveParameter] $keyOrKeyArray, + ?string $kid + ): Key { + if ($keyOrKeyArray instanceof Key) { + return $keyOrKeyArray; + } + + if (empty($kid) && $kid !== '0') { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + + if ($keyOrKeyArray instanceof CachedKeySet) { + // Skip "isset" check, as this will automatically refresh if not set + return $keyOrKeyArray[$kid]; + } + + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + + return $keyOrKeyArray[$kid]; + } + + /** + * @param string $left The string of known length to compare against + * @param string $right The user-supplied string + * @return bool + */ + public static function constantTimeEquals(string $left, string $right): bool + { + if (\function_exists('hash_equals')) { + return \hash_equals($left, $right); + } + $len = \min(self::safeStrlen($left), self::safeStrlen($right)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (\ord($left[$i]) ^ \ord($right[$i])); + } + $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); + + return ($status === 0); + } + + /** + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error() + * + * @throws DomainException + * + * @return void + */ + private static function handleJsonError(int $errno): void + { + $messages = [ + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 + ]; + throw new DomainException( + isset($messages[$errno]) + ? $messages[$errno] + : 'Unknown JSON error: ' . $errno + ); + } + + /** + * Get the number of bytes in cryptographic strings. + * + * @param string $str + * + * @return int + */ + private static function safeStrlen(string $str): int + { + if (\function_exists('mb_strlen')) { + return \mb_strlen($str, '8bit'); + } + return \strlen($str); + } + + /** + * Convert an ECDSA signature to an ASN.1 DER sequence + * + * @param string $sig The ECDSA signature to convert + * @return string The encoded DER object + */ + private static function signatureToDER(string $sig): string + { + // Separate the signature into r-value and s-value + $length = max(1, (int) (\strlen($sig) / 2)); + list($r, $s) = \str_split($sig, $length); + + // Trim leading zeros + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); + + // Convert r-value and s-value from unsigned big-endian integers to + // signed two's complement + if (\ord($r[0]) > 0x7f) { + $r = "\x00" . $r; + } + if (\ord($s[0]) > 0x7f) { + $s = "\x00" . $s; + } + + return self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER(self::ASN1_INTEGER, $r) . + self::encodeDER(self::ASN1_INTEGER, $s) + ); + } + + /** + * Encodes a value into a DER object. + * + * @param int $type DER tag + * @param string $value the value to encode + * + * @return string the encoded object + */ + private static function encodeDER(int $type, string $value): string + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + + // Type + $der = \chr($tag_header | $type); + + // Length + $der .= \chr(\strlen($value)); + + return $der . $value; + } + + /** + * Encodes signature from a DER object. + * + * @param string $der binary signature in DER format + * @param int $keySize the number of bits in the key + * + * @return string the signature + */ + private static function signatureFromDER(string $der, int $keySize): string + { + // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE + list($offset, $_) = self::readDER($der); + list($offset, $r) = self::readDER($der, $offset); + list($offset, $s) = self::readDER($der, $offset); + + // Convert r-value and s-value from signed two's compliment to unsigned + // big-endian integers + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); + + // Pad out r and s so that they are $keySize bits long + $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); + $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); + + return $r . $s; + } + + /** + * Reads binary DER-encoded data and decodes into a single object + * + * @param string $der the binary data in DER format + * @param int $offset the offset of the data stream containing the object + * to decode + * + * @return array{int, string|null} the new offset and the decoded object + */ + private static function readDER(string $der, int $offset = 0): array + { + $pos = $offset; + $size = \strlen($der); + $constructed = (\ord($der[$pos]) >> 5) & 0x01; + $type = \ord($der[$pos++]) & 0x1f; + + // Length + $len = \ord($der[$pos++]); + if ($len & 0x80) { + $n = $len & 0x1f; + $len = 0; + while ($n-- && $pos < $size) { + $len = ($len << 8) | \ord($der[$pos++]); + } + } + + // Value + if ($type === self::ASN1_BIT_STRING) { + $pos++; // Skip the first contents octet (padding indicator) + $data = \substr($der, $pos, $len - 1); + $pos += $len - 1; + } elseif (!$constructed) { + $data = \substr($der, $pos, $len); + $pos += $len; + } else { + $data = null; + } + + return [$pos, $data]; + } + + /** + * Validate HMAC key length + * + * @param string $key HMAC key material + * @param string $algorithm The algorithm + * + * @throws DomainException Provided key is too short + */ + private static function validateHmacKeyLength(string $key, string $algorithm): void + { + $keyLength = \strlen($key) * 8; + $minKeyLength = (int) \str_replace('SHA', '', $algorithm); + if ($keyLength < $minKeyLength) { + throw new DomainException('Provided key is too short'); + } + } + + /** + * Validate RSA key length + * + * @param OpenSSLAsymmetricKey $key RSA key material + * @throws DomainException Provided key is too short + */ + private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymmetricKey $key): void + { + if (!$keyDetails = openssl_pkey_get_details($key)) { + throw new DomainException('Unable to validate key'); + } + if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) { + throw new DomainException('Provided key is too short'); + } + } + + /** + * Validate RSA key length + * + * @param OpenSSLAsymmetricKey $key RSA key material + * @param string $algorithm The algorithm + * @throws DomainException Provided key is too short + */ + private static function validateEcKeyLength( + #[\SensitiveParameter] OpenSSLAsymmetricKey $key, + string $algorithm + ): void { + if (!$keyDetails = openssl_pkey_get_details($key)) { + throw new DomainException('Unable to validate key'); + } + $minKeyLength = (int) \str_replace('ES', '', $algorithm); + if ($keyDetails['bits'] < $minKeyLength) { + throw new DomainException('Provided key is too short'); + } + } + + /** + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial + * @return non-empty-string + */ + private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): string + { + if (!\function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + if (!\is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $keyMaterial)); + $key = self::urlsafeB64Decode((string) end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } + return $key; + } +} diff --git a/lib/Vendor/Firebase/JWT/JWTExceptionWithPayloadInterface.php b/lib/Vendor/Firebase/JWT/JWTExceptionWithPayloadInterface.php new file mode 100644 index 0000000..4315a97 --- /dev/null +++ b/lib/Vendor/Firebase/JWT/JWTExceptionWithPayloadInterface.php @@ -0,0 +1,20 @@ +algorithm; + } + + /** + * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } +} diff --git a/lib/Vendor/Firebase/JWT/SignatureInvalidException.php b/lib/Vendor/Firebase/JWT/SignatureInvalidException.php new file mode 100644 index 0000000..ac13f54 --- /dev/null +++ b/lib/Vendor/Firebase/JWT/SignatureInvalidException.php @@ -0,0 +1,7 @@ + $keys A list of keys that can be obtained in a single operation. + * @param mixed $default Default value to return for keys that do not exist. + * + * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. + * + * @throws \OCA\Talk\Vendor\Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function getMultiple(iterable $keys, mixed $default = null): iterable; + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * @param iterable $values A list of key => value pairs for a multiple-set operation. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \OCA\Talk\Vendor\Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $values is neither an array nor a Traversable, + * or if any of the $values are not a legal value. + */ + public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool; + + /** + * Deletes multiple cache items in a single operation. + * + * @param iterable $keys A list of string-based keys to be deleted. + * + * @return bool True if the items were successfully removed. False if there was an error. + * + * @throws \OCA\Talk\Vendor\Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function deleteMultiple(iterable $keys): bool; + + /** + * Determines whether an item is present in the cache. + * + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * + * @return bool + * + * @throws \OCA\Talk\Vendor\Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function has(string $key): bool; +} diff --git a/lib/Vendor/Psr/SimpleCache/InvalidArgumentException.php b/lib/Vendor/Psr/SimpleCache/InvalidArgumentException.php new file mode 100644 index 0000000..070ff92 --- /dev/null +++ b/lib/Vendor/Psr/SimpleCache/InvalidArgumentException.php @@ -0,0 +1,13 @@ +