diff --git a/build/PHPStan/Build/TurboAttributeCollector.php b/build/PHPStan/Build/TurboAttributeCollector.php index 95477591096..eb7516e4041 100644 --- a/build/PHPStan/Build/TurboAttributeCollector.php +++ b/build/PHPStan/Build/TurboAttributeCollector.php @@ -7,18 +7,30 @@ use Nette\Utils\RegexpException; use Nette\Utils\Strings; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\ArrowFunction; +use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\CallLike; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\List_; +use PhpParser\Node\Expr\Match_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\NullsafeMethodCall; use PhpParser\Node\Expr\NullsafePropertyFetch; +use PhpParser\Node\Expr\PostDec; +use PhpParser\Node\Expr\PostInc; +use PhpParser\Node\Expr\PreDec; +use PhpParser\Node\Expr\PreInc; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Expr\UnaryMinus; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Expr\Yield_; @@ -27,14 +39,26 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Param; use PhpParser\Node\Scalar; +use PhpParser\Node\Scalar\Float_; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Declare_; +use PhpParser\Node\Stmt\Expression; +use PhpParser\Node\Stmt\HaltCompiler; +use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\VariadicPlaceholder; +use PhpParser\Node\VarLikeIdentifier; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionIntersectionType; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -164,6 +188,32 @@ final class TurboAttributeCollector 'conditionalTypeNode' => ConditionalTypeNode::class, 'conditionalTypeForParameterNode' => ConditionalTypeForParameterNode::class, 'reflectionEnum' => ReflectionEnum::class, + 'constFetch' => ConstFetch::class, + 'haltCompiler' => HaltCompiler::class, + 'match' => Match_::class, + 'nullsafeMethodCall' => NullsafeMethodCall::class, + 'staticPropertyFetch' => StaticPropertyFetch::class, + 'classConstFetch' => ClassConstFetch::class, + 'scalarString' => String_::class, + 'scalarInt' => Int_::class, + 'scalarFloat' => Float_::class, + 'varLikeIdentifier' => VarLikeIdentifier::class, + 'listExpr' => List_::class, + 'reflectionEnumBackedCase' => ReflectionEnumBackedCase::class, + 'arg' => Arg::class, + 'param' => Param::class, + 'preInc' => PreInc::class, + 'preDec' => PreDec::class, + 'postInc' => PostInc::class, + 'postDec' => PostDec::class, + 'adapterReflectionMethod' => ReflectionMethod::class, + 'expressionStmt' => Expression::class, + 'assignExpr' => Assign::class, + 'namespaceStmt' => Namespace_::class, + 'declareStmt' => Declare_::class, + 'classMethodStmt' => ClassMethod::class, + 'adapterReflectionClass' => \PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass::class, + 'betterReflectionClass' => \PHPStan\BetterReflection\Reflection\ReflectionClass::class, ]; private string $realRoot; diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index ff4298756fc..ef0637c8ca6 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -18,6 +18,7 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Constant\ConstantArrayType; use function array_is_list; use function array_key_exists; @@ -33,6 +34,7 @@ /** * @api */ +#[ReferencedByTurboExtension(key: 'argumentsNormalizer')] final class ArgumentsNormalizer { diff --git a/src/Analyser/ExprHandlerRegistry.php b/src/Analyser/ExprHandlerRegistry.php index 2205619116f..f1953c148f9 100644 --- a/src/Analyser/ExprHandlerRegistry.php +++ b/src/Analyser/ExprHandlerRegistry.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr; use PHPStan\DependencyInjection\Container; +use PHPStan\Turbo\ReferencedByTurboExtension; use function get_class; use function spl_object_id; @@ -12,6 +13,7 @@ * Expr class so dispatch does not re-scan every tagged handler (a linear * supports() sweep) on each call. */ +#[ReferencedByTurboExtension(key: 'exprHandlerRegistry')] final class ExprHandlerRegistry { diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 25e8958f6d3..44b24a2114c 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -10,6 +10,7 @@ use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\DependencyInjection\GenerateFactory; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ExpressionTypeResolverExtension; use PHPStan\Type\Type; @@ -22,6 +23,7 @@ use function spl_object_id; #[GenerateFactory(interface: ExpressionResultFactory::class)] +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/ExpressionResult.cpp')] final class ExpressionResult { diff --git a/src/Analyser/ExpressionResultStorageStack.php b/src/Analyser/ExpressionResultStorageStack.php index 7c1ac9d4ab2..136a3a31d3f 100644 --- a/src/Analyser/ExpressionResultStorageStack.php +++ b/src/Analyser/ExpressionResultStorageStack.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ShadowedByTurboExtension; use function array_pop; use function count; @@ -24,6 +25,7 @@ * A scope used outside any running analysis simply misses here and resolves * on demand with a throwaway storage. */ +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/ExpressionResultStorageStack.cpp')] final class ExpressionResultStorageStack { diff --git a/src/Analyser/Generics/TemplateArgumentFrame.php b/src/Analyser/Generics/TemplateArgumentFrame.php index 04856d934cf..43ee215721a 100644 --- a/src/Analyser/Generics/TemplateArgumentFrame.php +++ b/src/Analyser/Generics/TemplateArgumentFrame.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ResolvedFunctionVariant; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; @@ -17,6 +18,7 @@ * Immutable template inference context carried by a scope. The observation walk * and the resolved walk use distinct instances, including in saved callbacks. */ +#[ReferencedByTurboExtension(key: 'templateArgumentFrame')] final class TemplateArgumentFrame { diff --git a/src/Analyser/IssetabilityLinkInfo.php b/src/Analyser/IssetabilityLinkInfo.php index a347a8039fd..945fbbfc689 100644 --- a/src/Analyser/IssetabilityLinkInfo.php +++ b/src/Analyser/IssetabilityLinkInfo.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -15,6 +16,7 @@ * (IssetabilityResolution::isSet) and the rule (PHPStan\Rules\IssetCheck) read the * facts instead of re-walking and re-resolving. */ +#[ReferencedByTurboExtension(key: 'issetabilityLinkInfo')] final class IssetabilityLinkInfo { diff --git a/src/Analyser/IssetabilityResolution.php b/src/Analyser/IssetabilityResolution.php index 6a94eeed827..eeb2f2dae72 100644 --- a/src/Analyser/IssetabilityResolution.php +++ b/src/Analyser/IssetabilityResolution.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -11,6 +12,7 @@ * the fold via isSet() and the rule (PHPStan\Rules\IssetCheck) renders messages * from the same links - neither re-walks the AST nor re-resolves types. */ +#[ReferencedByTurboExtension(key: 'issetabilityResolution')] final class IssetabilityResolution { diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 50e475d0286..297692a2b47 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -17,11 +17,13 @@ use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\ClosureType; use PHPStan\Type\ExpressionTypeResolverExtension; use WeakReference; #[GenerateFactory(interface: InternalScopeFactoryFactory::class, resultType: LazyInternalScopeFactory::class)] +#[ReferencedByTurboExtension(key: 'lazyInternalScopeFactory')] final class LazyInternalScopeFactory implements InternalScopeFactory { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6a7b1cdd4ac..28c72dfd9e0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -72,7 +72,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; -use PHPStan\Turbo\ReferencedByTurboExtension; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -147,7 +147,7 @@ use const PHP_INT_MAX; use const PHP_INT_MIN; -#[ReferencedByTurboExtension(key: 'mutatingScope')] +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/MutatingScope.cpp')] class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter { diff --git a/src/Analyser/NodeCallbackScope.php b/src/Analyser/NodeCallbackScope.php index 9e0d4070b8f..0da0934e2b4 100644 --- a/src/Analyser/NodeCallbackScope.php +++ b/src/Analyser/NodeCallbackScope.php @@ -7,12 +7,14 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParameterReflection; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; use WeakReference; use function array_pop; use function count; use function spl_object_id; +#[ReferencedByTurboExtension(key: 'nodeCallbackScope')] final class NodeCallbackScope extends MutatingScope { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 4e3a95004d3..aab9e975177 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -45,6 +45,7 @@ use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -59,6 +60,7 @@ use function sprintf; #[AutowiredService] +#[ReferencedByTurboExtension(key: 'nodeScopeResolver')] class NodeScopeResolver { diff --git a/src/Analyser/Traverser/TransformStaticTypeTraverser.php b/src/Analyser/Traverser/TransformStaticTypeTraverser.php index a01266d7c1c..c16de7f8e3a 100644 --- a/src/Analyser/Traverser/TransformStaticTypeTraverser.php +++ b/src/Analyser/Traverser/TransformStaticTypeTraverser.php @@ -3,11 +3,13 @@ namespace PHPStan\Analyser\Traverser; use PHPStan\Analyser\Scope; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\StaticType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverserCallable; +#[ReferencedByTurboExtension(key: 'transformStaticTypeTraverser')] final class TransformStaticTypeTraverser implements TypeTraverserCallable { diff --git a/src/Analyser/Traverser/VoidToNullTraverser.php b/src/Analyser/Traverser/VoidToNullTraverser.php index 64f0ed9b74b..d502b90605f 100644 --- a/src/Analyser/Traverser/VoidToNullTraverser.php +++ b/src/Analyser/Traverser/VoidToNullTraverser.php @@ -2,11 +2,13 @@ namespace PHPStan\Analyser\Traverser; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverserCallable; use PHPStan\Type\UnionType; +#[ReferencedByTurboExtension(key: 'voidToNullTraverser')] final class VoidToNullTraverser implements TypeTraverserCallable { diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index 07d616f3edf..b480f474256 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -3,10 +3,12 @@ namespace PHPStan\Analyser; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * @api */ +#[ReferencedByTurboExtension(key: 'typeSpecifierContext')] final class TypeSpecifierContext { diff --git a/src/Analyser/UndefinedVariableException.php b/src/Analyser/UndefinedVariableException.php index a6e805d69ab..3dc2408a073 100644 --- a/src/Analyser/UndefinedVariableException.php +++ b/src/Analyser/UndefinedVariableException.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\AnalysedCodeException; +use PHPStan\Turbo\ReferencedByTurboExtension; use function sprintf; /** @@ -11,6 +12,7 @@ * Unchecked exception thrown from `PHPStan\Analyser\Scope::getVariableType()` * in case the user doesn't check `hasVariableType()` is not `no()`. */ +#[ReferencedByTurboExtension(key: 'undefinedVariableException')] final class UndefinedVariableException extends AnalysedCodeException { diff --git a/src/Analyser/VariableAccessFlow.php b/src/Analyser/VariableAccessFlow.php index 5cdb42fb95e..82a45a65e7d 100644 --- a/src/Analyser/VariableAccessFlow.php +++ b/src/Analyser/VariableAccessFlow.php @@ -3,8 +3,10 @@ namespace PHPStan\Analyser; use PHPStan\Node\Variable\VariableWrite; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; +#[ReferencedByTurboExtension(key: 'variableAccessFlow')] final class VariableAccessFlow extends VariableFlow { diff --git a/src/Analyser/VariableControlFlow.php b/src/Analyser/VariableControlFlow.php index 0a18bb5fc9a..196aafcae93 100644 --- a/src/Analyser/VariableControlFlow.php +++ b/src/Analyser/VariableControlFlow.php @@ -6,8 +6,10 @@ use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PHPStan\Node\Variable\VariableWrite; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; +#[ReferencedByTurboExtension(key: 'variableControlFlow')] final class VariableControlFlow extends VariableFlow { diff --git a/src/Analyser/VariableFlow.php b/src/Analyser/VariableFlow.php index 24b6b451b59..eec646890b4 100644 --- a/src/Analyser/VariableFlow.php +++ b/src/Analyser/VariableFlow.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PHPStan\Node\Variable\VariableWrite; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Type; use function count; use function in_array; @@ -14,6 +15,7 @@ * Immutable source execution fragment, composed with expression and statement * results. Liveness is resolved at the body boundary, independently of types. */ +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/VariableFlow.cpp')] abstract class VariableFlow { diff --git a/src/Analyser/VariableFlowBuilder.php b/src/Analyser/VariableFlowBuilder.php index b9cc5f13a45..d1a2c55b713 100644 --- a/src/Analyser/VariableFlowBuilder.php +++ b/src/Analyser/VariableFlowBuilder.php @@ -5,12 +5,14 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Node\Variable\VariableWrite; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Type; use function in_array; use function is_string; use function spl_object_id; /** Compose variable flow for assignment targets and arguments. */ +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/VariableFlowBuilder.cpp')] final class VariableFlowBuilder { diff --git a/src/Analyser/VariableInputFlow.php b/src/Analyser/VariableInputFlow.php index ab60b383e2c..2bd406f9a2e 100644 --- a/src/Analyser/VariableInputFlow.php +++ b/src/Analyser/VariableInputFlow.php @@ -2,6 +2,9 @@ namespace PHPStan\Analyser; +use PHPStan\Turbo\ReferencedByTurboExtension; + +#[ReferencedByTurboExtension(key: 'variableInputFlow')] final class VariableInputFlow extends VariableFlow { diff --git a/src/Analyser/VariableLivenessResolver.php b/src/Analyser/VariableLivenessResolver.php index a6e0e85afc2..25e685ef750 100644 --- a/src/Analyser/VariableLivenessResolver.php +++ b/src/Analyser/VariableLivenessResolver.php @@ -8,6 +8,7 @@ use PHPStan\Node\Variable\VariableWrite; use PHPStan\Node\VariableWritesNode; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use Throwable; @@ -27,6 +28,7 @@ use function substr; /** Resolve liveness backwards over immutable body fragments. */ +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/VariableLivenessResolver.cpp')] final class VariableLivenessResolver { diff --git a/src/Analyser/VariableSequenceFlow.php b/src/Analyser/VariableSequenceFlow.php index 965aeea8430..bfafd7b0e25 100644 --- a/src/Analyser/VariableSequenceFlow.php +++ b/src/Analyser/VariableSequenceFlow.php @@ -2,6 +2,9 @@ namespace PHPStan\Analyser; +use PHPStan\Turbo\ReferencedByTurboExtension; + +#[ReferencedByTurboExtension(key: 'variableSequenceFlow')] final class VariableSequenceFlow extends VariableFlow { diff --git a/src/Analyser/VariableWriteOffset.php b/src/Analyser/VariableWriteOffset.php index 4c7c5b428b5..b03c32b32b5 100644 --- a/src/Analyser/VariableWriteOffset.php +++ b/src/Analyser/VariableWriteOffset.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; use function count; use function is_int; @@ -12,6 +13,7 @@ * * @internal */ +#[ReferencedByTurboExtension(key: 'variableWriteOffset')] final class VariableWriteOffset { diff --git a/src/Analyser/VolatileExpressionHelper.php b/src/Analyser/VolatileExpressionHelper.php index f3118e6faa8..b54b2767abc 100644 --- a/src/Analyser/VolatileExpressionHelper.php +++ b/src/Analyser/VolatileExpressionHelper.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; +use PHPStan\Turbo\ShadowedByTurboExtension; use function array_key_exists; use function array_keys; use function count; @@ -12,6 +13,7 @@ use function str_starts_with; use function strtolower; +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/VolatileExpressionHelper.cpp')] final class VolatileExpressionHelper { diff --git a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php index 680376ba533..e12edbd599f 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -18,9 +18,11 @@ use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ReferencedByTurboExtension; use function array_merge; #[AutowiredService(as: ClassReflectionExtensionRegistryProvider::class)] +#[ReferencedByTurboExtension(key: 'lazyClassReflectionExtensionRegistryProvider')] final class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider { diff --git a/src/Node/EmitCollectedDataNode.php b/src/Node/EmitCollectedDataNode.php index b8b235adefd..f468ad342c0 100644 --- a/src/Node/EmitCollectedDataNode.php +++ b/src/Node/EmitCollectedDataNode.php @@ -6,11 +6,13 @@ use PhpParser\Node; use PhpParser\NodeAbstract; use PHPStan\Collectors\Collector; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * @template TNodeType of Node * @template TValue */ +#[ReferencedByTurboExtension(key: 'emitCollectedDataNode')] final class EmitCollectedDataNode extends NodeAbstract implements VirtualNode { diff --git a/src/Node/Expr/AlwaysRememberedExpr.php b/src/Node/Expr/AlwaysRememberedExpr.php index f389fac356b..10aa7478389 100644 --- a/src/Node/Expr/AlwaysRememberedExpr.php +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -5,9 +5,11 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** Wraps an expression so its type is always remembered in the scope, bypassing impurity checks. */ +#[ReferencedByTurboExtension(key: 'alwaysRememberedExpr')] final class AlwaysRememberedExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/CloneReinitializationExpr.php b/src/Node/Expr/CloneReinitializationExpr.php index 4bac55ec156..b97961cecda 100644 --- a/src/Node/Expr/CloneReinitializationExpr.php +++ b/src/Node/Expr/CloneReinitializationExpr.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * Tracks that a readonly property has been re-assigned within the current __clone() body. @@ -17,6 +18,7 @@ * __clone, and is excluded from rememberConstructorExpressions() so it never leaks into * __clone's entry scope. */ +#[ReferencedByTurboExtension(key: 'cloneReinitializationExpr')] final class CloneReinitializationExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/NativeTypeExpr.php b/src/Node/Expr/NativeTypeExpr.php index b3160ed79d6..c2bb29f92af 100644 --- a/src/Node/Expr/NativeTypeExpr.php +++ b/src/Node/Expr/NativeTypeExpr.php @@ -5,11 +5,13 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** * @api */ +#[ReferencedByTurboExtension(key: 'nativeTypeExpr')] final class NativeTypeExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/OriginalForeachKeyExpr.php b/src/Node/Expr/OriginalForeachKeyExpr.php index 3db12ab2eaa..04d85e5f6ae 100644 --- a/src/Node/Expr/OriginalForeachKeyExpr.php +++ b/src/Node/Expr/OriginalForeachKeyExpr.php @@ -5,7 +5,9 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'originalForeachKeyExpr')] final class OriginalForeachKeyExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/OriginalForeachValueExpr.php b/src/Node/Expr/OriginalForeachValueExpr.php index 3f196f0c8aa..63fd09d9e09 100644 --- a/src/Node/Expr/OriginalForeachValueExpr.php +++ b/src/Node/Expr/OriginalForeachValueExpr.php @@ -5,7 +5,9 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'originalForeachValueExpr')] final class OriginalForeachValueExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/ParameterVariableOriginalValueExpr.php b/src/Node/Expr/ParameterVariableOriginalValueExpr.php index 78c94cf2d90..6b7ca8ba73e 100644 --- a/src/Node/Expr/ParameterVariableOriginalValueExpr.php +++ b/src/Node/Expr/ParameterVariableOriginalValueExpr.php @@ -5,7 +5,9 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'parameterVariableOriginalValueExpr')] final class ParameterVariableOriginalValueExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/PossiblyImpureCallExpr.php b/src/Node/Expr/PossiblyImpureCallExpr.php index 8463238246f..b3bd7b544ab 100644 --- a/src/Node/Expr/PossiblyImpureCallExpr.php +++ b/src/Node/Expr/PossiblyImpureCallExpr.php @@ -5,7 +5,9 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'possiblyImpureCallExpr')] final class PossiblyImpureCallExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/PropertyInitializationExpr.php b/src/Node/Expr/PropertyInitializationExpr.php index 539d928f4a2..8761c6ee38d 100644 --- a/src/Node/Expr/PropertyInitializationExpr.php +++ b/src/Node/Expr/PropertyInitializationExpr.php @@ -5,7 +5,9 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'propertyInitializationExpr')] final class PropertyInitializationExpr extends Expr implements VirtualNode { diff --git a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php index a152f9f3291..39c6c1c2d82 100644 --- a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php +++ b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php @@ -5,7 +5,9 @@ use Override; use PhpParser\Node\Expr; use PHPStan\Node\VirtualNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'setExistingOffsetValueTypeExpr')] final class SetExistingOffsetValueTypeExpr extends Expr implements VirtualNode { diff --git a/src/Node/IssetExpr.php b/src/Node/IssetExpr.php index 9aa4ed2c8bd..09a9ebfc0d1 100644 --- a/src/Node/IssetExpr.php +++ b/src/Node/IssetExpr.php @@ -4,10 +4,12 @@ use Override; use PhpParser\Node\Expr; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * @api */ +#[ReferencedByTurboExtension(key: 'issetExpr')] final class IssetExpr extends Expr implements VirtualNode { diff --git a/src/Node/Variable/VariableWrite.php b/src/Node/Variable/VariableWrite.php index 7cb13e68c42..d6668679871 100644 --- a/src/Node/Variable/VariableWrite.php +++ b/src/Node/Variable/VariableWrite.php @@ -3,6 +3,7 @@ namespace PHPStan\Node\Variable; use PhpParser\Node; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * A write site of a local variable inside a function-like body. @@ -12,6 +13,7 @@ * * @api */ +#[ReferencedByTurboExtension(key: 'variableWrite')] final class VariableWrite { diff --git a/src/Node/VariableWritesNode.php b/src/Node/VariableWritesNode.php index ddf52f4486a..e739806c6b9 100644 --- a/src/Node/VariableWritesNode.php +++ b/src/Node/VariableWritesNode.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt\Foreach_; use PhpParser\NodeAbstract; use PHPStan\Node\Variable\VariableWrite; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -21,6 +22,7 @@ * * @api */ +#[ReferencedByTurboExtension(key: 'variableWritesNode')] final class VariableWritesNode extends NodeAbstract implements VirtualNode { diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 55dd59442fa..5a3e290ce11 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -3,6 +3,7 @@ namespace PHPStan\Php; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; @@ -17,6 +18,7 @@ * * @api */ +#[ReferencedByTurboExtension(key: 'phpVersions')] final class PhpVersions { diff --git a/src/Reflection/Attribute/PrivateProperty.php b/src/Reflection/Attribute/PrivateProperty.php index 90cbf9dab92..0e12fd5f4bf 100644 --- a/src/Reflection/Attribute/PrivateProperty.php +++ b/src/Reflection/Attribute/PrivateProperty.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\Attribute; use Attribute; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * Marks a property the phar build made public that is private in the @@ -13,6 +14,7 @@ * the phar cannot access it. */ #[Attribute(flags: Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] +#[ReferencedByTurboExtension(key: 'privatePropertyAttribute')] final class PrivateProperty { diff --git a/src/Reflection/Attribute/ProtectedProperty.php b/src/Reflection/Attribute/ProtectedProperty.php index 7836ed471c5..9e933448907 100644 --- a/src/Reflection/Attribute/ProtectedProperty.php +++ b/src/Reflection/Attribute/ProtectedProperty.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\Attribute; use Attribute; +use PHPStan\Turbo\ReferencedByTurboExtension; /** * Marks a property the phar build made public that is protected in the @@ -13,6 +14,7 @@ * the phar cannot access it from outside the class hierarchy. */ #[Attribute(flags: Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] +#[ReferencedByTurboExtension(key: 'protectedPropertyAttribute')] final class ProtectedProperty { diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index d9bbb270c2f..f504ae26b52 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -38,7 +38,7 @@ use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; -use PHPStan\Turbo\ReferencedByTurboExtension; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\CircularTypeAliasDefinitionException; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; @@ -83,7 +83,7 @@ * @api */ #[GenerateFactory(interface: ClassReflectionFactory::class)] -#[ReferencedByTurboExtension(key: 'classReflection')] +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/ClassReflection.cpp')] final class ClassReflection { diff --git a/src/Reflection/ClassReflectionExtensionRegistry.php b/src/Reflection/ClassReflectionExtensionRegistry.php index dd2a7d7b699..fbee6e31f73 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -5,7 +5,9 @@ use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'classReflectionExtensionRegistry')] final class ClassReflectionExtensionRegistry { diff --git a/src/Reflection/EnumCaseReflection.php b/src/Reflection/EnumCaseReflection.php index 94cced27392..3836bdc98b1 100644 --- a/src/Reflection/EnumCaseReflection.php +++ b/src/Reflection/EnumCaseReflection.php @@ -7,12 +7,14 @@ use PHPStan\Internal\DeprecatedAttributeHelper; use PHPStan\Reflection\Deprecation\DeprecationProvider; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Type; /** * @api */ +#[ReferencedByTurboExtension(key: 'enumCaseReflection')] final class EnumCaseReflection { diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 18129b6a919..9e9898e258e 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -4,6 +4,7 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -28,6 +29,7 @@ * @api * @api-do-not-implement */ +#[ReferencedByTurboExtension(key: 'extendedMethodReflection')] interface ExtendedMethodReflection extends MethodReflection { diff --git a/src/Reflection/ExtendedParametersAcceptor.php b/src/Reflection/ExtendedParametersAcceptor.php index 57b838c232c..df5122e0c66 100644 --- a/src/Reflection/ExtendedParametersAcceptor.php +++ b/src/Reflection/ExtendedParametersAcceptor.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; @@ -19,6 +20,7 @@ * @api * @api-do-not-implement */ +#[ReferencedByTurboExtension(key: 'extendedParametersAcceptor')] interface ExtendedParametersAcceptor extends ParametersAcceptor { diff --git a/src/Reflection/ExtendedPropertyReflection.php b/src/Reflection/ExtendedPropertyReflection.php index df2d8c100ad..86fd7d0bcbb 100644 --- a/src/Reflection/ExtendedPropertyReflection.php +++ b/src/Reflection/ExtendedPropertyReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -26,6 +27,7 @@ * @api * @api-do-not-implement */ +#[ReferencedByTurboExtension(key: 'extendedPropertyReflection')] interface ExtendedPropertyReflection extends PropertyReflection { diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index f9c7cefa520..51f8ad4146e 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -21,6 +22,7 @@ * @api * @api-do-not-implement */ +#[ReferencedByTurboExtension(key: 'functionReflection')] interface FunctionReflection { diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index 59ae90d27b9..5a80eb84eb6 100644 --- a/src/Reflection/InitializerExprContext.php +++ b/src/Reflection/InitializerExprContext.php @@ -11,6 +11,7 @@ use PHPStan\BetterReflection\Reflection\ReflectionConstant; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Turbo\ReferencedByTurboExtension; use function array_slice; use function count; use function explode; @@ -20,6 +21,7 @@ /** * @api */ +#[ReferencedByTurboExtension(key: 'initializerExprContext')] final class InitializerExprContext implements NamespaceAnswerer { diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 524fa98bfc1..6dac7c62880 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; /** @@ -20,6 +21,7 @@ * * @api */ +#[ReferencedByTurboExtension(key: 'methodReflection')] interface MethodReflection extends ClassMemberReflection { diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index d4957240fd1..2b85a28c886 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -8,9 +8,11 @@ use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +#[ReferencedByTurboExtension(key: 'extendedNativeParameterReflection')] final class ExtendedNativeParameterReflection implements ExtendedParameterReflection { diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index d614bda8811..298780447a7 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -14,12 +14,14 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use ReflectionException; use function count; use function strtolower; +#[ReferencedByTurboExtension(key: 'nativeMethodReflection')] final class NativeMethodReflection implements ExtendedMethodReflection { diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 3f829984d8a..40c170ba720 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -11,10 +11,12 @@ use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +#[ReferencedByTurboExtension(key: 'enumCasesMethodReflection')] final class EnumCasesMethodReflection implements ExtendedMethodReflection { diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 80874d54ae0..a047011fd93 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -46,6 +46,7 @@ use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\ArrayType; @@ -78,6 +79,7 @@ use function strtolower; #[AutowiredService] +#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/PhpClassReflectionExtension.cpp')] final class PhpClassReflectionExtension { diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index ef46fe63ac1..557cc125785 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -17,6 +17,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; @@ -30,6 +31,7 @@ /** * @api */ +#[ReferencedByTurboExtension(key: 'phpFunctionFromParserNodeReflection')] class PhpFunctionFromParserNodeReflection implements FunctionReflection, ExtendedParametersAcceptor { diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 290e0cb4b76..8a5a3ebc2ff 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -13,6 +13,7 @@ use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -32,6 +33,7 @@ /** * @api */ +#[ReferencedByTurboExtension(key: 'phpMethodFromParserNodeReflection')] final class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements ExtendedMethodReflection { diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index 8b30efb00ba..5077a6e8855 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; @@ -19,6 +20,7 @@ /** * @api */ +#[ReferencedByTurboExtension(key: 'phpPropertyReflection')] final class PhpPropertyReflection implements ExtendedPropertyReflection { diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php index c565feb0811..5316dc05687 100644 --- a/src/Reflection/RealClassClassConstantReflection.php +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -7,10 +7,12 @@ use PHPStan\Internal\DeprecatedAttributeHelper; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use function sprintf; +#[ReferencedByTurboExtension(key: 'realClassClassConstantReflection')] final class RealClassClassConstantReflection implements ClassConstantReflection { diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 79874e29186..04db875d3ef 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -5,11 +5,13 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_map; +#[ReferencedByTurboExtension(key: 'wrappedExtendedMethodReflection')] final class WrappedExtendedMethodReflection implements ExtendedMethodReflection { diff --git a/src/Reflection/WrappedExtendedPropertyReflection.php b/src/Reflection/WrappedExtendedPropertyReflection.php index c2e946c5585..a3b26b0d1e2 100644 --- a/src/Reflection/WrappedExtendedPropertyReflection.php +++ b/src/Reflection/WrappedExtendedPropertyReflection.php @@ -4,9 +4,11 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +#[ReferencedByTurboExtension(key: 'wrappedExtendedPropertyReflection')] final class WrappedExtendedPropertyReflection implements ExtendedPropertyReflection { diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 9db73c11042..e3ab1b11576 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '500bbe7'; + public const EXPECTED_EXTENSION_VERSION = 'd2409b8'; private static bool $active = false; diff --git a/src/Type/CircularTypeAliasDefinitionException.php b/src/Type/CircularTypeAliasDefinitionException.php index d0502cb9a1b..180302e04b5 100644 --- a/src/Type/CircularTypeAliasDefinitionException.php +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -3,7 +3,9 @@ namespace PHPStan\Type; use Exception; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'circularTypeAliasDefinitionException')] final class CircularTypeAliasDefinitionException extends Exception { diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php index 17bd6373e57..43798ff1435 100644 --- a/src/Type/TypeAlias.php +++ b/src/Type/TypeAlias.php @@ -6,7 +6,9 @@ use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Turbo\ReferencedByTurboExtension; +#[ReferencedByTurboExtension(key: 'typeAlias')] final class TypeAlias { diff --git a/turbo-ext/bin/side-by-side.php b/turbo-ext/bin/side-by-side.php index 2ccd74f1cf6..df2483b590e 100644 --- a/turbo-ext/bin/side-by-side.php +++ b/turbo-ext/bin/side-by-side.php @@ -680,10 +680,18 @@ function checkStructure(array $manifest): array $cppClasses = []; $declaredNames = []; foreach (array_merge(glob('turbo-ext/src/*.cpp'), glob('turbo-ext/src/parser/*.cpp')) as $file) { - preg_match_all('~reg::Class\s+\w+\("((?:PHPStan|PhpParser)\\\\[^"]+)"\)~', file_get_contents($file), $m); + $source = file_get_contents($file); + preg_match_all('~reg::Class\s+\w+\("((?:PHPStan|PhpParser)\\\\[^"]+)"\)~', $source, $m); if ($m[1] === []) { continue; } + // A port awaiting its flip declares its class differential-only + // (reg::Class::shadowDifferentialOnly()): the prefixed harness + // compares it against the twin while the twin stays the live class, + // so no attribute names it yet — parity is checked once it lands. + if (str_contains($source, 'shadowDifferentialOnly(')) { + continue; + } $cppClasses[] = basename($file, '.cpp'); foreach ($m[1] as $declared) { $declaredNames[basename($file, '.cpp')][] = stripslashes($declared); diff --git a/turbo-ext/config.w32 b/turbo-ext/config.w32 index 68e528fc501..98a9497c1aa 100644 --- a/turbo-ext/config.w32 +++ b/turbo-ext/config.w32 @@ -29,7 +29,7 @@ if (PHP_PHPSTAN_TURBO != "no") { // directory by splitting on backslashes only — with forward slashes the // objects compile flat while the link list expects the subpath EXTENSION("phpstan_turbo", - "src\\main.cpp src\\support.cpp src\\AbsorbedTemplateArgumentType.cpp src\\AcceptsResult.cpp src\\AccessoryArrayListType.cpp src\\AccessoryDecimalIntegerStringType.cpp src\\AccessoryLiteralStringType.cpp src\\AccessoryLowercaseStringType.cpp src\\AccessoryNonEmptyStringType.cpp src\\AccessoryNonFalsyStringType.cpp src\\AccessoryNumericStringType.cpp src\\AccessoryUppercaseStringType.cpp src\\ArenaCache.cpp src\\ArrayType.cpp src\\BenevolentUnionType.cpp src\\BooleanType.cpp src\\CallableType.cpp src\\CallableTypeHelper.cpp src\\CallbackUnresolvedMethodPrototypeReflection.cpp src\\CallbackUnresolvedPropertyPrototypeReflection.cpp src\\CalledOnTypeUnresolvedMethodPrototypeReflection.cpp src\\CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp src\\CircularTypeAliasErrorType.cpp src\\ClassConstantAccessType.cpp src\\ClassReflectionAccess.cpp src\\ClassStringType.cpp src\\ClosureType.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ConditionalType.cpp src\\ConditionalTypeForParameter.cpp src\\ConstantArrayType.cpp src\\ConstantArrayTypeBuilder.cpp src\\ConstantBooleanType.cpp src\\ConstantFloatType.cpp src\\ConstantIntegerType.cpp src\\ConstantStringType.cpp src\\ConstantTypeHelper.cpp src\\EnumCaseObjectType.cpp src\\ErrorType.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\FiniteTypeSet.cpp src\\FloatType.cpp src\\GenericClassStringType.cpp src\\GenericObjectType.cpp src\\GenericStaticType.cpp src\\GetTemplateTypeType.cpp src\\HasMethodType.cpp src\\HasOffsetType.cpp src\\HasOffsetValueType.cpp src\\HasPropertyType.cpp src\\IntegerRangeType.cpp src\\IntegerType.cpp src\\IntersectionType.cpp src\\IsSuperTypeOfResult.cpp src\\IterableType.cpp src\\KeyOfType.cpp src\\LateResolvableArrayShapeType.cpp src\\LruCache.cpp src\\MixedType.cpp src\\NativeParameterReflection.cpp src\\NeverType.cpp src\\NewObjectType.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\NonAcceptingNeverType.cpp src\\NonEmptyArrayType.cpp src\\NonexistentParentClassType.cpp src\\NullType.cpp src\\ObjectShapeType.cpp src\\ObjectType.cpp src\\ObjectWithoutClassType.cpp src\\OffsetAccessType.cpp src\\OversizedArrayType.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\RecursionGuard.cpp src\\ReflectionAccess.cpp src\\ResourceType.cpp src\\ScopeContext.cpp src\\ScopeOps.cpp src\\Shadow.cpp src\\StaticType.cpp src\\StaticTypeFactory.cpp src\\StrictMixedType.cpp src\\StringAlwaysAcceptingObjectWithToStringType.cpp src\\StringNeverAcceptingObjectWithToStringType.cpp src\\StringType.cpp src\\SymbolFinderInFiles.cpp src\\TemplateArrayType.cpp src\\TemplateBenevolentUnionType.cpp src\\TemplateBooleanType.cpp src\\TemplateConstantArrayType.cpp src\\TemplateConstantIntegerType.cpp src\\TemplateConstantStringType.cpp src\\TemplateFloatType.cpp src\\TemplateGenericObjectType.cpp src\\TemplateIntegerType.cpp src\\TemplateIntersectionType.cpp src\\TemplateIterableType.cpp src\\TemplateKeyOfType.cpp src\\TemplateMixedType.cpp src\\TemplateNullType.cpp src\\TemplateObjectShapeType.cpp src\\TemplateObjectType.cpp src\\TemplateObjectWithoutClassType.cpp src\\TemplateStrictMixedType.cpp src\\TemplateStringType.cpp src\\TemplateTypeArgumentStrategy.cpp src\\TemplateTypeFactory.cpp src\\TemplateTypeHelper.cpp src\\TemplateTypeMap.cpp src\\TemplateTypeParameterStrategy.cpp src\\TemplateTypeReference.cpp src\\TemplateTypeScope.cpp src\\TemplateTypeVariance.cpp src\\TemplateTypeVarianceMap.cpp src\\TemplateUnionType.cpp src\\ThisType.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinator.cpp src\\TypeCombinatorCache.cpp src\\TypehintHelper.cpp src\\TypeOps.cpp src\\TypeProjectionHelper.cpp src\\TypeResult.cpp src\\TypeTraits.cpp src\\TypeTraverser.cpp src\\TypeUtils.cpp src\\UnionType.cpp src\\UnionTypeHelper.cpp src\\UnresolvableTypeHelper.cpp src\\UnresolvedTemplateArgumentType.cpp src\\ValueOfType.cpp src\\VerbosityLevel.cpp src\\VoidType.cpp", + "src\\main.cpp src\\support.cpp src\\AbsorbedTemplateArgumentType.cpp src\\AcceptsResult.cpp src\\AccessoryArrayListType.cpp src\\AccessoryDecimalIntegerStringType.cpp src\\AccessoryLiteralStringType.cpp src\\AccessoryLowercaseStringType.cpp src\\AccessoryNonEmptyStringType.cpp src\\AccessoryNonFalsyStringType.cpp src\\AccessoryNumericStringType.cpp src\\AccessoryUppercaseStringType.cpp src\\ArenaCache.cpp src\\ArrayType.cpp src\\BenevolentUnionType.cpp src\\BooleanType.cpp src\\CallableType.cpp src\\CallableTypeHelper.cpp src\\CallbackUnresolvedMethodPrototypeReflection.cpp src\\CallbackUnresolvedPropertyPrototypeReflection.cpp src\\CalledOnTypeUnresolvedMethodPrototypeReflection.cpp src\\CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp src\\CircularTypeAliasErrorType.cpp src\\ClassConstantAccessType.cpp src\\ClassReflection.cpp src\\ClassStringType.cpp src\\ClosureType.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ConditionalType.cpp src\\ConditionalTypeForParameter.cpp src\\ConstantArrayType.cpp src\\ConstantArrayTypeBuilder.cpp src\\ConstantBooleanType.cpp src\\ConstantFloatType.cpp src\\ConstantIntegerType.cpp src\\ConstantStringType.cpp src\\ConstantTypeHelper.cpp src\\EnumCaseObjectType.cpp src\\ErrorType.cpp src\\ExpressionResult.cpp src\\ExpressionResultStorage.cpp src\\ExpressionResultStorageStack.cpp src\\ExpressionTypeHolder.cpp src\\FiniteTypeSet.cpp src\\FloatType.cpp src\\GenericClassStringType.cpp src\\GenericObjectType.cpp src\\GenericStaticType.cpp src\\GetTemplateTypeType.cpp src\\HasMethodType.cpp src\\HasOffsetType.cpp src\\HasOffsetValueType.cpp src\\HasPropertyType.cpp src\\IntegerRangeType.cpp src\\IntegerType.cpp src\\IntersectionType.cpp src\\IsSuperTypeOfResult.cpp src\\IterableType.cpp src\\KeyOfType.cpp src\\LateResolvableArrayShapeType.cpp src\\LruCache.cpp src\\MixedType.cpp src\\MutatingScope.cpp src\\NativeParameterReflection.cpp src\\NeverType.cpp src\\NewObjectType.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\NonAcceptingNeverType.cpp src\\NonEmptyArrayType.cpp src\\NonexistentParentClassType.cpp src\\NullType.cpp src\\ObjectShapeType.cpp src\\ObjectType.cpp src\\ObjectWithoutClassType.cpp src\\OffsetAccessType.cpp src\\OversizedArrayType.cpp src\\PharForkGuard.cpp src\\PhpClassReflectionExtension.cpp src\\PhpFileCleaner.cpp src\\RecursionGuard.cpp src\\ReflectionAccess.cpp src\\ResourceType.cpp src\\ScopeContext.cpp src\\ScopeOps.cpp src\\Shadow.cpp src\\StaticType.cpp src\\StaticTypeFactory.cpp src\\StrictMixedType.cpp src\\StringAlwaysAcceptingObjectWithToStringType.cpp src\\StringNeverAcceptingObjectWithToStringType.cpp src\\StringType.cpp src\\SymbolFinderInFiles.cpp src\\TemplateArrayType.cpp src\\TemplateBenevolentUnionType.cpp src\\TemplateBooleanType.cpp src\\TemplateConstantArrayType.cpp src\\TemplateConstantIntegerType.cpp src\\TemplateConstantStringType.cpp src\\TemplateFloatType.cpp src\\TemplateGenericObjectType.cpp src\\TemplateIntegerType.cpp src\\TemplateIntersectionType.cpp src\\TemplateIterableType.cpp src\\TemplateKeyOfType.cpp src\\TemplateMixedType.cpp src\\TemplateNullType.cpp src\\TemplateObjectShapeType.cpp src\\TemplateObjectType.cpp src\\TemplateObjectWithoutClassType.cpp src\\TemplateStrictMixedType.cpp src\\TemplateStringType.cpp src\\TemplateTypeArgumentStrategy.cpp src\\TemplateTypeFactory.cpp src\\TemplateTypeHelper.cpp src\\TemplateTypeMap.cpp src\\TemplateTypeParameterStrategy.cpp src\\TemplateTypeReference.cpp src\\TemplateTypeScope.cpp src\\TemplateTypeVariance.cpp src\\TemplateTypeVarianceMap.cpp src\\TemplateUnionType.cpp src\\ThisType.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinator.cpp src\\TypeCombinatorCache.cpp src\\TypehintHelper.cpp src\\TypeOps.cpp src\\TypeProjectionHelper.cpp src\\TypeResult.cpp src\\TypeTraits.cpp src\\TypeTraverser.cpp src\\TypeUtils.cpp src\\UnionType.cpp src\\UnionTypeHelper.cpp src\\UnresolvableTypeHelper.cpp src\\UnresolvedTemplateArgumentType.cpp src\\ValueOfType.cpp src\\VariableFlow.cpp src\\VariableFlowBuilder.cpp src\\VariableLivenessResolver.cpp src\\VerbosityLevel.cpp src\\VoidType.cpp src\\VolatileExpressionHelper.cpp", true, flags); ADD_SOURCES(configure_module_dirname + "/src/parser", diff --git a/turbo-ext/src/ClassReflection.cpp b/turbo-ext/src/ClassReflection.cpp new file mode 100644 index 00000000000..70adaf83f6f --- /dev/null +++ b/turbo-ext/src/ClassReflection.cpp @@ -0,0 +1,4882 @@ +/* + * PHPStanTurbo\ClassReflection — native implementation of + * PHPStan\Reflection\ClassReflection, declared as that class itself at + * activation (reg::Class::shadow(), final like the twin). The seven + * getters the Type kernel calls millions of times per run + * (getName/getCacheKey/getNativeReflection/isGeneric/hasMethod/ + * hasFinalByKeywordOverride/isEnum) are re-exported below the class as + * pt_class_reflection_*() — a direct call into the native body, the PHP + * method for a foreign object. + * + * Design + * ------ + * Class shape. The twin is final: no PHP subclass exists, so every + * `$this->method()` is a direct C++ call — no Z_OBJCE dispatch, no handler + * identity checks. The DI container never instantiates this class directly: + * the generated ClassReflectionFactory does (GenerateFactory), reflecting + * the constructor for the eleven autowired services — the arginfo therefore + * declares the twin's exact parameter class names (README rule 6). + * + * Layout. The twin's properties are declared typed property slots in the + * twin's declaration order: the 35 class-body memo properties first (with + * their defaults — [] / null / false, exactly the twin's), the static + * $resolvingTypeAliasImports in its place (no instance slot), then the 19 + * promoted constructor properties in parameter order, uninitialized until + * the constructor writes them. The std object handlers do GC/clone/free. + * The names are load-bearing: the differential harness reads every slot by + * reflection to compare the memo state of both sides. + * + * Collaborators. The eleven injected services (ClassReflectionFactory, + * ReflectionProvider, InitializerExprTypeResolver, FileTypeMapper, + * StubPhpDocProvider, PhpDocInheritanceResolver, PhpVersion, + * SignatureMapProvider, DeprecationProvider, AttributeReflectionFactory, + * ClassReflectionExtensionRegistryProvider) and the BetterReflection + * adapter ($reflection) are PHP objects held in their slots and called by + * name (pt_type_call). The reflection provider goes through the slot + * readers of ReflectionAccess.cpp (a memoizing provider answers from its + * cache), a ClassMemberAccessAnswerer scope through + * pt_scope_get_class_reflection() of ScopeContext.cpp (a MutatingScope's + * context slot). Other + * ClassReflection instances (parents, interfaces, the provider's answers) + * are called directly when they are exactly this class and by name + * otherwise (crCall()) — under the prefixed harness they are PHP twins. + * The Type kernel is reached natively: ObjectType::getClassReflection(), + * TemplateTypeHelper::resolveTemplateTypes()/resolveToDefaults(), + * TypeProjectionHelper::describe(), TypehintHelper::decideTypeFromReflection(), + * `new ErrorType()`, `new ObjectType()` / `new GenericObjectType()`, `new + * TemplateTypeMap()` / `new TemplateTypeVarianceMap()`; the VerbosityLevel + * and TemplateTypeVariance singletons — and the two statics that classify + * a Type by its class, TemplateTypeScope::createWithClass() and + * TemplateTypeFactory::fromTemplateTag() — are reached through the + * classes' real names (kernelSingleton() / kernelStatic(): the native + * class in production, the PHP twin's in the prefixed harness, where PHP + * types would refuse a native singleton and where the native factory would + * widen a PHP bound it cannot recognise to TemplateMixedType). + * `instanceof` against a shadowed Type class + * (GenericObjectType, ObjectType, ErrorType, MixedType) accepts the PHP + * twin declared next to the native class in the differential tests + * (instanceOfShadowed()): the delegate twin's tags hand PHP types to the + * native bodies there; in a production run the native class carries the + * real name and the second lookup never runs. Class-map classes + * (OutOfClassScope, the Missing*FromReflectionException classes, + * ShouldNotHappenException, CircularTypeAliasDefinitionException, + * UniversalObjectCratesClassReflectionExtension, ReflectionEnum, + * ReflectionEnumBackedCase, InitializerExprContext, EnumCaseReflection, + * RealClassClassConstantReflection, TypeAlias, ArgumentsNormalizer and the + * parser nodes it needs (Arg, Identifier, FullyQualified, StaticCall), the + * Extended*Reflection interfaces and their Wrapped* implementations, + * TemplateType) go through pt_type_new / pt_type_call_static / + * pt_type_instanceof. The three that receive `$this` — + * InitializerExprContext::fromClassReflection(), `new EnumCaseReflection`, + * `new RealClassClassConstantReflection` — are remapped to stand-ins for + * the duration of the differential test, as is the crate check. is_file() is the internal function, called through + * its cached zend_function; a ReflectionException the adapter throws is + * caught by class name (the reflection extension's header is not part of + * every PHP install). + * + * TemplateTypeMap::map() with the twin's closures is expanded in place + * (map() rebuilds the map from getTypes() through the callback, nothing + * else): the `static fn (): Type => new ErrorType()` of getParentClass() / + * getImmediateInterfaces() only feeds withTypes() a list of fresh + * ErrorTypes, and the ancestor-resolution closure becomes the loop in + * getActiveTemplateTypeMapForAncestorResolution(); getActiveTemplateTypeMap() + * expands its own. The one closure that must stay a callable is + * typeMapFromList()'s TypeTraverser::map() callback, a + * pt_type_native_callback() holder over the `use ($map, $className)` + * snapshot. + */ + +#include "TypeTraits.h" +#include "generated/ClassReflection.h" + +namespace sigs = ptdecl::ClassReflection::sig; +#include "TypeOps.h" + +#include "zend_closures.h" + +#include +#include +#include + +zend_class_entry *pt_ce_class_reflection = nullptr; + +/* OBJ_PROP_NUM slots, in the twin's declaration order: the class-body + * properties first (the static $resolvingTypeAliasImports between + * typeAliases and hasMethodCache takes no instance slot), the promoted + * constructor properties after them */ +enum : uint32_t +{ + PT_CR_PROP_METHODS = 0, + PT_CR_PROP_PROPERTIES, + PT_CR_PROP_INSTANCE_PROPERTIES, + PT_CR_PROP_STATIC_PROPERTIES, + PT_CR_PROP_CONSTANTS, + PT_CR_PROP_ENUM_CASES, + PT_CR_PROP_CLASS_HIERARCHY_DISTANCES, + PT_CR_PROP_DEPRECATED_DESCRIPTION, + PT_CR_PROP_IS_DEPRECATED, + PT_CR_PROP_ALLOWED_SUB_TYPES, + PT_CR_PROP_ALLOWED_SUB_TYPES_RESOLVED, + PT_CR_PROP_IS_GENERIC, + PT_CR_PROP_IS_INTERNAL, + PT_CR_PROP_IS_FINAL, + PT_CR_PROP_IS_IMMUTABLE, + PT_CR_PROP_HAS_CONSISTENT_CONSTRUCTOR, + PT_CR_PROP_ACCEPTS_NAMED_ARGUMENTS, + PT_CR_PROP_TEMPLATE_TYPE_MAP, + PT_CR_PROP_ACTIVE_TEMPLATE_TYPE_MAP, + PT_CR_PROP_DEFAULT_CALL_SITE_VARIANCE_MAP, + PT_CR_PROP_CALL_SITE_VARIANCE_MAP, + PT_CR_PROP_ANCESTORS, + PT_CR_PROP_CACHE_KEY, + PT_CR_PROP_SUBCLASSES, + PT_CR_PROP_FILENAME, + PT_CR_PROP_REFLECTION_DOC_COMMENT, + PT_CR_PROP_STUB_PHP_DOC_BLOCK, + PT_CR_PROP_RESOLVED_PHP_DOC_BLOCK, + PT_CR_PROP_TRAIT_CONTEXT_RESOLVED_PHP_DOC_BLOCK, + PT_CR_PROP_CACHED_INTERFACES, + PT_CR_PROP_CACHED_PARENT_CLASS, + PT_CR_PROP_CIRCULAR_PARENT_CLASS_NAME, + PT_CR_PROP_TYPE_ALIASES, + PT_CR_PROP_HAS_METHOD_CACHE, + PT_CR_PROP_HAS_PROPERTY_CACHE, + PT_CR_PROP_HAS_INSTANCE_PROPERTY_CACHE, + PT_CR_PROP_HAS_STATIC_PROPERTY_CACHE, + PT_CR_PROP_NAME, + PT_CR_PROP_CLASS_REFLECTION_FACTORY, + PT_CR_PROP_REFLECTION_PROVIDER, + PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, + PT_CR_PROP_FILE_TYPE_MAPPER, + PT_CR_PROP_STUB_PHP_DOC_PROVIDER, + PT_CR_PROP_PHP_DOC_INHERITANCE_RESOLVER, + PT_CR_PROP_PHP_VERSION, + PT_CR_PROP_SIGNATURE_MAP_PROVIDER, + PT_CR_PROP_DEPRECATION_PROVIDER, + PT_CR_PROP_ATTRIBUTE_REFLECTION_FACTORY, + PT_CR_PROP_CLASS_REFLECTION_EXTENSION_REGISTRY_PROVIDER, + PT_CR_PROP_DISPLAY_NAME, + PT_CR_PROP_REFLECTION, + PT_CR_PROP_ANONYMOUS_FILENAME, + PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP, + PT_CR_PROP_STUB_PHP_DOC_BLOCK_CALLBACK, + PT_CR_PROP_EXTRA_CACHE_KEY, + PT_CR_PROP_RESOLVED_CALL_SITE_VARIANCE_MAP, + PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE, + PT_CR_PROP_COUNT, +}; + +namespace phpstanturbo { + +/* the real names of the shadowed Type classes instanceOfShadowed() falls + * back to under the prefixed activation */ +#define PT_CR_GENERIC_OBJECT_TYPE_NAME "PHPStan\\Type\\Generic\\GenericObjectType" +#define PT_CR_OBJECT_TYPE_NAME "PHPStan\\Type\\ObjectType" +#define PT_CR_ERROR_TYPE_NAME "PHPStan\\Type\\ErrorType" +#define PT_CR_MIXED_TYPE_NAME "PHPStan\\Type\\MixedType" +#define PT_CR_CONSTANT_INTEGER_TYPE_NAME "PHPStan\\Type\\Constant\\ConstantIntegerType" + +/* Mirrors PHPStan\Reflection\ClassReflection. State lives in the PHP + * object's property slots. Methods returning zv::Val use UNDEF to signal a + * pending exception, a legitimate PHP null is zv::Val::null(); methods + * returning bool with an `out` parameter return false on a pending + * exception. */ +class ClassReflection +{ +public: + explicit ClassReflection(zend_object *self) : self(self) {} + + /* {{{ the slots */ + + zv::Ref slot(uint32_t index) const { return zv::ObjRef(self).propAt(index); } + + /* a slot write that also clears IS_PROP_UNINIT (the promoted typed + * properties start uninitialized) */ + void writeSlot(uint32_t index, zv::Val value) + { + zval *p = OBJ_PROP_NUM(self, index); + zv::ObjRef(self).propAtWrite(index, std::move(value)); + Z_PROP_FLAG_P(p) = 0; + } + + zv::Val copyOfSlot(uint32_t index) const { return zv::Val::copyOf(slot(index)); } + + zval *thisZval() + { + ZVAL_OBJ(&selfZval, self); + return &selfZval; + } + + /* the Error the twin's typed-property read raises when the constructor + * never ran (the declaring class names it, as the engine does) */ + zv::Val uninitializedProperty(const char *name) const + { + zend_class_entry *declaring = pt_ce_class_reflection != NULL ? pt_ce_class_reflection : self->ce; + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(declaring->name), name); + return zv::Val(); + } + + /* a promoted slot that must hold an object by now (the services, the + * adapter); NULL with the Error pending otherwise */ + zend_object *service(uint32_t index, const char *name) const + { + zv::Ref value = slot(index); + if (UNEXPECTED(!value.isObject())) { + if (value.isUndef()) { + (void) uninitializedProperty(name); + } else { + zend_throw_error(NULL, "Call to a member function on %s", zend_zval_value_name(value.raw())); + } + return NULL; + } + return value.asObject(); + } + + /* $this->reflection->method(...$args) / $this->phpVersion->method(...) / ... */ + zv::Val callService(uint32_t index, const char *name, const char *lcname, size_t len, uint32_t argc, zval *argv) const + { + zend_object *object = service(index, name); + if (UNEXPECTED(object == NULL)) return zv::Val(); + return pt_type_call(object, lcname, len, argc, argv); + } + + zv::Val reflectionCall(const char *lcname, size_t len, uint32_t argc, zval *argv) const { return callService(PT_CR_PROP_REFLECTION, "reflection", lcname, len, argc, argv); } + + bool reflectionCallBool(const char *lcname, size_t len, bool &out) const + { + zv::Val result = reflectionCall(lcname, len, 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* the memo arrays keyed by member / class names: PHP array keys, so a + * numeric string is an integer key (symtable) */ + zval *memoFind(uint32_t index, zend_string *key) const + { + zv::Ref table = slot(index); + if (UNEXPECTED(!table.isArray())) return NULL; + return zend_symtable_find(table.asArrayTable(), key); + } + + /* array_key_exists($key, $this->memo) */ + bool memoExists(uint32_t index, zend_string *key) const { return memoFind(index, key) != NULL; } + + /* isset($this->memo[$key]) */ + bool memoIsset(uint32_t index, zend_string *key) const + { + zval *found = memoFind(index, key); + return found != NULL && Z_TYPE_P(found) != IS_NULL; + } + + /* $this->memo[$key] = $value; the value is returned as the twin's + * assignment expression yields it */ + zv::Ref memoSet(uint32_t index, zend_string *key, zv::Val value) + { + zval *table = OBJ_PROP_NUM(self, index); + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) { + zval fresh; + array_init(&fresh); + writeSlot(index, zv::Val::adopt(fresh)); + } + SEPARATE_ARRAY(table); + zval v = value.take(); + return zv::Ref(zend_symtable_update(Z_ARRVAL_P(table), key, &v)); + } + + bool memoSetBool(uint32_t index, zend_string *key, bool value) + { + memoSet(index, key, zv::Val::boolean(value)); + return value; + } + + /* }}} */ + + /* {{{ calls on other objects */ + + /* $object->method(...$args); the engine's Error on a non-object */ + static zv::Val callOn(zv::Ref object, const char *lcname, size_t len, uint32_t argc, zval *argv) + { + zv::Ref value = object.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Call to a member function %s() on %s", lcname, zend_zval_value_name(value.raw())); + return zv::Val(); + } + return pt_type_call(value.asObject(), lcname, len, argc, argv); + } + + static bool callBool(zv::Ref object, const char *lcname, size_t len, uint32_t argc, zval *argv, bool &out) + { + zv::Val result = callOn(object, lcname, len, argc, argv); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* the registry the eleventh service hands out, and its getters — fetched + * afresh at every use, as the twin does, but out of the provider's memo + * and the registry's own slots where those two final classes are what + * holds them (ReflectionAccess.cpp); every member lookup below starts + * with one of these */ + zv::Val registryGet(pt_registry_member member) const + { + zend_object *provider = service(PT_CR_PROP_CLASS_REFLECTION_EXTENSION_REGISTRY_PROVIDER, "classReflectionExtensionRegistryProvider"); + if (UNEXPECTED(provider == NULL)) return zv::Val(); + return pt_class_reflection_extension_registry_member(provider, member); + } + + zv::Val phpClassReflectionExtension() const { return registryGet(PT_REGISTRY_PHP_CLASS_REFLECTION_EXTENSION); } + zv::Val methodsClassReflectionExtensions() const { return registryGet(PT_REGISTRY_METHODS_EXTENSIONS); } + zv::Val propertiesClassReflectionExtensions() const { return registryGet(PT_REGISTRY_PROPERTIES_EXTENSIONS); } + zv::Val requireExtendsMethodsClassReflectionExtension() const { return registryGet(PT_REGISTRY_REQUIRE_EXTENDS_METHODS_EXTENSION); } + zv::Val requireExtendsPropertyClassReflectionExtension() const { return registryGet(PT_REGISTRY_REQUIRE_EXTENDS_PROPERTIES_EXTENSION); } + + /* $extension->method($this, $memberName) as bool / as value */ + bool extensionBool(zv::Ref extension, const char *lcname, size_t len, zend_string *memberName, bool &out) + { + zv::Args args{self, memberName}; + return callBool(extension, lcname, len, 2, args, out); + } + + zv::Val extensionCall(zv::Ref extension, const char *lcname, size_t len, zend_string *memberName) + { + zv::Args args{self, memberName}; + return callOn(extension, lcname, len, 2, args); + } + + /* the reflection provider through the slot readers of ReflectionAccess.cpp */ + bool providerHasClass(zend_string *className, bool &out) const + { + zend_object *provider = service(PT_CR_PROP_REFLECTION_PROVIDER, "reflectionProvider"); + if (UNEXPECTED(provider == NULL)) return false; + zval name; + ZVAL_STR(&name, className); + return pt_reflection_provider_has_class(provider, &name, out); + } + + zv::Val providerGetClass(zend_string *className) const + { + zend_object *provider = service(PT_CR_PROP_REFLECTION_PROVIDER, "reflectionProvider"); + if (UNEXPECTED(provider == NULL)) return zv::Val(); + zval name; + ZVAL_STR(&name, className); + return pt_reflection_provider_get_class(provider, &name); + } + + /* }}} */ + + /* {{{ other ClassReflection instances: direct when exactly this class, + * by name otherwise (the PHP twins of the differential harness) */ + + static bool isNative(zend_object *object) { return object->ce == pt_ce_class_reflection; } + + template + static zv::Val crCall(zv::Ref object, const char *lcname, size_t len, Direct direct) + { + zv::Ref value = object.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Call to a member function %s() on %s", lcname, zend_zval_value_name(value.raw())); + return zv::Val(); + } + if (EXPECTED(isNative(value.asObject()))) return direct(ClassReflection(value.asObject())); + return pt_type_call(value.asObject(), lcname, len, 0, NULL); + } + + template + static bool crCallBool(zv::Ref object, const char *lcname, size_t len, bool &out, Direct direct) + { + zv::Ref value = object.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Call to a member function %s() on %s", lcname, zend_zval_value_name(value.raw())); + return false; + } + if (EXPECTED(isNative(value.asObject()))) return direct(ClassReflection(value.asObject()), out); + zv::Val result = pt_type_call(value.asObject(), lcname, len, 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + static zv::Val crGetName(zv::Ref cr) { return crCall(cr, PT_LC("getname"), [](ClassReflection other) { return other.getName(); }); } + static zv::Val crGetCacheKey(zv::Ref cr) { return crCall(cr, PT_LC("getcachekey"), [](ClassReflection other) { return other.getCacheKey(); }); } + static zv::Val crGetParentClass(zv::Ref cr) { return crCall(cr, PT_LC("getparentclass"), [](ClassReflection other) { return other.getParentClass(); }); } + static zv::Val crGetNativeReflection(zv::Ref cr) { return crCall(cr, PT_LC("getnativereflection"), [](ClassReflection other) { return other.getNativeReflection(); }); } + static zv::Val crGetImmediateInterfaces(zv::Ref cr) { return crCall(cr, PT_LC("getimmediateinterfaces"), [](ClassReflection other) { return other.getImmediateInterfaces(); }); } + static bool crIsGeneric(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("isgeneric"), out, [](ClassReflection other, bool &o) { return other.isGeneric(o); }); } + static bool crIsFinalByKeyword(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("isfinalbykeyword"), out, [](ClassReflection other, bool &o) { return other.isFinalByKeyword(o); }); } + static bool crIsAnonymous(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("isanonymous"), out, [](ClassReflection other, bool &o) { return other.isAnonymous(o); }); } + static bool crAllowsDynamicProperties(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("allowsdynamicproperties"), out, [](ClassReflection other, bool &o) { return other.allowsDynamicProperties(o); }); } + static zv::Val crGetFileName(zv::Ref cr) { return crCall(cr, PT_LC("getfilename"), [](ClassReflection other) { return other.getFileName(); }); } + static zv::Val crGetAncestors(zv::Ref cr) { return crCall(cr, PT_LC("getancestors"), [](ClassReflection other) { return other.getAncestors(); }); } + static zv::Val crGetTypeAliases(zv::Ref cr) { return crCall(cr, PT_LC("gettypealiases"), [](ClassReflection other) { return other.getTypeAliases(); }); } + static zv::Val crGetTemplateTypeMap(zv::Ref cr) { return crCall(cr, PT_LC("gettemplatetypemap"), [](ClassReflection other) { return other.getTemplateTypeMap(); }); } + static zv::Val crGetActiveTemplateTypeMap(zv::Ref cr) { return crCall(cr, PT_LC("getactivetemplatetypemap"), [](ClassReflection other) { return other.getActiveTemplateTypeMap(); }); } + static zv::Val crGetCallSiteVarianceMap(zv::Ref cr) { return crCall(cr, PT_LC("getcallsitevariancemap"), [](ClassReflection other) { return other.getCallSiteVarianceMap(); }); } + static zv::Val crGetConstructor(zv::Ref cr) { return crCall(cr, PT_LC("getconstructor"), [](ClassReflection other) { return other.getConstructor(); }); } + static bool crIsImmutable(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("isimmutable"), out, [](ClassReflection other, bool &o) { return other.isImmutable(o); }); } + static bool crIsTrait(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("istrait"), out, [](ClassReflection other, bool &o) { return other.isTrait(o); }); } + static bool crHasConstructor(zv::Ref cr, bool &out) { return crCallBool(cr, PT_LC("hasconstructor"), out, [](ClassReflection other, bool &o) { return other.hasConstructor(o); }); } + + /* $classReflection->getTraits($recursive) */ + static zv::Val crGetTraits(zv::Ref cr, bool recursive) + { + zv::Ref value = cr.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Call to a member function getTraits() on %s", zend_zval_value_name(value.raw())); + return zv::Val(); + } + if (EXPECTED(isNative(value.asObject()))) return ClassReflection(value.asObject()).getTraits(recursive); + zval arg; + ZVAL_BOOL(&arg, recursive); + return pt_type_call(value.asObject(), PT_LC("gettraits"), 1, &arg); + } + + /* $classReflection->withTypes($types) */ + static zv::Val crWithTypes(zv::Ref cr, zv::Ref types) + { + zv::Ref value = cr.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Call to a member function withTypes() on %s", zend_zval_value_name(value.raw())); + return zv::Val(); + } + if (EXPECTED(isNative(value.asObject()))) return ClassReflection(value.asObject()).withTypes(types); + zval arg; + ZVAL_COPY_VALUE(&arg, types.raw()); + return pt_type_call(value.asObject(), PT_LC("withtypes"), 1, &arg); + } + + /* $class->reflection — the private slot of another instance, read as + * the twin reads it from inside the class: the slot of a native + * instance, the twin's property by name otherwise */ + static zv::Val crReflection(zv::Ref cr) + { + zv::Ref value = cr.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Attempt to read property \"reflection\" on %s", zend_zval_value_name(value.raw())); + return zv::Val(); + } + if (EXPECTED(isNative(value.asObject()))) return ClassReflection(value.asObject()).getNativeReflection(); + zv::Ref reflection = zv::ObjRef(value.asObject()).prop(PT_LC("reflection")); + if (UNEXPECTED(reflection.raw() == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: %s has no property $reflection", ZSTR_VAL(value.asObject()->ce->name)); + return zv::Val(); + } + if (UNEXPECTED(reflection.isUndef())) { + zend_throw_error(NULL, "Typed property %s::$reflection must not be accessed before initialization", ZSTR_VAL(value.asObject()->ce->name)); + return zv::Val(); + } + return zv::Val::copyOf(reflection); + } + + /* $cr->withTypes(array_values($cr->getTemplateTypeMap()->map(static fn + * (): Type => new ErrorType())->getTypes())) — the mapped map only + * feeds withTypes() one fresh ErrorType per template type */ + static zv::Val crWithErrorTypes(zv::Ref cr) + { + zv::Val map = crGetTemplateTypeMap(cr); + if (UNEXPECTED(map.isUndef())) return zv::Val(); + zv::Val types = callOn(map.ref(), PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(types.isUndef())) return zv::Val(); + uint32_t count = types.ref().isArray() ? zend_hash_num_elements(types.ref().asArrayTable()) : 0; + zv::Arr list = zv::Arr::create(count); + for (uint32_t i = 0; i < count; i++) { + zv::Val errorType = pt_type_new_error_type(); + if (UNEXPECTED(errorType.isUndef())) return zv::Val(); + list.push(std::move(errorType)); + } + return crWithTypes(cr, list.ref()); + } + + /* }}} */ + + /* {{{ the Type kernel */ + + /* $value instanceof : the native class — or, under + * the prefixed activation of the differential tests, where the PHP + * twins are declared next to the native classes, the twin of the same + * real name looked up without autoloading (an undeclared class is "no + * instance"); in a production run the native class carries the real + * name and the lookup never happens */ + static bool instanceOfShadowed(zv::Ref value, zend_class_entry *ce, const char *realName, size_t len) + { + zv::Ref v = value.deref(); + if (!v.isObject()) return false; + if (ce != NULL && instanceof_function(v.asObject()->ce, ce)) return true; + if (ce != NULL && zend_string_equals_cstr(ce->name, realName, len)) return false; + zend_string *name = zend_string_init(realName, len, 0); + zend_class_entry *twin = zend_lookup_class_ex(name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_string_release(name); + return twin != NULL && instanceof_function(v.asObject()->ce, twin); + } + + /* TemplateTypeVariance::createStatic() / VerbosityLevel::cache() / + * VerbosityLevel::typeOnly() — the singleton of the class under its + * real name: the native class in a production run; under the prefixed + * activation the PHP twin's, which both the PHP types the delegate twin + * hands over and the native kernel (its twin-aware value readers) + * accept — a native singleton would be refused by the PHP types' typed + * parameters there. UNDEF = pending exception */ + static zv::Val kernelSingleton(const char *className, size_t classLen, const char *lcmethod, size_t methodLen) + { + return kernelStatic(className, classLen, lcmethod, methodLen, 0, NULL); + } + + /* Class::method(...$args) by the class's REAL name, for the same reason: + * the native class in a production run, the PHP twin's body under the + * prefixed activation — TemplateTypeFactory picks the Template*Type + * class by testing the bound against the Type class entries, and the + * native one would widen a PHP bound (what the delegate twin's tags hand + * over there) to TemplateMixedType. UNDEF = pending exception */ + static zv::Val kernelStatic(const char *className, size_t classLen, const char *lcmethod, size_t methodLen, uint32_t argc, zval *argv) + { + zend_string *name = zend_string_init(className, classLen, 0); + zend_class_entry *ce = zend_lookup_class(name); + zend_string_release(name); + if (UNEXPECTED(ce == NULL)) { + if (!EG(exception)) { + zend_throw_error(NULL, "phpstan_turbo: class %s not found", className); + } + return zv::Val(); + } + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcmethod, methodLen); + if (UNEXPECTED(fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", className, lcmethod); + return zv::Val(); + } + zval result; + zend_call_known_function(fn, NULL, ce, &result, argc, argv, NULL); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&result); + return zv::Val(); + } + return zv::Val::adopt(result); + } + + /* TemplateTypeScope::createWithClass($className) / TemplateTypeFactory::fromTemplateTag($scope, $tag) */ + static zv::Val templateTypeScopeWithClass(zval *className) { return kernelStatic(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeScope"), PT_LC("createwithclass"), 1, className); } + + static zv::Val templateTypeFactoryFromTemplateTag(zval *scope, zval *tag) + { + zv::Args args{scope, tag}; + return kernelStatic(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeFactory"), PT_LC("fromtemplatetag"), 2, args); + } + + static zv::Val verbosityLevelTypeOnly() { return kernelSingleton(PT_LC("PHPStan\\Type\\VerbosityLevel"), PT_LC("typeonly")); } + static zv::Val verbosityLevelCache() { return kernelSingleton(PT_LC("PHPStan\\Type\\VerbosityLevel"), PT_LC("cache")); } + static zv::Val templateTypeVarianceStatic() { return kernelSingleton(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeVariance"), PT_LC("createstatic")); } + + /* $type->getClassReflection() of an object type: natively for a native + * ObjectType (GenericObjectType included), by name otherwise */ + /* $type->getClassReflection() — the op entry of the RECEIVER's own class + * (GenericObjectType and StaticType override ObjectType's body, so a + * pt_object_type_get_class_reflection() shortcut would silently run the + * parent's), the PHP method for anything else */ + static zv::Val typeGetClassReflection(zv::Ref type) + { + zv::Ref v = type.deref(); + if (UNEXPECTED(!v.isObject())) { + zend_throw_error(NULL, "Call to a member function getClassReflection() on %s", zend_zval_value_name(v.raw())); + return zv::Val(); + } + const pt_type_ops *ops = pt_type_ops_of(v.asObject()->ce); + if (EXPECTED(ops != NULL)) { + const pt_type_op_entry &entry = ops->entries[PT_OP_GET_CLASS_REFLECTION]; + if (EXPECTED(entry.fn != NULL)) return entry.fn(v.asObject(), entry.scope, 0, NULL); + } + return pt_type_call(v.asObject(), PT_LC("getclassreflection"), 0, NULL); + } + + /* TemplateTypeHelper::resolveTemplateTypes($type, + * $this->getActiveTemplateTypeMapForAncestorResolution(), + * $this->getCallSiteVarianceMap(), TemplateTypeVariance::createStatic(), + * $keepErrorTypes) */ + zv::Val resolveAncestorTemplateTypes(zv::Ref type, bool keepErrorTypes) + { + zv::Val standins = getActiveTemplateTypeMapForAncestorResolution(); + if (UNEXPECTED(standins.isUndef())) return zv::Val(); + zv::Val callSiteVariances = getCallSiteVarianceMap(); + if (UNEXPECTED(callSiteVariances.isUndef())) return zv::Val(); + zv::Val staticVariance = templateTypeVarianceStatic(); + if (UNEXPECTED(staticVariance.isUndef())) return zv::Val(); + return pt_type_template_type_helper_resolve_template_types(type.raw(), standins.raw(), callSiteVariances.raw(), staticVariance.raw(), keepErrorTypes); + } + + /* }}} */ + + /* {{{ small helpers */ + + /* is_file($fileName) — the internal function through its zend_function + * (persistent: resolved once); false = pending exception */ + [[nodiscard]] static bool isFile(zval *fileName, bool &out) + { + static zend_function *fn = nullptr; + if (UNEXPECTED(fn == nullptr)) { + fn = (zend_function *) zend_hash_str_find_ptr(EG(function_table), PT_LC("is_file")); + if (UNEXPECTED(fn == nullptr)) { + zend_throw_error(NULL, "phpstan_turbo: is_file() is not available"); + return false; + } + } + zval result; + zend_call_known_function(fn, NULL, NULL, &result, 1, fileName, NULL); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&result); + return false; + } + out = zend_is_true(&result); + zval_ptr_dtor(&result); + return true; + } + + /* catch (ReflectionException) — clears the pending exception when it + * is one (the class looked up by name: the reflection extension's + * header is not part of every PHP install) */ + static bool caughtReflectionException() + { + if (EG(exception) == NULL) return false; + zend_string *name = zend_string_init(PT_LC("ReflectionException"), 0); + zend_class_entry *ce = zend_lookup_class_ex(name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_string_release(name); + if (ce == NULL || !instanceof_function(EG(exception)->ce, ce)) return false; + zend_clear_exception(); + return true; + } + + /* throw new (...$args) */ + static void throwNew(int classIdx, uint32_t argc, zval *argv) + { + zv::Val exception = pt_type_new(classIdx, argc, argv); + if (UNEXPECTED(exception.isUndef())) return; + zval z = exception.take(); + zend_throw_exception_object(&z); + } + + /* throw new MissingMethodFromReflectionException($this->getName(), $methodName) */ + void throwMissingMethod(zend_string *methodName) + { + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return; + zv::Args args{name.raw(), methodName}; + throwNew(PT_CLASS_MISSING_METHOD_FROM_REFLECTION_EXCEPTION, 2, args); + } + + /* throw new MissingPropertyFromReflectionException($this->getName(), $propertyName) */ + void throwMissingProperty(zend_string *propertyName) + { + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return; + zv::Args args{name.raw(), propertyName}; + throwNew(PT_CLASS_MISSING_PROPERTY_FROM_REFLECTION_EXCEPTION, 2, args); + } + + /* implode(',', $strings) */ + static zv::Val implodeComma(zv::Ref list) + { + std::string out; + bool first = true; + for (auto entry : zv::ArrRef(list.raw())) { + if (!first) { + out += ','; + } + first = false; + zend_string *s = zval_get_string(entry.value().raw()); + out.append(ZSTR_VAL(s), ZSTR_LEN(s)); + zend_string_release(s); + } + return zv::Val::string(out.data(), out.size()); + } + + static uint32_t countOf(zv::Ref array) { return array.isArray() ? zend_hash_num_elements(array.asArrayTable()) : 0; } + + /* the interned literals of the class */ + static zend_string *literal(zend_string *&memo, const char *value, size_t len) + { + if (UNEXPECTED(memo == nullptr)) { + memo = zend_string_init_interned(value, len, 1); + } + return memo; + } + + /* $this as a value (the twin's `return $this`) */ + zv::Val thisValue() const + { + zval self_; + ZVAL_OBJ(&self_, self); + return zv::Val::copyOf(zv::Ref(&self_)); + } + + /* ($callable)(...$args) — the stub PHPDoc callback; UNDEF = pending exception */ + static zv::Val callCallable(zv::Ref callable, uint32_t argc, zval *argv) { return pt_type_call_callable(callable.raw(), argc, argv); } + + /* $target[] = $value — array_map()/array_filter() key preservation */ + static void setAtKey(zv::Arr &target, const zv::ArrayEntry &entry, zv::Val value) + { + if (entry.stringKeyOrNull() != NULL) { + target.set(entry.stringKeyOrNull(), std::move(value)); + } else { + target.arrRef().setIndex(entry.indexKey(), value.ref()); + } + } + + /* the entry's key as the `string $name` an array-map callback receives */ + static zv::Val entryKeyAsValue(const zv::ArrayEntry &entry) + { + return entry.stringKeyOrNull() != NULL + ? zv::Val::string(entry.stringKeyOrNull()) + : zv::Val::adoptString(zend_long_to_str((zend_long) entry.indexKey())); + } + + /* array_merge($a, $b): string keys overwrite, integer keys renumber */ + static zv::Val arrayMerge(zv::Ref a, zv::Ref b) + { + zv::Arr merged = zv::Arr::create(countOf(a) + countOf(b)); + zval *sources[2] = { a.raw(), b.raw() }; + for (int i = 0; i < 2; i++) { + if (Z_TYPE_P(sources[i]) != IS_ARRAY) continue; + for (auto entry : zv::ArrRef(sources[i])) { + if (entry.stringKeyOrNull() != NULL) { + merged.set(entry.stringKeyOrNull(), zv::Val::copyOf(entry.value())); + } else { + merged.push(entry.value()); + } + } + } + + return zv::Val(std::move(merged)); + } + + /* array_values(array_unique($list)) over a list of strings */ + static zv::Val arrayUniqueValues(zv::Ref list) + { + zv::Arr unique = zv::Arr::create(countOf(list)); + if (list.isArray()) { + zv::ScratchTable seen(countOf(list)); + for (auto entry : zv::ArrRef(list.raw())) { + zend_string *key = zval_get_string(entry.value().raw()); + if (zend_hash_find(seen.table(), key) == NULL) { + zval marker; + ZVAL_TRUE(&marker); + zend_hash_add_new(seen.table(), key, &marker); + unique.push(entry.value()); + } + zend_string_release(key); + } + } + + return zv::Val(std::move(unique)); + } + + /* the twin's `private static array $resolvingTypeAliasImports` slot + * (borrowed; resolved once per activated class) — always the declaring + * class's, as `self::` names it */ + static zval *resolvingTypeAliasImports() + { + static zend_class_entry *memoCe = nullptr; + static zval *memoSlot = nullptr; + zend_class_entry *ce = pt_ce_class_reflection; + if (UNEXPECTED(ce == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: ClassReflection is not registered"); + return NULL; + } + if (UNEXPECTED(memoCe != ce)) { + if (CE_STATIC_MEMBERS(ce) == NULL) { + zend_class_init_statics(ce); + } + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("resolvingTypeAliasImports")); + if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) == 0)) { + zend_throw_error(NULL, "phpstan_turbo: ClassReflection has no static $resolvingTypeAliasImports"); + return NULL; + } + memoSlot = CE_STATIC_MEMBERS(ce) + info->offset; + memoCe = ce; + } + return memoSlot; + } + + /* catch (CircularTypeAliasDefinitionException) */ + static bool caughtCircularTypeAliasDefinitionException() + { + if (EG(exception) == NULL) return false; + zend_class_entry *ce = pt_class_loaded(PT_CLASS_CIRCULAR_TYPE_ALIAS_DEFINITION_EXCEPTION); + if (ce == NULL || !instanceof_function(EG(exception)->ce, ce)) return false; + zend_clear_exception(); + return true; + } + + static zv::Val templateTypeVarianceInvariant() { return kernelSingleton(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeVariance"), PT_LC("createinvariant")); } + + /* }}} */ + + /* {{{ __construct */ + + /* the 19 constructor arguments as zpp delivers them (NULL = a defaulted + * optional parameter) */ + struct ConstructArgs + { + zval *classReflectionFactory, *reflectionProvider, *initializerExprTypeResolver, *fileTypeMapper, *stubPhpDocProvider, *phpDocInheritanceResolver, *phpVersion, *signatureMapProvider, *deprecationProvider, *attributeReflectionFactory, *classReflectionExtensionRegistryProvider; + zend_string *displayName; + zval *reflection; + zend_string *anonymousFilename; + zval *resolvedTemplateTypeMap; + zval *stubPhpDocBlockCallback; + zend_string *extraCacheKey; + zval *resolvedCallSiteVarianceMap; + bool finalByKeywordOverride; + bool finalByKeywordOverrideIsNull; + }; + + void construct(const ConstructArgs &a) + { + writeSlot(PT_CR_PROP_CLASS_REFLECTION_FACTORY, zv::Val::copyOf(zv::Ref(a.classReflectionFactory))); + writeSlot(PT_CR_PROP_REFLECTION_PROVIDER, zv::Val::copyOf(zv::Ref(a.reflectionProvider))); + writeSlot(PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, zv::Val::copyOf(zv::Ref(a.initializerExprTypeResolver))); + writeSlot(PT_CR_PROP_FILE_TYPE_MAPPER, zv::Val::copyOf(zv::Ref(a.fileTypeMapper))); + writeSlot(PT_CR_PROP_STUB_PHP_DOC_PROVIDER, zv::Val::copyOf(zv::Ref(a.stubPhpDocProvider))); + writeSlot(PT_CR_PROP_PHP_DOC_INHERITANCE_RESOLVER, zv::Val::copyOf(zv::Ref(a.phpDocInheritanceResolver))); + writeSlot(PT_CR_PROP_PHP_VERSION, zv::Val::copyOf(zv::Ref(a.phpVersion))); + writeSlot(PT_CR_PROP_SIGNATURE_MAP_PROVIDER, zv::Val::copyOf(zv::Ref(a.signatureMapProvider))); + writeSlot(PT_CR_PROP_DEPRECATION_PROVIDER, zv::Val::copyOf(zv::Ref(a.deprecationProvider))); + writeSlot(PT_CR_PROP_ATTRIBUTE_REFLECTION_FACTORY, zv::Val::copyOf(zv::Ref(a.attributeReflectionFactory))); + writeSlot(PT_CR_PROP_CLASS_REFLECTION_EXTENSION_REGISTRY_PROVIDER, zv::Val::copyOf(zv::Ref(a.classReflectionExtensionRegistryProvider))); + writeSlot(PT_CR_PROP_DISPLAY_NAME, zv::Val::string(a.displayName)); + writeSlot(PT_CR_PROP_REFLECTION, zv::Val::copyOf(zv::Ref(a.reflection))); + writeSlot(PT_CR_PROP_ANONYMOUS_FILENAME, a.anonymousFilename == NULL ? zv::Val::null() : zv::Val::string(a.anonymousFilename)); + writeSlot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP, optional(a.resolvedTemplateTypeMap)); + writeSlot(PT_CR_PROP_STUB_PHP_DOC_BLOCK_CALLBACK, optional(a.stubPhpDocBlockCallback)); + writeSlot(PT_CR_PROP_EXTRA_CACHE_KEY, a.extraCacheKey == NULL ? zv::Val::null() : zv::Val::string(a.extraCacheKey)); + writeSlot(PT_CR_PROP_RESOLVED_CALL_SITE_VARIANCE_MAP, optional(a.resolvedCallSiteVarianceMap)); + writeSlot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE, a.finalByKeywordOverrideIsNull ? zv::Val::null() : zv::Val::boolean(a.finalByKeywordOverride)); + } + + static zv::Val optional(zval *value) { return value == NULL ? zv::Val::null() : zv::Val::copyOf(zv::Ref(value)); } + + /* }}} */ + + zv::Val getNativeReflection() const + { + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + if (UNEXPECTED(reflection.isUndef())) return uninitializedProperty("reflection"); + return zv::Val::copyOf(reflection); + } + + zv::Val getFileName() + { + zv::Ref filename = slot(PT_CR_PROP_FILENAME); + if (!filename.isBool()) return zv::Val::copyOf(filename); + + zv::Ref anonymousFilename = slot(PT_CR_PROP_ANONYMOUS_FILENAME); + if (UNEXPECTED(anonymousFilename.isUndef())) return uninitializedProperty("anonymousFilename"); + if (!anonymousFilename.isNull()) { + zv::Val value = zv::Val::copyOf(anonymousFilename); + writeSlot(PT_CR_PROP_FILENAME, zv::Val::copyOf(value.ref())); + return value; + } + zv::Val fileName = reflectionCall(PT_LC("getfilename"), 0, NULL); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + if (fileName.ref().isFalse()) { + writeSlot(PT_CR_PROP_FILENAME, zv::Val::null()); + return zv::Val::null(); + } + + bool exists; + if (UNEXPECTED(!isFile(fileName.raw(), exists))) return zv::Val(); + if (!exists) { + writeSlot(PT_CR_PROP_FILENAME, zv::Val::null()); + return zv::Val::null(); + } + + writeSlot(PT_CR_PROP_FILENAME, zv::Val::copyOf(fileName.ref())); + return fileName; + } + + zv::Val getParentClass() + { + zv::Ref cached = slot(PT_CR_PROP_CACHED_PARENT_CLASS); + if (!cached.isBool()) return zv::Val::copyOf(cached); + + zv::Val parentClass = reflectionCall(PT_LC("getparentclass"), 0, NULL); + if (UNEXPECTED(parentClass.isUndef())) return zv::Val(); + + if (parentClass.ref().isFalse()) { + writeSlot(PT_CR_PROP_CACHED_PARENT_CLASS, zv::Val::null()); + return zv::Val::null(); + } + + /* $parentClass->getName() — the adapter's getter, pure: read once + * for the uses the twin makes of it */ + zv::Val parentName = callOn(parentClass.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(parentName.isUndef())) return zv::Val(); + zend_string *parentNameStr = zval_get_string(parentName.raw()); + zv::Str parentNameOwned = zv::Str::adopt(parentNameStr); + + zv::Val circularParentClassName = findCircularParentClassName(parentNameStr); + if (UNEXPECTED(circularParentClassName.isUndef())) return zv::Val(); + if (!circularParentClassName.isNull()) { + throwCircularReference(circularParentClassName.ref()); + return zv::Val(); + } + + zv::Val extendsTag = getFirstExtendsTag(); + if (UNEXPECTED(extendsTag.isUndef())) return zv::Val(); + + if (!extendsTag.isNull()) { + zv::Val extendedType = callOn(extendsTag.ref(), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(extendedType.isUndef())) return zv::Val(); + bool valid; + if (UNEXPECTED(!isValidAncestorType(extendedType.ref(), parentNameStr, valid))) return zv::Val(); + if (valid) { + bool generic; + if (UNEXPECTED(!isGeneric(generic))) return zv::Val(); + if (generic) { + extendedType = resolveAncestorTemplateTypes(extendedType.ref(), false); + if (UNEXPECTED(extendedType.isUndef())) return zv::Val(); + } + + if (!instanceOfShadowed(extendedType.ref(), pt_ce_generic_object_type, PT_LC(PT_CR_GENERIC_OBJECT_TYPE_NAME))) { + return providerGetClass(parentNameStr); + } + + zv::Val reflection = typeGetClassReflection(extendedType.ref()); + if (UNEXPECTED(reflection.isUndef())) return zv::Val(); + if (!reflection.isNull()) return reflection; + return providerGetClass(parentNameStr); + } + } + + zv::Val parentReflection = providerGetClass(parentNameStr); + if (UNEXPECTED(parentReflection.isUndef())) return zv::Val(); + bool parentGeneric; + if (UNEXPECTED(!crIsGeneric(parentReflection.ref(), parentGeneric))) return zv::Val(); + if (parentGeneric) return crWithErrorTypes(parentReflection.ref()); + + writeSlot(PT_CR_PROP_CACHED_PARENT_CLASS, zv::Val::copyOf(parentReflection.ref())); + + return parentReflection; + } + + /* private: the class the parent class chain loops back to, null when + * the chain ends. BetterReflection only rejects a class extending + * itself directly; a cycle spanning several classes would make every + * walk over the hierarchy run forever */ + zv::Val findCircularParentClassName(zend_string *parentClassName) + { + zv::Ref memo = slot(PT_CR_PROP_CIRCULAR_PARENT_CLASS_NAME); + if (!memo.isBool()) return zv::Val::copyOf(memo); + + writeSlot(PT_CR_PROP_CIRCULAR_PARENT_CLASS_NAME, zv::Val::null()); + + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Arr visitedClassNames = zv::Arr::create(4); + { + zv::Str nameStr = zv::Str::adopt(zval_get_string(name.raw())); + zv::Str lowercased = zv::Str::adopt(zend_string_tolower(nameStr.get())); + visitedClassNames.set(lowercased.get(), zv::Val::boolean(true)); + } + + zv::Str currentClassName = zv::Str::copyOf(parentClassName); + while (true) { + zv::Str lowercased = zv::Str::adopt(zend_string_tolower(currentClassName.get())); + if (visitedClassNames.arrRef().exists(lowercased.get())) { + writeSlot(PT_CR_PROP_CIRCULAR_PARENT_CLASS_NAME, zv::Val::string(currentClassName.get())); + return zv::Val::string(currentClassName.get()); + } + + visitedClassNames.set(lowercased.get(), zv::Val::boolean(true)); + + bool has; + if (UNEXPECTED(!providerHasClass(currentClassName.get(), has))) return zv::Val(); + if (!has) return zv::Val::null(); + + /* $this->reflectionProvider->getClass($currentClassName)->reflection->getParentClass() */ + zv::Val classReflection = providerGetClass(currentClassName.get()); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + zv::Val reflection = crGetNativeReflection(classReflection.ref()); + if (UNEXPECTED(reflection.isUndef())) return zv::Val(); + zv::Val parentClass = callOn(reflection.ref(), PT_LC("getparentclass"), 0, NULL); + if (UNEXPECTED(parentClass.isUndef())) return zv::Val(); + if (parentClass.ref().isFalse()) return zv::Val::null(); + + zv::Val parentName = callOn(parentClass.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(parentName.isUndef())) return zv::Val(); + currentClassName = zv::Str::adopt(zval_get_string(parentName.raw())); + } + } + + /* throw CircularReference::fromClassName($className) */ + static void throwCircularReference(zv::Ref className) + { + static const char circularReference[] = "PHPStan\\BetterReflection\\Reflection\\Exception\\CircularReference"; + zv::Str name = zv::Str::adopt(zend_string_init(circularReference, sizeof(circularReference) - 1, 0)); + zend_class_entry *ce = zend_lookup_class(name.get()); + if (UNEXPECTED(ce == NULL)) { + if (!EG(exception)) zend_throw_error(NULL, "Class \"%s\" not found", circularReference); + return; + } + zv::Val exception = pt_type_call_static_ce(ce, PT_LC("fromclassname"), 1, className.raw()); + if (UNEXPECTED(exception.isUndef())) return; + zval thrown = exception.take(); + zend_throw_exception_object(&thrown); + } + + zv::Val getName() + { + zv::Ref name = slot(PT_CR_PROP_NAME); + if (EXPECTED(!name.isNull())) return zv::Val::copyOf(name); + zv::Val computed = reflectionCall(PT_LC("getname"), 0, NULL); + if (UNEXPECTED(computed.isUndef())) return zv::Val(); + writeSlot(PT_CR_PROP_NAME, zv::Val::copyOf(computed.ref())); + return computed; + } + + zv::Val displayName() const + { + zv::Ref displayName = slot(PT_CR_PROP_DISPLAY_NAME); + if (UNEXPECTED(displayName.isUndef())) return uninitializedProperty("displayName"); + return zv::Val::copyOf(displayName); + } + + /* the `$templateTypes` list both getDisplayName() and getCacheKey() + * build: the active template types projected through the call-site + * variances (an entry with no variance skipped), described at the + * given verbosity */ + zv::Val describeTemplateTypes(bool forCacheKey) + { + zv::Val varianceMap = getCallSiteVarianceMap(); + if (UNEXPECTED(varianceMap.isUndef())) return zv::Val(); + zv::Val variances = callOn(varianceMap.ref(), PT_LC("getvariances"), 0, NULL); + if (UNEXPECTED(variances.isUndef())) return zv::Val(); + zv::Val activeMap = getActiveTemplateTypeMap(); + if (UNEXPECTED(activeMap.isUndef())) return zv::Val(); + zv::Val types = callOn(activeMap.ref(), PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(types.isUndef())) return zv::Val(); + zv::Val level = forCacheKey ? verbosityLevelCache() : verbosityLevelTypeOnly(); + if (UNEXPECTED(level.isUndef())) return zv::Val(); + zv::Arr templateTypes = zv::Arr::create(countOf(types.ref())); + if (!types.ref().isArray()) return zv::Val(std::move(templateTypes)); + HashTable *variancesTable = variances.ref().isArray() ? variances.ref().asArrayTable() : NULL; + for (auto entry : zv::ArrRef(types.raw())) { + /* $variances[$name] ?? null — the same PHP array key on both sides */ + zval *variance = NULL; + if (variancesTable != NULL) { + zend_string *key = entry.stringKeyOrNull(); + variance = key != NULL ? zend_hash_find(variancesTable, key) : zend_hash_index_find(variancesTable, entry.indexKey()); + } + if (variance == NULL || Z_TYPE_P(variance) == IS_NULL) continue; + zv::Val described = pt_type_projection_helper_describe(entry.value().raw(), variance, level.raw()); + if (UNEXPECTED(described.isUndef())) return zv::Val(); + templateTypes.push(std::move(described)); + } + return zv::Val(std::move(templateTypes)); + } + + zv::Val getDisplayName(bool withTemplateTypes) + { + if (!withTemplateTypes) return displayName(); + zv::Ref resolved = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolved.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + if (resolved.isNull()) return displayName(); + zv::Val resolvedTypes = callOn(resolved, PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(resolvedTypes.isUndef())) return zv::Val(); + if (countOf(resolvedTypes.ref()) == 0) return displayName(); + + zv::Val templateTypes = describeTemplateTypes(false); + if (UNEXPECTED(templateTypes.isUndef())) return zv::Val(); + zv::Val name = displayName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Val joined = implodeComma(templateTypes.ref()); + return zv::Val::adoptString(zend_strpprintf(0, "%s<%s>", Z_STRVAL_P(name.raw()), Z_STRVAL_P(joined.raw()))); + } + + zv::Val getCacheKey() + { + zv::Ref cacheKey = slot(PT_CR_PROP_CACHE_KEY); + if (!cacheKey.isNull()) return zv::Val::copyOf(cacheKey); + + zv::Val name = displayName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + std::string key(Z_STRVAL_P(name.raw()), Z_STRLEN_P(name.raw())); + + zv::Ref resolved = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolved.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + if (!resolved.isNull()) { + zv::Val templateTypes = describeTemplateTypes(true); + if (UNEXPECTED(templateTypes.isUndef())) return zv::Val(); + zv::Val joined = implodeComma(templateTypes.ref()); + key += '<'; + key.append(Z_STRVAL_P(joined.raw()), Z_STRLEN_P(joined.raw())); + key += '>'; + } + + bool hasOverride = false; + if (UNEXPECTED(!hasFinalByKeywordOverride(hasOverride))) return zv::Val(); + if (hasOverride) { + bool finalByKeyword = false; + if (UNEXPECTED(!isFinalByKeyword(finalByKeyword))) return zv::Val(); + key += finalByKeyword ? "-f=t" : "-f=f"; + } + + zv::Ref extraCacheKey = slot(PT_CR_PROP_EXTRA_CACHE_KEY); + if (UNEXPECTED(extraCacheKey.isUndef())) return uninitializedProperty("extraCacheKey"); + if (!extraCacheKey.isNull()) { + zend_string *extra = zval_get_string(extraCacheKey.raw()); + key += '-'; + key.append(ZSTR_VAL(extra), ZSTR_LEN(extra)); + zend_string_release(extra); + } + + zv::Val result = zv::Val::string(key.data(), key.size()); + writeSlot(PT_CR_PROP_CACHE_KEY, zv::Val::copyOf(result.ref())); + + return result; + } + + zv::Val getClassHierarchyDistances() + { + zv::Ref memo = slot(PT_CR_PROP_CLASS_HIERARCHY_DISTANCES); + if (memo.isNull()) { + zend_long distance = 0; + zv::Arr distances = zv::Arr::create(8); + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + distances.set(Z_STR_P(name.raw()), zv::Val::integer(distance)); + zval selfZv; + ZVAL_OBJ(&selfZv, self); + zv::Val current = zv::Val::copyOf(zv::Ref(&selfZv)); + zv::Val ownReflection = getNativeReflection(); + if (UNEXPECTED(ownReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(!addTraitDistances(ownReflection.ref(), distance, distances))) return zv::Val(); + + /* while (($currentClassReflection = $currentClassReflection->getParentClass()) !== null): + * walking the parents through getParentClass() and not through + * the native reflection makes a cyclic class hierarchy end in a + * CircularReference exception instead of looping forever */ + while (true) { + zv::Val parent = crGetParentClass(current.ref()); + if (UNEXPECTED(parent.isUndef())) return zv::Val(); + if (parent.isNull()) break; + current = std::move(parent); + distance++; + zv::Val parentName = crGetName(current.ref()); + if (UNEXPECTED(parentName.isUndef())) return zv::Val(); + zend_string *parentNameStr = zval_get_string(parentName.raw()); + if (!distances.arrRef().exists(parentNameStr)) { + distances.set(parentNameStr, zv::Val::integer(distance)); + } + zend_string_release(parentNameStr); + zv::Val parentReflection = crGetNativeReflection(current.ref()); + if (UNEXPECTED(parentReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(!addTraitDistances(parentReflection.ref(), distance, distances))) return zv::Val(); + } + + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + zv::Val interfaces = callOn(nativeReflection.ref(), PT_LC("getinterfaces"), 0, NULL); + if (UNEXPECTED(interfaces.isUndef())) return zv::Val(); + if (interfaces.ref().isArray()) { + for (auto entry : zv::ArrRef(interfaces.raw())) { + distance++; + zv::Val interfaceName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(interfaceName.isUndef())) return zv::Val(); + zend_string *interfaceNameStr = zval_get_string(interfaceName.raw()); + if (!distances.arrRef().exists(interfaceNameStr)) { + distances.set(interfaceNameStr, zv::Val::integer(distance)); + } + zend_string_release(interfaceNameStr); + } + } + + writeSlot(PT_CR_PROP_CLASS_HIERARCHY_DISTANCES, zv::Val::copyOf(distances.ref())); + return zv::Val(std::move(distances)); + } + + return zv::Val::copyOf(memo); + } + + /* the `foreach ($this->collectTraits($class) as $trait)` blocks of + * getClassHierarchyDistances() */ + bool addTraitDistances(zv::Ref classReflection, zend_long &distance, zv::Arr &distances) + { + zv::Val traits = collectTraits(classReflection); + if (UNEXPECTED(traits.isUndef())) return false; + for (auto entry : zv::ArrRef(traits.raw())) { + distance++; + zv::Val traitName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(traitName.isUndef())) return false; + zend_string *traitNameStr = zval_get_string(traitName.raw()); + bool exists = distances.arrRef().exists(traitNameStr); + if (!exists) { + distances.set(traitNameStr, zv::Val::integer(distance)); + } + zend_string_release(traitNameStr); + } + return true; + } + + /* private; a list of the class's traits, breadth-first through the + * traits' own traits (the twin's array_shift() queue), each trait name + * once - traits can use each other in a cycle and the reflection + * objects are not guaranteed to be identical; UNDEF = pending + * exception */ + zv::Val collectTraits(zv::Ref classReflection) + { + zv::Arr traits = zv::Arr::create(4); + zv::Val initial = callOn(classReflection, PT_LC("gettraits"), 0, NULL); + if (UNEXPECTED(initial.isUndef())) return zv::Val(); + std::vector queue; + if (initial.ref().isArray()) { + for (auto entry : zv::ArrRef(initial.raw())) { + queue.push_back(zv::Val::copyOf(entry.value())); + } + } + + for (size_t head = 0; head < queue.size(); head++) { + zv::Val trait = zv::Val::copyOf(queue[head].ref()); + /* $trait->getName() — the adapter's getter, pure: read once for + * both uses */ + zv::Val traitName = callOn(trait.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(traitName.isUndef())) return zv::Val(); + zv::Str traitNameStr = zv::Str::adopt(zval_get_string(traitName.raw())); + if (traits.arrRef().exists(traitNameStr.get())) continue; + + traits.set(traitNameStr.get(), zv::Val::copyOf(trait.ref())); + + zv::Val subTraits = callOn(trait.ref(), PT_LC("gettraits"), 0, NULL); + if (UNEXPECTED(subTraits.isUndef())) return zv::Val(); + if (!subTraits.ref().isArray()) continue; + for (auto entry : zv::ArrRef(subTraits.raw())) { + queue.push_back(zv::Val::copyOf(entry.value())); + } + } + + /* array_values($traits) */ + zv::Arr list = zv::Arr::create(zend_hash_num_elements(traits.table())); + for (auto entry : traits.arrRef()) { + list.push(entry.value()); + } + return zv::Val(std::move(list)); + } + + bool allowsDynamicProperties(bool &out) + { + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return false; + if (isEnum_) { + out = false; + return true; + } + + zv::Val deprecates = callService(PT_CR_PROP_PHP_VERSION, "phpVersion", PT_LC("deprecatesdynamicproperties"), 0, NULL); + if (UNEXPECTED(deprecates.isUndef())) return false; + if (!zend_is_true(deprecates.raw())) { + out = true; + return true; + } + + static zend_string *magicGet = nullptr, *magicSet = nullptr, *magicIsset = nullptr; + bool hasMagicMethod; + if (UNEXPECTED(!hasNativeMethod(literal(magicGet, PT_LC("__get")), hasMagicMethod))) return false; + if (!hasMagicMethod && UNEXPECTED(!hasNativeMethod(literal(magicSet, PT_LC("__set")), hasMagicMethod))) return false; + if (!hasMagicMethod && UNEXPECTED(!hasNativeMethod(literal(magicIsset, PT_LC("__isset")), hasMagicMethod))) return false; + if (hasMagicMethod) { + out = true; + return true; + } + + zv::Val requireExtendsTags = getRequireExtendsTags(); + if (UNEXPECTED(requireExtendsTags.isUndef())) return false; + if (requireExtendsTags.ref().isArray()) { + for (auto entry : zv::ArrRef(requireExtendsTags.raw())) { + zv::Val type = callOn(entry.value(), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return false; + if (!instanceOfShadowed(type.ref(), pt_ce_object_type, PT_LC(PT_CR_OBJECT_TYPE_NAME))) continue; + + zv::Val reflection = typeGetClassReflection(type.ref()); + if (UNEXPECTED(reflection.isUndef())) return false; + if (reflection.isNull()) continue; + bool allows; + if (UNEXPECTED(!crAllowsDynamicProperties(reflection.ref(), allows))) return false; + if (!allows) continue; + + out = true; + return true; + } + } + + bool readOnly; + if (UNEXPECTED(!isReadOnly(readOnly))) return false; + if (readOnly) { + out = false; + return true; + } + + zend_object *provider = service(PT_CR_PROP_REFLECTION_PROVIDER, "reflectionProvider"); + if (UNEXPECTED(provider == NULL)) return false; + zv::Args crateArgs{provider, self}; + zv::Val isCrate = pt_type_call_static(PT_CLASS_UNIVERSAL_OBJECT_CRATES_CLASS_REFLECTION_EXTENSION, PT_LC("isuniversalobjectcrate"), 2, crateArgs); + if (UNEXPECTED(isCrate.isUndef())) return false; + if (zend_is_true(isCrate.raw())) { + out = true; + return true; + } + + static zend_string *allowDynamicProperties = nullptr; + zval attributeName; + ZVAL_STR(&attributeName, literal(allowDynamicProperties, PT_LC("AllowDynamicProperties"))); + zval selfZv; + ZVAL_OBJ(&selfZv, self); + zv::Val cls = zv::Val::copyOf(zv::Ref(&selfZv)); + zv::Val attributes; + do { + zv::Val reflection = crReflection(cls.ref()); + if (UNEXPECTED(reflection.isUndef())) return false; + attributes = callOn(reflection.ref(), PT_LC("getattributes"), 1, &attributeName); + if (UNEXPECTED(attributes.isUndef())) return false; + cls = crGetParentClass(cls.ref()); + if (UNEXPECTED(cls.isUndef())) return false; + } while (attributes.ref().isArray() && countOf(attributes.ref()) == 0 && !cls.isNull()); + + out = !(attributes.ref().isArray() && countOf(attributes.ref()) == 0); + return true; + } + + /** @deprecated Use hasInstanceProperty or hasStaticProperty instead */ + bool hasProperty(zend_string *propertyName, bool &out) + { + zval *cached = memoFind(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName); + if (cached != NULL) { + out = zend_is_true(cached); + return true; + } + + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return false; + if (isEnum_) { + bool hasNative; + if (UNEXPECTED(!hasNativeProperty(propertyName, hasNative))) return false; + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, hasNative); + return true; + } + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return false; + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, true); + return true; + } + + bool allowsDynamic; + if (UNEXPECTED(!allowsDynamicProperties(allowsDynamic))) return false; + if (allowsDynamic) { + zv::Val extensions = propertiesClassReflectionExtensions(); + if (UNEXPECTED(extensions.isUndef())) return false; + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + if (UNEXPECTED(!extensionBool(entry.value(), PT_LC("hasproperty"), propertyName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, true); + return true; + } + } + } + } + + zv::Val requireExtends = requireExtendsPropertyClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return false; + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasproperty"), propertyName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, true); + return true; + } + + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, false); + return true; + } + + bool hasInstanceProperty(zend_string *propertyName, bool &out) + { + zval *cached = memoFind(PT_CR_PROP_HAS_INSTANCE_PROPERTY_CACHE, propertyName); + if (cached != NULL) { + out = zend_is_true(cached); + return true; + } + + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return false; + if (isEnum_) { + bool hasNative; + if (UNEXPECTED(!hasNativeProperty(propertyName, hasNative))) return false; + out = memoSetBool(PT_CR_PROP_HAS_INSTANCE_PROPERTY_CACHE, propertyName, hasNative); + return true; + } + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return false; + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return false; + if (has) { + zv::Val property = extensionCall(phpExtension.ref(), PT_LC("getnativeproperty"), propertyName); + if (UNEXPECTED(property.isUndef())) return false; + bool isStatic; + if (UNEXPECTED(!callBool(property.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return false; + if (!isStatic) { + out = memoSetBool(PT_CR_PROP_HAS_INSTANCE_PROPERTY_CACHE, propertyName, true); + return true; + } + } + + bool allowsDynamic; + if (UNEXPECTED(!allowsDynamicProperties(allowsDynamic))) return false; + if (allowsDynamic) { + zv::Val extensions = propertiesClassReflectionExtensions(); + if (UNEXPECTED(extensions.isUndef())) return false; + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + if (UNEXPECTED(!extensionBool(entry.value(), PT_LC("hasproperty"), propertyName, has))) return false; + if (!has) continue; + zv::Val property = extensionCall(entry.value(), PT_LC("getproperty"), propertyName); + if (UNEXPECTED(property.isUndef())) return false; + bool isStatic; + if (UNEXPECTED(!callBool(property.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return false; + if (isStatic) continue; + out = memoSetBool(PT_CR_PROP_HAS_INSTANCE_PROPERTY_CACHE, propertyName, true); + return true; + } + } + } + + /* the twin writes the last two answers into $this->hasPropertyCache + * (not the instance cache) — kept as it is */ + zv::Val requireExtends = requireExtendsPropertyClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return false; + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasinstanceproperty"), propertyName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, true); + return true; + } + + out = memoSetBool(PT_CR_PROP_HAS_PROPERTY_CACHE, propertyName, false); + return true; + } + + bool hasStaticProperty(zend_string *propertyName, bool &out) + { + zval *cached = memoFind(PT_CR_PROP_HAS_STATIC_PROPERTY_CACHE, propertyName); + if (cached != NULL) { + out = zend_is_true(cached); + return true; + } + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return false; + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return false; + if (has) { + zv::Val property = extensionCall(phpExtension.ref(), PT_LC("getnativeproperty"), propertyName); + if (UNEXPECTED(property.isUndef())) return false; + bool isStatic; + if (UNEXPECTED(!callBool(property.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return false; + if (isStatic) { + out = memoSetBool(PT_CR_PROP_HAS_STATIC_PROPERTY_CACHE, propertyName, true); + return true; + } + } + + zv::Val requireExtends = requireExtendsPropertyClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return false; + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasstaticproperty"), propertyName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_STATIC_PROPERTY_CACHE, propertyName, true); + return true; + } + + out = memoSetBool(PT_CR_PROP_HAS_STATIC_PROPERTY_CACHE, propertyName, false); + return true; + } + + bool hasMethod(zend_string *methodName, bool &out) + { + zval *cached = memoFind(PT_CR_PROP_HAS_METHOD_CACHE, methodName); + if (cached != NULL) { + out = zend_is_true(cached); + return true; + } + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return false; + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasmethod"), methodName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_METHOD_CACHE, methodName, true); + return true; + } + + zv::Val extensions = methodsClassReflectionExtensions(); + if (UNEXPECTED(extensions.isUndef())) return false; + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + if (UNEXPECTED(!extensionBool(entry.value(), PT_LC("hasmethod"), methodName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_METHOD_CACHE, methodName, true); + return true; + } + } + } + + zv::Val requireExtends = requireExtendsMethodsClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return false; + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasmethod"), methodName, has))) return false; + if (has) { + out = memoSetBool(PT_CR_PROP_HAS_METHOD_CACHE, methodName, true); + return true; + } + + out = memoSetBool(PT_CR_PROP_HAS_METHOD_CACHE, methodName, false); + return true; + } + + /* the `$key` of getMethod() / getProperty() / getInstanceProperty(): + * the member name, suffixed by the scope's class cache key inside a + * class; UNDEF = pending exception */ + static zv::Val memberKey(zend_string *memberName, zval *scope) + { + bool inClass; + if (UNEXPECTED(!pt_scope_is_in_class(Z_OBJ_P(scope), inClass))) return zv::Val(); + if (!inClass) return zv::Val::string(memberName); + zv::Val classReflection = pt_scope_get_class_reflection(Z_OBJ_P(scope)); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + zv::Val cacheKey = crGetCacheKey(classReflection.ref()); + if (UNEXPECTED(cacheKey.isUndef())) return zv::Val(); + zend_string *cacheKeyStr = zval_get_string(cacheKey.raw()); + zv::Val key = zv::Val::adoptString(zend_strpprintf(0, "%s-%s", ZSTR_VAL(memberName), ZSTR_VAL(cacheKeyStr))); + zend_string_release(cacheKeyStr); + return key; + } + + zv::Val getMethod(zend_string *methodName, zval *scope) + { + zv::Val keyVal = memberKey(methodName, scope); + if (UNEXPECTED(keyVal.isUndef())) return zv::Val(); + zend_string *key = Z_STR_P(keyVal.raw()); + + zval *cached = memoFind(PT_CR_PROP_METHODS, key); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return zv::Val(); + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasmethod"), methodName, has))) return zv::Val(); + if (has) { + zv::Val method = extensionCall(phpExtension.ref(), PT_LC("getmethod"), methodName); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + bool canCall; + if (UNEXPECTED(!callBool(zv::Ref(scope), PT_LC("cancallmethod"), 1, method.raw(), canCall))) return zv::Val(); + memoSet(PT_CR_PROP_METHODS, key, zv::Val::copyOf(method.ref())); + if (canCall) return method; + } + + zv::Val extensions = methodsClassReflectionExtensions(); + if (UNEXPECTED(extensions.isUndef())) return zv::Val(); + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + if (UNEXPECTED(!extensionBool(entry.value(), PT_LC("hasmethod"), methodName, has))) return zv::Val(); + if (!has) continue; + + zv::Val naked = extensionCall(entry.value(), PT_LC("getmethod"), methodName); + if (UNEXPECTED(naked.isUndef())) return zv::Val(); + zv::Val method = wrapExtendedMethod(std::move(naked)); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + bool canCall; + if (UNEXPECTED(!callBool(zv::Ref(scope), PT_LC("cancallmethod"), 1, method.raw(), canCall))) return zv::Val(); + memoSet(PT_CR_PROP_METHODS, key, zv::Val::copyOf(method.ref())); + if (canCall) return method; + } + } + + if (!memoIsset(PT_CR_PROP_METHODS, key)) { + zv::Val requireExtends = requireExtendsMethodsClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return zv::Val(); + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasmethod"), methodName, has))) return zv::Val(); + if (has) { + zv::Val method = extensionCall(requireExtends.ref(), PT_LC("getmethod"), methodName); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + memoSet(PT_CR_PROP_METHODS, key, std::move(method)); + } + } + + if (!memoIsset(PT_CR_PROP_METHODS, key)) { + throwMissingMethod(methodName); + return zv::Val(); + } + + return zv::Val::copyOf(zv::Ref(memoFind(PT_CR_PROP_METHODS, key))); + } + + /* private */ + static zv::Val wrapExtendedMethod(zv::Val method) + { + bool isExtended; + if (UNEXPECTED(!pt_type_instanceof(method.raw(), PT_CLASS_EXTENDED_METHOD_REFLECTION, isExtended))) return zv::Val(); + if (isExtended) return method; + + return pt_type_new(PT_CLASS_WRAPPED_EXTENDED_METHOD_REFLECTION, 1, method.raw()); + } + + /* private */ + static zv::Val wrapExtendedProperty(zend_string *propertyName, zv::Val property) + { + bool isExtended; + if (UNEXPECTED(!pt_type_instanceof(property.raw(), PT_CLASS_EXTENDED_PROPERTY_REFLECTION, isExtended))) return zv::Val(); + if (isExtended) return property; + + zv::Args args{propertyName, property.raw()}; + return pt_type_new(PT_CLASS_WRAPPED_EXTENDED_PROPERTY_REFLECTION, 2, args); + } + + bool hasNativeMethod(zend_string *methodName, bool &out) + { + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return false; + return extensionBool(phpExtension.ref(), PT_LC("hasnativemethod"), methodName, out); + } + + zv::Val getNativeMethod(zend_string *methodName) + { + bool has; + if (UNEXPECTED(!hasNativeMethod(methodName, has))) return zv::Val(); + if (!has) { + throwMissingMethod(methodName); + return zv::Val(); + } + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return zv::Val(); + return extensionCall(phpExtension.ref(), PT_LC("getnativemethod"), methodName); + } + + bool hasConstructor(bool &out) + { + zv::Val constructor = findConstructor(); + if (UNEXPECTED(constructor.isUndef())) return false; + out = !constructor.isNull(); + return true; + } + + zv::Val getConstructor() + { + zv::Val constructor = findConstructor(); + if (UNEXPECTED(constructor.isUndef())) return zv::Val(); + if (constructor.isNull()) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Val name = callOn(constructor.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + zv::Val method = getNativeMethod(nameStr); + zend_string_release(nameStr); + return method; + } + + /* private; the adapter's ReflectionMethod or null */ + zv::Val findConstructor() + { + zv::Val constructor = reflectionCall(PT_LC("getconstructor"), 0, NULL); + if (UNEXPECTED(constructor.isUndef())) return zv::Val(); + if (constructor.isNull()) return zv::Val::null(); + + zv::Val legacy = callService(PT_CR_PROP_PHP_VERSION, "phpVersion", PT_LC("supportslegacyconstructor"), 0, NULL); + if (UNEXPECTED(legacy.isUndef())) return zv::Val(); + if (zend_is_true(legacy.raw())) return constructor; + + zv::Val name = callOn(constructor.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + zend_string *lower = zend_string_tolower(nameStr); + bool isConstruct = zend_string_equals_literal(lower, "__construct"); + zend_string_release(lower); + zend_string_release(nameStr); + if (!isConstruct) return zv::Val::null(); + + return constructor; + } + + /** @internal */ + bool evictPrivateSymbols() + { + static const uint32_t tables[] = { PT_CR_PROP_CONSTANTS, PT_CR_PROP_PROPERTIES, PT_CR_PROP_INSTANCE_PROPERTIES, PT_CR_PROP_STATIC_PROPERTIES, PT_CR_PROP_METHODS }; + for (uint32_t index : tables) { + zval *table = OBJ_PROP_NUM(self, index); + if (Z_TYPE_P(table) != IS_ARRAY) continue; + /* foreach iterates the array as it was; the unset()s separate + * the slot's table from that snapshot */ + zv::Val snapshot = zv::Val::copyOf(zv::Ref(table)); + for (auto entry : zv::ArrRef(snapshot.raw())) { + bool isPrivate; + if (UNEXPECTED(!callBool(entry.value(), PT_LC("isprivate"), 0, NULL, isPrivate))) return false; + if (!isPrivate) continue; + SEPARATE_ARRAY(table); + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + zend_hash_del(Z_ARRVAL_P(table), key); + } else { + zend_hash_index_del(Z_ARRVAL_P(table), entry.indexKey()); + } + } + } + /* PhpClassReflectionExtension's member caches are governed by their + * own LRU instead of per-class private-symbol eviction */ + return true; + } + + /** @deprecated Use getInstanceProperty or getStaticProperty */ + zv::Val getProperty(zend_string *propertyName, zval *scope) + { + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return zv::Val(); + if (isEnum_) return getNativeProperty(propertyName); + + zv::Val keyVal = memberKey(propertyName, scope); + if (UNEXPECTED(keyVal.isUndef())) return zv::Val(); + zend_string *key = Z_STR_P(keyVal.raw()); + + zval *cached = memoFind(PT_CR_PROP_PROPERTIES, key); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return zv::Val(); + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (has) { + /* $this->classReflectionExtensionRegistryProvider->getRegistry()->getPhpClassReflectionExtension()->getProperty($this, $propertyName, $scope) */ + zv::Val freshExtension = phpClassReflectionExtension(); + if (UNEXPECTED(freshExtension.isUndef())) return zv::Val(); + zv::Val property = extensionGetProperty(freshExtension.ref(), propertyName, scope); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + bool canRead; + if (UNEXPECTED(!callBool(zv::Ref(scope), PT_LC("canreadproperty"), 1, property.raw(), canRead))) return zv::Val(); + memoSet(PT_CR_PROP_PROPERTIES, key, zv::Val::copyOf(property.ref())); + if (canRead) return property; + } + + bool allowsDynamic; + if (UNEXPECTED(!allowsDynamicProperties(allowsDynamic))) return zv::Val(); + if (allowsDynamic) { + zv::Val extensions = propertiesClassReflectionExtensions(); + if (UNEXPECTED(extensions.isUndef())) return zv::Val(); + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + if (UNEXPECTED(!extensionBool(entry.value(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (!has) continue; + + zv::Val naked = extensionCall(entry.value(), PT_LC("getproperty"), propertyName); + if (UNEXPECTED(naked.isUndef())) return zv::Val(); + zv::Val property = wrapExtendedProperty(propertyName, std::move(naked)); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + bool canRead; + if (UNEXPECTED(!callBool(zv::Ref(scope), PT_LC("canreadproperty"), 1, property.raw(), canRead))) return zv::Val(); + memoSet(PT_CR_PROP_PROPERTIES, key, zv::Val::copyOf(property.ref())); + if (canRead) return property; + } + } + } + + /* For BC purpose */ + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (has) { + zv::Val property = extensionGetProperty(phpExtension.ref(), propertyName, scope); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + memoSet(PT_CR_PROP_PROPERTIES, key, zv::Val::copyOf(property.ref())); + return property; + } + + if (!memoIsset(PT_CR_PROP_PROPERTIES, key)) { + zv::Val requireExtends = requireExtendsPropertyClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return zv::Val(); + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (has) { + zv::Val property = extensionCall(requireExtends.ref(), PT_LC("getproperty"), propertyName); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + memoSet(PT_CR_PROP_PROPERTIES, key, std::move(property)); + } + } + + if (!memoIsset(PT_CR_PROP_PROPERTIES, key)) { + throwMissingProperty(propertyName); + return zv::Val(); + } + + return zv::Val::copyOf(zv::Ref(memoFind(PT_CR_PROP_PROPERTIES, key))); + } + + /* $phpClassReflectionExtension->getProperty($this, $propertyName, $scope) */ + zv::Val extensionGetProperty(zv::Ref extension, zend_string *propertyName, zval *scope) + { + zv::Args args{self, propertyName, scope}; + return callOn(extension, PT_LC("getproperty"), 3, args); + } + + zv::Val getInstanceProperty(zend_string *propertyName, zval *scope) + { + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return zv::Val(); + if (isEnum_) return getNativeProperty(propertyName); + + zv::Val keyVal = memberKey(propertyName, scope); + if (UNEXPECTED(keyVal.isUndef())) return zv::Val(); + zend_string *key = Z_STR_P(keyVal.raw()); + + if (!memoIsset(PT_CR_PROP_INSTANCE_PROPERTIES, key)) { + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return zv::Val(); + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (has) { + zv::Val property = extensionGetProperty(phpExtension.ref(), propertyName, scope); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + bool isStatic; + if (UNEXPECTED(!callBool(property.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return zv::Val(); + if (!isStatic) { + bool canRead; + if (UNEXPECTED(!callBool(zv::Ref(scope), PT_LC("canreadproperty"), 1, property.raw(), canRead))) return zv::Val(); + memoSet(PT_CR_PROP_INSTANCE_PROPERTIES, key, zv::Val::copyOf(property.ref())); + if (canRead) return property; + } + } + + bool allowsDynamic; + if (UNEXPECTED(!allowsDynamicProperties(allowsDynamic))) return zv::Val(); + if (allowsDynamic) { + zv::Val extensions = propertiesClassReflectionExtensions(); + if (UNEXPECTED(extensions.isUndef())) return zv::Val(); + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + if (UNEXPECTED(!extensionBool(entry.value(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (!has) continue; + + zv::Val naked = extensionCall(entry.value(), PT_LC("getproperty"), propertyName); + if (UNEXPECTED(naked.isUndef())) return zv::Val(); + bool isStatic; + if (UNEXPECTED(!callBool(naked.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return zv::Val(); + if (isStatic) continue; + + zv::Val property = wrapExtendedProperty(propertyName, std::move(naked)); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + bool canRead; + if (UNEXPECTED(!callBool(zv::Ref(scope), PT_LC("canreadproperty"), 1, property.raw(), canRead))) return zv::Val(); + memoSet(PT_CR_PROP_INSTANCE_PROPERTIES, key, zv::Val::copyOf(property.ref())); + if (canRead) return property; + } + } + } + } + + if (!memoIsset(PT_CR_PROP_INSTANCE_PROPERTIES, key)) { + zv::Val requireExtends = requireExtendsPropertyClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return zv::Val(); + bool has; + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasinstanceproperty"), propertyName, has))) return zv::Val(); + if (has) { + zv::Val property = extensionCall(requireExtends.ref(), PT_LC("getinstanceproperty"), propertyName); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + memoSet(PT_CR_PROP_INSTANCE_PROPERTIES, key, std::move(property)); + } + } + + if (!memoIsset(PT_CR_PROP_INSTANCE_PROPERTIES, key)) { + throwMissingProperty(propertyName); + return zv::Val(); + } + + return zv::Val::copyOf(zv::Ref(memoFind(PT_CR_PROP_INSTANCE_PROPERTIES, key))); + } + + zv::Val getStaticProperty(zend_string *propertyName) + { + zend_string *key = propertyName; + if (memoIsset(PT_CR_PROP_STATIC_PROPERTIES, key)) return zv::Val::copyOf(zv::Ref(memoFind(PT_CR_PROP_STATIC_PROPERTIES, key))); + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return zv::Val(); + bool has; + if (UNEXPECTED(!extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, has))) return zv::Val(); + if (has) { + zv::Val outOfClassScope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL); + if (UNEXPECTED(outOfClassScope.isUndef())) return zv::Val(); + zv::Val naked = extensionGetProperty(phpExtension.ref(), propertyName, outOfClassScope.raw()); + if (UNEXPECTED(naked.isUndef())) return zv::Val(); + bool isStatic; + if (UNEXPECTED(!callBool(naked.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return zv::Val(); + if (isStatic) { + zv::Val property = wrapExtendedProperty(propertyName, std::move(naked)); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + if (UNEXPECTED(!callBool(property.ref(), PT_LC("isstatic"), 0, NULL, isStatic))) return zv::Val(); + if (isStatic) { + memoSet(PT_CR_PROP_STATIC_PROPERTIES, key, zv::Val::copyOf(property.ref())); + return property; + } + } + } + + zv::Val requireExtends = requireExtendsPropertyClassReflectionExtension(); + if (UNEXPECTED(requireExtends.isUndef())) return zv::Val(); + if (UNEXPECTED(!extensionBool(requireExtends.ref(), PT_LC("hasstaticproperty"), propertyName, has))) return zv::Val(); + if (has) { + zv::Val property = extensionCall(requireExtends.ref(), PT_LC("getstaticproperty"), propertyName); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + memoSet(PT_CR_PROP_STATIC_PROPERTIES, key, zv::Val::copyOf(property.ref())); + return property; + } + + throwMissingProperty(propertyName); + return zv::Val(); + } + + bool hasNativeProperty(zend_string *propertyName, bool &out) + { + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return false; + return extensionBool(phpExtension.ref(), PT_LC("hasproperty"), propertyName, out); + } + + zv::Val getNativeProperty(zend_string *propertyName) + { + bool has; + if (UNEXPECTED(!hasNativeProperty(propertyName, has))) return zv::Val(); + if (!has) { + throwMissingProperty(propertyName); + return zv::Val(); + } + + zv::Val phpExtension = phpClassReflectionExtension(); + if (UNEXPECTED(phpExtension.isUndef())) return zv::Val(); + return extensionCall(phpExtension.ref(), PT_LC("getnativeproperty"), propertyName); + } + + bool isAbstract(bool &out) const { return reflectionCallBool(PT_LC("isabstract"), out); } + bool isInterface(bool &out) const { return reflectionCallBool(PT_LC("isinterface"), out); } + bool isTrait(bool &out) const { return reflectionCallBool(PT_LC("istrait"), out); } + + /* $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum() + * — the adapter class looked up without autoloading: an undeclared + * class is "no instance of it" */ + bool isEnum(bool &out) const + { + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + if (UNEXPECTED(reflection.isUndef())) { + (void) uninitializedProperty("reflection"); + return false; + } + zend_class_entry *enumCe = pt_class_loaded(PT_CLASS_REFLECTION_ENUM); + if (enumCe == NULL) { + if (UNEXPECTED(EG(exception))) return false; + out = false; + return true; + } + if (!reflection.isObject() || !instanceof_function(reflection.asObject()->ce, enumCe)) { + out = false; + return true; + } + return reflectionCallBool(PT_LC("isenum"), out); + } + + /* 'Interface'|'Trait'|'Enum'|'Class' */ + zv::Val getClassTypeDescription() + { + bool is; + if (UNEXPECTED(!isInterface(is))) return zv::Val(); + if (is) return zv::Val::string(PT_LC("Interface")); + if (UNEXPECTED(!isTrait(is))) return zv::Val(); + if (is) return zv::Val::string(PT_LC("Trait")); + if (UNEXPECTED(!isEnum(is))) return zv::Val(); + if (is) return zv::Val::string(PT_LC("Enum")); + + return zv::Val::string(PT_LC("Class")); + } + + bool isReadOnly(bool &out) const { return reflectionCallBool(PT_LC("isreadonly"), out); } + + bool isBackedEnum(bool &out) const + { + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + if (UNEXPECTED(reflection.isUndef())) { + (void) uninitializedProperty("reflection"); + return false; + } + zend_class_entry *enumCe = pt_class_loaded(PT_CLASS_REFLECTION_ENUM); + if (enumCe == NULL) { + if (UNEXPECTED(EG(exception))) return false; + out = false; + return true; + } + if (!reflection.isObject() || !instanceof_function(reflection.asObject()->ce, enumCe)) { + out = false; + return true; + } + + return reflectionCallBool(PT_LC("isbacked"), out); + } + + /* ?Type — the backing type of a backed enum */ + zv::Val getBackedEnumType() + { + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + if (UNEXPECTED(reflection.isUndef())) return uninitializedProperty("reflection"); + zend_class_entry *enumCe = pt_class_loaded(PT_CLASS_REFLECTION_ENUM); + if (enumCe == NULL) return UNEXPECTED(EG(exception) != NULL) ? zv::Val() : zv::Val::null(); + if (!reflection.isObject() || !instanceof_function(reflection.asObject()->ce, enumCe)) return zv::Val::null(); + + bool backed; + if (UNEXPECTED(!reflectionCallBool(PT_LC("isbacked"), backed))) return zv::Val(); + if (!backed) return zv::Val::null(); + + zv::Val backingType = reflectionCall(PT_LC("getbackingtype"), 0, NULL); + if (UNEXPECTED(backingType.isUndef())) return zv::Val(); + + return pt_typehint_helper_decide_type_from_reflection(backingType.raw()); + } + + bool hasEnumCase(zend_string *name, bool &out) + { + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return false; + if (!isEnum_) { + out = false; + return true; + } + + zval arg; + ZVAL_STR(&arg, name); + zv::Val result = reflectionCall(PT_LC("hascase"), 1, &arg); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* array */ + zv::Val getEnumCases() + { + bool isEnum_ = false; + if (UNEXPECTED(!isEnum(isEnum_))) return zv::Val(); + if (!isEnum_) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + + zv::Ref memo = slot(PT_CR_PROP_ENUM_CASES); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Val initializerExprContext = initializerExprContextFromClassReflection(); + if (UNEXPECTED(initializerExprContext.isUndef())) return zv::Val(); + zv::Val reflectionCases = reflectionCall(PT_LC("getcases"), 0, NULL); + if (UNEXPECTED(reflectionCases.isUndef())) return zv::Val(); + + zv::Arr cases = zv::Arr::create(countOf(reflectionCases.ref())); + if (reflectionCases.ref().isArray()) { + for (auto entry : zv::ArrRef(reflectionCases.raw())) { + zv::Val valueType = enumCaseValueType(entry.value(), initializerExprContext.raw()); + if (UNEXPECTED(valueType.isUndef())) return zv::Val(); + zv::Val caseName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(caseName.isUndef())) return zv::Val(); + zv::Val attributes = enumCaseAttributes(entry.value()); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + zv::Val caseReflection = newEnumCaseReflection(entry.value(), valueType.ref(), attributes.ref()); + if (UNEXPECTED(caseReflection.isUndef())) return zv::Val(); + zend_string *key = zval_get_string(caseName.raw()); + cases.set(key, std::move(caseReflection)); + zend_string_release(key); + } + } + + writeSlot(PT_CR_PROP_ENUM_CASES, zv::Val::copyOf(cases.ref())); + + return zv::Val(std::move(cases)); + } + + zv::Val getEnumCase(zend_string *name) + { + bool has; + if (UNEXPECTED(!hasEnumCase(name, has))) return zv::Val(); + if (!has) { + zv::Val displayName_ = getDisplayName(true); + if (UNEXPECTED(displayName_.isUndef())) return zv::Val(); + zend_string *displayNameStr = zval_get_string(displayName_.raw()); + zval message; + ZVAL_STR(&message, zend_strpprintf(0, "Enum case %s::%s does not exist.", ZSTR_VAL(displayNameStr), ZSTR_VAL(name))); + zend_string_release(displayNameStr); + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 1, &message); + zval_ptr_dtor(&message); + return zv::Val(); + } + + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + zend_class_entry *enumCe = pt_class_loaded(PT_CLASS_REFLECTION_ENUM); + if (UNEXPECTED(EG(exception) != NULL)) return zv::Val(); + if (enumCe == NULL || !reflection.isObject() || !instanceof_function(reflection.asObject()->ce, enumCe)) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + + zv::Ref enumCases = slot(PT_CR_PROP_ENUM_CASES); + if (!enumCases.isNull() && enumCases.isArray()) { + zval *found = zend_symtable_find(enumCases.asArrayTable(), name); + if (found != NULL) return zv::Val::copyOf(zv::Ref(found)); + } + + zval arg; + ZVAL_STR(&arg, name); + zv::Val case_ = reflectionCall(PT_LC("getcase"), 1, &arg); + if (UNEXPECTED(case_.isUndef())) return zv::Val(); + zv::Val valueType = enumCaseValueType(case_.ref(), NULL); + if (UNEXPECTED(valueType.isUndef())) return zv::Val(); + zv::Val attributes = enumCaseAttributes(case_.ref()); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + + return newEnumCaseReflection(case_.ref(), valueType.ref(), attributes.ref()); + } + + /* $case instanceof ReflectionEnumBackedCase && $case->hasBackingValue() + * ? $this->initializerExprTypeResolver->getType($case->getValueExpression(), + * $context) : null — $context NULL where the twin builds it inside the + * branch (getEnumCase()), the loop's shared one otherwise */ + zv::Val enumCaseValueType(zv::Ref case_, zval *context) + { + bool isBackedCase; + if (UNEXPECTED(!pt_type_instanceof(case_.raw(), PT_CLASS_REFLECTION_ENUM_BACKED_CASE, isBackedCase))) return zv::Val(); + if (!isBackedCase) return zv::Val::null(); + bool hasBackingValue; + if (UNEXPECTED(!callBool(case_, PT_LC("hasbackingvalue"), 0, NULL, hasBackingValue))) return zv::Val(); + if (!hasBackingValue) return zv::Val::null(); + + zv::Val valueExpression = callOn(case_, PT_LC("getvalueexpression"), 0, NULL); + if (UNEXPECTED(valueExpression.isUndef())) return zv::Val(); + zv::Val ownContext; + if (context == NULL) { + ownContext = initializerExprContextFromClassReflection(); + if (UNEXPECTED(ownContext.isUndef())) return zv::Val(); + context = ownContext.raw(); + } + + zv::Args args{valueExpression.raw(), context}; + return callService(PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, "initializerExprTypeResolver", PT_LC("gettype"), 2, args); + } + + /* $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), + * InitializerExprContext::fromClass($this->getName(), $this->getFileName())) */ + zv::Val enumCaseAttributes(zv::Ref case_) + { + zv::Val attributes = callOn(case_, PT_LC("getattributes"), 0, NULL); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + zv::Val context = initializerExprContextFromClass(); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Args args{attributes.raw(), context.raw()}; + return callService(PT_CR_PROP_ATTRIBUTE_REFLECTION_FACTORY, "attributeReflectionFactory", PT_LC("fromnativereflection"), 2, args); + } + + /* new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider) */ + zv::Val newEnumCaseReflection(zv::Ref case_, zv::Ref valueType, zv::Ref attributes) + { + zv::Ref deprecationProvider = slot(PT_CR_PROP_DEPRECATION_PROVIDER); + if (UNEXPECTED(deprecationProvider.isUndef())) return uninitializedProperty("deprecationProvider"); + zv::Args args{self, case_.raw(), valueType.raw(), attributes.raw(), deprecationProvider.raw()}; + return pt_type_new(PT_CLASS_ENUM_CASE_REFLECTION, 5, args); + } + + /* InitializerExprContext::fromClassReflection($this) */ + zv::Val initializerExprContextFromClassReflection() + { + return pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclassreflection"), 1, thisZval()); + } + + /* InitializerExprContext::fromClass($this->getName(), $this->getFileName()) */ + zv::Val initializerExprContextFromClass() + { + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Val fileName = getFileName(); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + zv::Args args{name.raw(), fileName.raw()}; + return pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclass"), 2, args); + } + + bool isClass(bool &out) + { + bool is; + if (UNEXPECTED(!isInterface(is))) return false; + if (is) { + out = false; + return true; + } + if (UNEXPECTED(!isTrait(is))) return false; + if (is) { + out = false; + return true; + } + if (UNEXPECTED(!isEnum(is))) return false; + out = !is; + return true; + } + + bool isAnonymous(bool &out) const + { + zv::Ref anonymousFilename = slot(PT_CR_PROP_ANONYMOUS_FILENAME); + if (UNEXPECTED(anonymousFilename.isUndef())) { + (void) uninitializedProperty("anonymousFilename"); + return false; + } + out = !anonymousFilename.isNull(); + return true; + } + + bool is(zend_string *className, bool &out) + { + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return false; + if (Z_TYPE_P(name.raw()) == IS_STRING && zend_string_equals(Z_STR_P(name.raw()), className)) { + out = true; + return true; + } + + bool has; + if (UNEXPECTED(!providerHasClass(className, has))) return false; + if (!has) { + out = false; + return true; + } + + zv::Val classReflection = providerGetClass(className); + if (UNEXPECTED(classReflection.isUndef())) return false; + return isSubclassOfClass(classReflection.ref(), out); + } + + /** @deprecated Use isSubclassOfClass instead. */ + bool isSubclassOf(zend_string *className, bool &out) + { + bool has; + if (UNEXPECTED(!providerHasClass(className, has))) return false; + if (!has) { + out = false; + return true; + } + + zv::Val classReflection = providerGetClass(className); + if (UNEXPECTED(classReflection.isUndef())) return false; + return isSubclassOfClass(classReflection.ref(), out); + } + + bool isSubclassOfClass(zv::Ref classReflection, bool &out) + { + zv::Val cacheKey = crGetCacheKey(classReflection); + if (UNEXPECTED(cacheKey.isUndef())) return false; + zend_string *cacheKeyStr = zval_get_string(cacheKey.raw()); + zv::Str cacheKeyOwned = zv::Str::adopt(cacheKeyStr); + if (memoIsset(PT_CR_PROP_SUBCLASSES, cacheKeyStr)) { + out = zend_is_true(memoFind(PT_CR_PROP_SUBCLASSES, cacheKeyStr)); + return true; + } + + bool finalOrAnonymous = false; + if (UNEXPECTED(!crIsFinalByKeyword(classReflection, finalOrAnonymous))) return false; + if (!finalOrAnonymous && UNEXPECTED(!crIsAnonymous(classReflection, finalOrAnonymous))) return false; + if (finalOrAnonymous) { + out = memoSetBool(PT_CR_PROP_SUBCLASSES, cacheKeyStr, false); + return true; + } + + zv::Val name = crGetName(classReflection); + if (UNEXPECTED(name.isUndef())) return false; + zv::Val result = reflectionCall(PT_LC("issubclassof"), 1, name.raw()); + if (UNEXPECTED(result.isUndef())) { + if (caughtReflectionException()) { + out = memoSetBool(PT_CR_PROP_SUBCLASSES, cacheKeyStr, false); + return true; + } + return false; + } + out = memoSetBool(PT_CR_PROP_SUBCLASSES, cacheKeyStr, zend_is_true(result.raw())); + return true; + } + + bool implementsInterface(zend_string *className, bool &out) + { + zval name; + ZVAL_STR(&name, className); + zv::Val result = reflectionCall(PT_LC("implementsinterface"), 1, &name); + if (UNEXPECTED(result.isUndef())) { + if (caughtReflectionException()) { + out = false; + return true; + } + return false; + } + out = zend_is_true(result.raw()); + return true; + } + + /* list */ + zv::Val getParents() + { + zv::Arr parents = zv::Arr::create(2); + zv::Val parent = getParentClass(); + if (UNEXPECTED(parent.isUndef())) return zv::Val(); + while (!parent.isNull()) { + parents.push(parent.ref()); + zv::Val next = crGetParentClass(parent.ref()); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + parent = std::move(next); + } + + return zv::Val(std::move(parents)); + } + + /* array */ + zv::Val getInterfaces() + { + zv::Ref cached = slot(PT_CR_PROP_CACHED_INTERFACES); + if (!cached.isNull()) return zv::Val::copyOf(cached); + + zv::Val immediateInterfaces = getImmediateInterfaces(); + if (UNEXPECTED(immediateInterfaces.isUndef())) return zv::Val(); + zv::Arr interfaces = zv::Arr::adoptVal(zv::Val::copyOf(immediateInterfaces.ref())); + zv::Val parent = getParentClass(); + if (UNEXPECTED(parent.isUndef())) return zv::Val(); + while (!parent.isNull()) { + zv::Val parentInterfaces = crGetImmediateInterfaces(parent.ref()); + if (UNEXPECTED(parentInterfaces.isUndef())) return zv::Val(); + if (parentInterfaces.ref().isArray()) { + for (auto entry : zv::ArrRef(parentInterfaces.raw())) { + if (UNEXPECTED(!addInterface(interfaces, entry.value()))) return zv::Val(); + if (UNEXPECTED(!addCollectedInterfaces(interfaces, entry.value()))) return zv::Val(); + } + } + + zv::Val next = crGetParentClass(parent.ref()); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + parent = std::move(next); + } + + if (immediateInterfaces.ref().isArray()) { + for (auto entry : zv::ArrRef(immediateInterfaces.raw())) { + if (UNEXPECTED(!addCollectedInterfaces(interfaces, entry.value()))) return zv::Val(); + } + } + + writeSlot(PT_CR_PROP_CACHED_INTERFACES, zv::Val::copyOf(interfaces.ref())); + + return zv::Val(std::move(interfaces)); + } + + /* $interfaces[$interface->getName()] = $interface; */ + static bool addInterface(zv::Arr &interfaces, zv::Ref interface) + { + zv::Val name = crGetName(interface); + if (UNEXPECTED(name.isUndef())) return false; + zend_string *nameStr = zval_get_string(name.raw()); + interfaces.set(nameStr, zv::Val::copyOf(interface)); + zend_string_release(nameStr); + return true; + } + + /* foreach ($this->collectInterfaces($interface) as $i) { $interfaces[$i->getName()] = $i; } */ + bool addCollectedInterfaces(zv::Arr &interfaces, zv::Ref interface) + { + zv::Val collected = collectInterfaces(interface); + if (UNEXPECTED(collected.isUndef())) return false; + for (auto entry : zv::ArrRef(collected.raw())) { + if (UNEXPECTED(!addInterface(interfaces, entry.value()))) return false; + } + return true; + } + + /* private; array — the interfaces an interface + * extends, transitively (the twin's array_pop() stack) */ + zv::Val collectInterfaces(zv::Ref interface) + { + zv::Arr interfaces = zv::Arr::create(4); + std::vector queue; + queue.push_back(zv::Val::copyOf(interface)); + while (!queue.empty()) { + zv::Val current = std::move(queue.back()); + queue.pop_back(); + zv::Val immediate = crGetImmediateInterfaces(current.ref()); + if (UNEXPECTED(immediate.isUndef())) return zv::Val(); + if (!immediate.ref().isArray()) continue; + for (auto entry : zv::ArrRef(immediate.raw())) { + zv::Val name = crGetName(entry.value()); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + bool exists = interfaces.arrRef().exists(nameStr); + if (!exists) { + interfaces.set(nameStr, zv::Val::copyOf(entry.value())); + queue.push_back(zv::Val::copyOf(entry.value())); + } + zend_string_release(nameStr); + } + } + + return zv::Val(std::move(interfaces)); + } + + /* array */ + zv::Val getImmediateInterfaces() + { + /* $indirectInterfaceNames: the interfaces of the parents, and of + * the interfaces' interfaces */ + std::vector indirectInterfaceNames; + zv::Val parent = getParentClass(); + if (UNEXPECTED(parent.isUndef())) return zv::Val(); + while (!parent.isNull()) { + zv::Val parentReflection = crGetNativeReflection(parent.ref()); + if (UNEXPECTED(parentReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(!collectInterfaceNames(parentReflection.ref(), indirectInterfaceNames))) return zv::Val(); + + zv::Val next = crGetParentClass(parent.ref()); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + parent = std::move(next); + } + + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + zv::Val interfaceInterfaces = callOn(nativeReflection.ref(), PT_LC("getinterfaces"), 0, NULL); + if (UNEXPECTED(interfaceInterfaces.isUndef())) return zv::Val(); + if (interfaceInterfaces.ref().isArray()) { + for (auto entry : zv::ArrRef(interfaceInterfaces.raw())) { + if (UNEXPECTED(!collectInterfaceNames(entry.value(), indirectInterfaceNames))) return zv::Val(); + } + } + + bool isInterface_; + if (UNEXPECTED(!isInterface(isInterface_))) return zv::Val(); + zv::Val implementsTags = isInterface_ ? getExtendsTags() : getImplementsTags(); + if (UNEXPECTED(implementsTags.isUndef())) return zv::Val(); + + /* array_diff($this->getNativeReflection()->getInterfaceNames(), $indirectInterfaceNames) */ + zv::Val nativeReflectionAgain = getNativeReflection(); + if (UNEXPECTED(nativeReflectionAgain.isUndef())) return zv::Val(); + zv::Val interfaceNames = callOn(nativeReflectionAgain.ref(), PT_LC("getinterfacenames"), 0, NULL); + if (UNEXPECTED(interfaceNames.isUndef())) return zv::Val(); + zv::Arr immediateInterfaces = zv::Arr::create(4); + if (!interfaceNames.ref().isArray()) return zv::Val(std::move(immediateInterfaces)); + for (auto entry : zv::ArrRef(interfaceNames.raw())) { + zend_string *interfaceName = zval_get_string(entry.value().raw()); + zv::Str interfaceNameOwned = zv::Str::adopt(interfaceName); + bool indirect = false; + for (const zv::Str &indirectName : indirectInterfaceNames) { + if (zend_string_equals(indirectName.get(), interfaceName)) { + indirect = true; + break; + } + } + if (indirect) continue; + + bool has; + if (UNEXPECTED(!providerHasClass(interfaceName, has))) return zv::Val(); + if (!has) continue; + + zv::Val immediateInterface = providerGetClass(interfaceName); + if (UNEXPECTED(immediateInterface.isUndef())) return zv::Val(); + zv::Val immediateName = crGetName(immediateInterface.ref()); + if (UNEXPECTED(immediateName.isUndef())) return zv::Val(); + zend_string *immediateNameStr = zval_get_string(immediateName.raw()); + zv::Str immediateNameOwned = zv::Str::adopt(immediateNameStr); + + zval *implementsTag = implementsTags.ref().isArray() ? zend_symtable_find(implementsTags.ref().asArrayTable(), immediateNameStr) : NULL; + if (implementsTag != NULL) { + zv::Val implementedType = callOn(zv::Ref(implementsTag), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(implementedType.isUndef())) return zv::Val(); + bool generic; + if (UNEXPECTED(!isGeneric(generic))) return zv::Val(); + if (generic) { + implementedType = resolveAncestorTemplateTypes(implementedType.ref(), true); + if (UNEXPECTED(implementedType.isUndef())) return zv::Val(); + } + + if (instanceOfShadowed(implementedType.ref(), pt_ce_generic_object_type, PT_LC(PT_CR_GENERIC_OBJECT_TYPE_NAME))) { + zv::Val reflection = typeGetClassReflection(implementedType.ref()); + if (UNEXPECTED(reflection.isUndef())) return zv::Val(); + if (!reflection.isNull()) { + immediateInterfaces.set(immediateNameStr, std::move(reflection)); + continue; + } + } + } + + bool immediateGeneric; + if (UNEXPECTED(!crIsGeneric(immediateInterface.ref(), immediateGeneric))) return zv::Val(); + if (immediateGeneric) { + zv::Val withErrorTypes = crWithErrorTypes(immediateInterface.ref()); + if (UNEXPECTED(withErrorTypes.isUndef())) return zv::Val(); + immediateInterfaces.set(immediateNameStr, std::move(withErrorTypes)); + continue; + } + + immediateInterfaces.set(immediateNameStr, std::move(immediateInterface)); + } + + return zv::Val(std::move(immediateInterfaces)); + } + + /* foreach ($reflection->getInterfaceNames() as $name) { $names[] = $name; } */ + static bool collectInterfaceNames(zv::Ref reflection, std::vector &names) + { + zv::Val interfaceNames = callOn(reflection, PT_LC("getinterfacenames"), 0, NULL); + if (UNEXPECTED(interfaceNames.isUndef())) return false; + if (!interfaceNames.ref().isArray()) return true; + for (auto entry : zv::ArrRef(interfaceNames.raw())) { + names.push_back(zv::Str::adopt(zval_get_string(entry.value().raw()))); + } + return true; + } + + /* {{{ traits, constants, the PHPDoc machinery, generics */ + + /* array */ + zv::Val getTraits(bool recursive) + { + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + + zv::Val source; + if (recursive) { + zv::Val collected = collectTraits(nativeReflection.ref()); + if (UNEXPECTED(collected.isUndef())) return zv::Val(); + zv::Arr keyed = zv::Arr::create(countOf(collected.ref())); + if (collected.ref().isArray()) { + for (auto entry : zv::ArrRef(collected.raw())) { + zv::Val name = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + keyed.set(nameStr, zv::Val::copyOf(entry.value())); + zend_string_release(nameStr); + } + } + source = zv::Val(std::move(keyed)); + } else { + source = callOn(nativeReflection.ref(), PT_LC("gettraits"), 0, NULL); + if (UNEXPECTED(source.isUndef())) return zv::Val(); + } + + /* array_map(fn (ReflectionClass $trait) => $this->reflectionProvider->getClass($trait->getName()), $traits) */ + zv::Arr traits = zv::Arr::create(countOf(source.ref())); + if (source.ref().isArray()) { + for (auto entry : zv::ArrRef(source.raw())) { + zv::Val name = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + zv::Val classReflection = providerGetClass(nameStr); + zend_string_release(nameStr); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (entry.stringKeyOrNull() != NULL) { + traits.set(entry.stringKeyOrNull(), std::move(classReflection)); + } else { + traits.arrRef().setIndex(entry.indexKey(), classReflection.ref()); + } + } + } + + if (!recursive) return zv::Val(std::move(traits)); + + zv::Val parent = getParentClass(); + if (UNEXPECTED(parent.isUndef())) return zv::Val(); + if (parent.isNull()) return zv::Val(std::move(traits)); + + zv::Val parentTraits = crGetTraits(parent.ref(), true); + if (UNEXPECTED(parentTraits.isUndef())) return zv::Val(); + + return arrayMerge(traits.ref(), parentTraits.ref()); + } + + /* list */ + zv::Val getParentClassesNames() + { + zv::Arr parentNames = zv::Arr::create(2); + zv::Val parentClass = getParentClass(); + if (UNEXPECTED(parentClass.isUndef())) return zv::Val(); + while (!parentClass.isNull()) { + zv::Val name = crGetName(parentClass.ref()); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + parentNames.push(std::move(name)); + zv::Val next = crGetParentClass(parentClass.ref()); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + parentClass = std::move(next); + } + + return zv::Val(std::move(parentNames)); + } + + bool hasConstant(zend_string *name, bool &out) + { + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return false; + zval arg; + ZVAL_STR(&arg, name); + bool has; + if (UNEXPECTED(!callBool(nativeReflection.ref(), PT_LC("hasconstant"), 1, &arg, has))) return false; + if (!has) { + out = false; + return true; + } + + zv::Val reflectionConstant = callOn(nativeReflection.ref(), PT_LC("getreflectionconstant"), 1, &arg); + if (UNEXPECTED(reflectionConstant.isUndef())) return false; + if (reflectionConstant.ref().isFalse()) { + out = false; + return true; + } + + zv::Val declaringClassName = constantDeclaringClassName(reflectionConstant.ref()); + if (UNEXPECTED(declaringClassName.isUndef())) return false; + zend_string *nameStr = zval_get_string(declaringClassName.raw()); + bool ok = providerHasClass(nameStr, out); + zend_string_release(nameStr); + return ok; + } + + /* $reflectionConstant->getDeclaringClass()->getName() */ + static zv::Val constantDeclaringClassName(zv::Ref reflectionConstant) + { + zv::Val declaringClass = callOn(reflectionConstant, PT_LC("getdeclaringclass"), 0, NULL); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + return callOn(declaringClass.ref(), PT_LC("getname"), 0, NULL); + } + + zv::Val getConstant(zend_string *name) + { + if (!memoIsset(PT_CR_PROP_CONSTANTS, name)) { + zv::Val constant = createConstant(name); + if (UNEXPECTED(constant.isUndef())) return zv::Val(); + memoSet(PT_CR_PROP_CONSTANTS, name, std::move(constant)); + } + + zval *found = memoFind(PT_CR_PROP_CONSTANTS, name); + if (UNEXPECTED(found == NULL)) return zv::Val::null(); + return zv::Val::copyOf(zv::Ref(found)); + } + + /* the twin's getConstant() body up to the memo write */ + zv::Val createConstant(zend_string *name) + { + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + zval nameArg; + ZVAL_STR(&nameArg, name); + zv::Val reflectionConstant = callOn(nativeReflection.ref(), PT_LC("getreflectionconstant"), 1, &nameArg); + if (UNEXPECTED(reflectionConstant.isUndef())) return zv::Val(); + if (reflectionConstant.ref().isFalse()) { + zv::Val className = getName(); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Args args{className.raw(), name}; + throwNew(PT_CLASS_MISSING_CONSTANT_FROM_REFLECTION_EXCEPTION, 2, args); + return zv::Val(); + } + + /* $deprecation = $this->deprecationProvider->getClassConstantDeprecation($reflectionConstant) */ + zval constantArg; + ZVAL_COPY_VALUE(&constantArg, reflectionConstant.raw()); + zv::Val deprecation = callService(PT_CR_PROP_DEPRECATION_PROVIDER, "deprecationProvider", PT_LC("getclassconstantdeprecation"), 1, &constantArg); + if (UNEXPECTED(deprecation.isUndef())) return zv::Val(); + zv::Val deprecatedDescription = zv::Val::null(); + bool isDeprecated_ = !deprecation.isNull(); + if (isDeprecated_) { + deprecatedDescription = callOn(deprecation.ref(), PT_LC("getdescription"), 0, NULL); + if (UNEXPECTED(deprecatedDescription.isUndef())) return zv::Val(); + } + + zv::Val declaringClassName = constantDeclaringClassName(reflectionConstant.ref()); + if (UNEXPECTED(declaringClassName.isUndef())) return zv::Val(); + zend_string *declaringClassNameStr = zval_get_string(declaringClassName.raw()); + zv::Val declaringClass = getAncestorWithClassName(declaringClassNameStr); + zend_string_release(declaringClassNameStr); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + if (declaringClass.isNull()) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + + zv::Val fileName = crGetFileName(declaringClass.ref()); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + zv::Val phpDocType = zv::Val::null(); + zv::Val currentResolvedPhpDoc = findConstantResolvedPhpDoc(reflectionConstant.ref()); + if (UNEXPECTED(currentResolvedPhpDoc.isUndef())) return zv::Val(); + + zv::Val nativeType = constantNativeType(reflectionConstant.ref(), declaringClass.ref(), name); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + + /* $this->phpDocInheritanceResolver->resolvePhpDocForConstant($declaringClass, $name, $currentResolvedPhpDoc) */ + zv::Args resolveArgs{declaringClass.raw(), name, currentResolvedPhpDoc.raw()}; + zv::Val resolvedPhpDoc = callService(PT_CR_PROP_PHP_DOC_INHERITANCE_RESOLVER, "phpDocInheritanceResolver", PT_LC("resolvephpdocforconstant"), 3, resolveArgs); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + + bool isInternal_ = false; + bool isFinal_ = false; + if (!resolvedPhpDoc.isNull()) { + if (!isDeprecated_) { + zv::Val deprecatedTag = callOn(resolvedPhpDoc.ref(), PT_LC("getdeprecatedtag"), 0, NULL); + if (UNEXPECTED(deprecatedTag.isUndef())) return zv::Val(); + if (!deprecatedTag.isNull()) { + deprecatedDescription = callOn(deprecatedTag.ref(), PT_LC("getmessage"), 0, NULL); + if (UNEXPECTED(deprecatedDescription.isUndef())) return zv::Val(); + } else { + deprecatedDescription = zv::Val::null(); + } + if (UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isdeprecated"), 0, NULL, isDeprecated_))) return zv::Val(); + } + if (UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isinternal"), 0, NULL, isInternal_))) return zv::Val(); + if (UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isfinal"), 0, NULL, isFinal_))) return zv::Val(); + phpDocType = resolveConstantVarPhpDocType(resolvedPhpDoc.ref(), nativeType.ref(), declaringClass.ref()); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + } + + /* $this->attributeReflectionFactory->fromNativeReflection($reflectionConstant->getAttributes(), + * InitializerExprContext::fromClass($declaringClass->getName(), $fileName)) */ + zv::Val attributes = callOn(reflectionConstant.ref(), PT_LC("getattributes"), 0, NULL); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + zv::Val declaringName = crGetName(declaringClass.ref()); + if (UNEXPECTED(declaringName.isUndef())) return zv::Val(); + zv::Args contextArgs{declaringName.raw(), fileName.raw()}; + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclass"), 2, contextArgs); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Args attributeArgs{attributes.raw(), context.raw()}; + zv::Val attributeReflections = callService(PT_CR_PROP_ATTRIBUTE_REFLECTION_FACTORY, "attributeReflectionFactory", PT_LC("fromnativereflection"), 2, attributeArgs); + if (UNEXPECTED(attributeReflections.isUndef())) return zv::Val(); + + zv::Ref initializerExprTypeResolver = slot(PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER); + if (UNEXPECTED(initializerExprTypeResolver.isUndef())) return uninitializedProperty("initializerExprTypeResolver"); + + zval args[11]; + ZVAL_COPY_VALUE(&args[0], initializerExprTypeResolver.raw()); + ZVAL_COPY_VALUE(&args[1], declaringClass.raw()); + ZVAL_COPY_VALUE(&args[2], reflectionConstant.raw()); + ZVAL_COPY_VALUE(&args[3], nativeType.raw()); + ZVAL_COPY_VALUE(&args[4], phpDocType.raw()); + ZVAL_COPY_VALUE(&args[5], resolvedPhpDoc.raw()); + ZVAL_COPY_VALUE(&args[6], deprecatedDescription.raw()); + ZVAL_BOOL(&args[7], isDeprecated_); + ZVAL_BOOL(&args[8], isInternal_); + ZVAL_BOOL(&args[9], isFinal_); + ZVAL_COPY_VALUE(&args[10], attributeReflections.raw()); + + return pt_type_new(PT_CLASS_REAL_CLASS_CLASS_CONSTANT_REFLECTION, 11, args); + } + + /* $reflectionConstant->getType() !== null + * ? TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $declaringClass) + * : ($this->signatureMapProvider->hasClassConstantMetadata(...) ? ...['nativeType'] : null) */ + zv::Val constantNativeType(zv::Ref reflectionConstant, zv::Ref declaringClass, zend_string *name) + { + zv::Val type = callOn(reflectionConstant, PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (!type.isNull()) return pt_typehint_helper_decide_type_from_reflection(type.raw(), NULL, declaringClass.raw()); + + zv::Val declaringName = crGetName(declaringClass); + if (UNEXPECTED(declaringName.isUndef())) return zv::Val(); + zv::Args args{declaringName.raw(), name}; + bool hasMetadata; + zend_object *signatureMapProvider = service(PT_CR_PROP_SIGNATURE_MAP_PROVIDER, "signatureMapProvider"); + if (UNEXPECTED(signatureMapProvider == NULL)) return zv::Val(); + zv::Val has = pt_type_call(signatureMapProvider, PT_LC("hasclassconstantmetadata"), 2, args); + if (UNEXPECTED(has.isUndef())) return zv::Val(); + hasMetadata = zend_is_true(has.raw()); + if (!hasMetadata) return zv::Val::null(); + + zv::Val metadata = pt_type_call(signatureMapProvider, PT_LC("getclassconstantmetadata"), 2, args); + if (UNEXPECTED(metadata.isUndef())) return zv::Val(); + if (!metadata.ref().isArray()) return zv::Val::null(); + zval *nativeType = zend_hash_str_find(metadata.ref().asArrayTable(), PT_LC("nativeType")); + if (nativeType == NULL) return zv::Val::null(); + return zv::Val::copyOf(zv::Ref(nativeType)); + } + + /* @internal; the @var PHPDoc type of a class constant, without walking ancestors */ + zv::Val getConstantPhpDocType(zend_string *name) + { + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + zval nameArg; + ZVAL_STR(&nameArg, name); + zv::Val reflectionConstant = callOn(nativeReflection.ref(), PT_LC("getreflectionconstant"), 1, &nameArg); + if (UNEXPECTED(reflectionConstant.isUndef())) return zv::Val(); + if (reflectionConstant.ref().isFalse()) return zv::Val::null(); + + zv::Val resolvedPhpDoc = findConstantResolvedPhpDoc(reflectionConstant.ref()); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) return zv::Val::null(); + + zv::Val nativeType = zv::Val::null(); + zv::Val type = callOn(reflectionConstant.ref(), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (!type.isNull()) { + nativeType = pt_typehint_helper_decide_type_from_reflection(type.raw()); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + } + + zv::Val declaringClassName = constantDeclaringClassName(reflectionConstant.ref()); + if (UNEXPECTED(declaringClassName.isUndef())) return zv::Val(); + zv::Val ownName = getName(); + if (UNEXPECTED(ownName.isUndef())) return zv::Val(); + zend_string *declaringClassNameStr = zval_get_string(declaringClassName.raw()); + zv::Val declaringClass; + if (Z_TYPE_P(ownName.raw()) == IS_STRING && zend_string_equals(Z_STR_P(ownName.raw()), declaringClassNameStr)) { + zval self_; + ZVAL_OBJ(&self_, self); + declaringClass = zv::Val::copyOf(zv::Ref(&self_)); + } else { + declaringClass = getAncestorWithClassName(declaringClassNameStr); + } + zend_string_release(declaringClassNameStr); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + if (declaringClass.isNull()) return zv::Val::null(); + + return resolveConstantVarPhpDocType(resolvedPhpDoc.ref(), nativeType.ref(), declaringClass.ref()); + } + + /* private; ?ResolvedPhpDocBlock */ + zv::Val findConstantResolvedPhpDoc(zv::Ref reflectionConstant) + { + zv::Val declaringClass = callOn(reflectionConstant, PT_LC("getdeclaringclass"), 0, NULL); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + zv::Val declaringClassName = callOn(declaringClass.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(declaringClassName.isUndef())) return zv::Val(); + zv::Val constantName = callOn(reflectionConstant, PT_LC("getname"), 0, NULL); + if (UNEXPECTED(constantName.isUndef())) return zv::Val(); + + zv::Args stubArgs{declaringClassName.raw(), constantName.raw()}; + zv::Val resolvedPhpDoc = callService(PT_CR_PROP_STUB_PHP_DOC_PROVIDER, "stubPhpDocProvider", PT_LC("findclassconstantphpdoc"), 2, stubArgs); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (!resolvedPhpDoc.isNull()) return resolvedPhpDoc; + + zv::Val docComment = callOn(reflectionConstant, PT_LC("getdoccomment"), 0, NULL); + if (UNEXPECTED(docComment.isUndef())) return zv::Val(); + if (docComment.ref().isFalse()) return zv::Val::null(); + + /* $this->fileTypeMapper->getResolvedPhpDoc($reflectionConstant->getDeclaringClass()->getFileName() ?: null, + * $declaringClassName, null, null, $docComment) */ + zv::Val fileName = callOn(declaringClass.ref(), PT_LC("getfilename"), 0, NULL); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + zval args[5]; + if (zend_is_true(fileName.raw())) { + ZVAL_COPY_VALUE(&args[0], fileName.raw()); + } else { + ZVAL_NULL(&args[0]); + } + ZVAL_COPY_VALUE(&args[1], declaringClassName.raw()); + ZVAL_NULL(&args[2]); + ZVAL_NULL(&args[3]); + ZVAL_COPY_VALUE(&args[4], docComment.raw()); + return callService(PT_CR_PROP_FILE_TYPE_MAPPER, "fileTypeMapper", PT_LC("getresolvedphpdoc"), 5, args); + } + + /* private static; the single explicit-or-compatible @var tag's type, + * resolved against the declaring class's template types */ + static zv::Val resolveConstantVarPhpDocType(zv::Ref resolvedPhpDoc, zv::Ref nativeType, zv::Ref declaringClass) + { + zv::Val varTags = callOn(resolvedPhpDoc, PT_LC("getvartags"), 0, NULL); + if (UNEXPECTED(varTags.isUndef())) return zv::Val(); + if (!varTags.ref().isArray()) return zv::Val::null(); + zval *varTag = zend_hash_index_find(varTags.ref().asArrayTable(), 0); + if (varTag == NULL || Z_TYPE_P(varTag) == IS_NULL || zend_hash_num_elements(varTags.ref().asArrayTable()) != 1) return zv::Val::null(); + + zv::Val varType = callOn(zv::Ref(varTag), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(varType.isUndef())) return zv::Val(); + bool isExplicit; + if (UNEXPECTED(!callBool(zv::Ref(varTag), PT_LC("isexplicit"), 0, NULL, isExplicit))) return zv::Val(); + if (!isExplicit && !nativeType.isNull()) { + zval arg; + ZVAL_COPY_VALUE(&arg, varType.raw()); + zv::Val isSuperType = callOn(nativeType, PT_LC("issupertypeof"), 1, &arg); + if (UNEXPECTED(isSuperType.isUndef())) return zv::Val(); + bool yes; + if (UNEXPECTED(!callBool(isSuperType.ref(), PT_LC("yes"), 0, NULL, yes))) return zv::Val(); + if (!yes) return zv::Val::null(); + } + + zv::Val activeTemplateTypeMap = crGetActiveTemplateTypeMap(declaringClass); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = crGetCallSiteVarianceMap(declaringClass); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val invariant = templateTypeVarianceInvariant(); + if (UNEXPECTED(invariant.isUndef())) return zv::Val(); + + return pt_type_template_type_helper_resolve_template_types(varType.raw(), activeTemplateTypeMap.raw(), callSiteVarianceMap.raw(), invariant.raw(), false); + } + + bool hasTraitUse(zend_string *traitName, bool &out) + { + zv::Val traitNames = getTraitNames(); + if (UNEXPECTED(traitNames.isUndef())) return false; + out = false; + if (traitNames.ref().isArray()) { + for (auto entry : zv::ArrRef(traitNames.raw())) { + if (entry.value().isString() && zend_string_equals(entry.value().asString(), traitName)) { + out = true; + break; + } + } + } + return true; + } + + /* private; list */ + zv::Val getTraitNames() + { + zv::Val class_ = getNativeReflection(); + if (UNEXPECTED(class_.isUndef())) return zv::Val(); + zv::Val traits = collectTraits(class_.ref()); + if (UNEXPECTED(traits.isUndef())) return zv::Val(); + zv::Arr traitNames = zv::Arr::create(countOf(traits.ref())); + if (traits.ref().isArray()) { + for (auto entry : zv::ArrRef(traits.raw())) { + zv::Val name = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + traitNames.push(std::move(name)); + } + } + + zv::Val names = zv::Val(std::move(traitNames)); + for (;;) { + zv::Val parentClass = callOn(class_.ref(), PT_LC("getparentclass"), 0, NULL); + if (UNEXPECTED(parentClass.isUndef())) return zv::Val(); + if (parentClass.ref().isFalse()) break; + zv::Val parentTraitNames = callOn(parentClass.ref(), PT_LC("gettraitnames"), 0, NULL); + if (UNEXPECTED(parentTraitNames.isUndef())) return zv::Val(); + zv::Val merged = arrayMerge(names.ref(), parentTraitNames.ref()); + if (UNEXPECTED(merged.isUndef())) return zv::Val(); + names = arrayUniqueValues(merged.ref()); + class_ = std::move(parentClass); + } + + return names; + } + + /* array */ + zv::Val getTypeAliases() + { + if (slot(PT_CR_PROP_TYPE_ALIASES).isNull()) { + zv::Val computed = resolveTypeAliases(); + if (UNEXPECTED(computed.isUndef())) return zv::Val(); + writeSlot(PT_CR_PROP_TYPE_ALIASES, std::move(computed)); + } + + return copyOfSlot(PT_CR_PROP_TYPE_ALIASES); + } + + /* the twin's getTypeAliases() body; the memo write is the caller's (the + * twin's early returns write it too — they are zv::Val results here) */ + zv::Val resolveTypeAliases() + { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) return zv::Val(zv::Arr::empty()); + + zv::Val typeAliasImportTags = callOn(resolvedPhpDoc.ref(), PT_LC("gettypealiasimporttags"), 0, NULL); + if (UNEXPECTED(typeAliasImportTags.isUndef())) return zv::Val(); + zv::Val typeAliasTags = callOn(resolvedPhpDoc.ref(), PT_LC("gettypealiastags"), 0, NULL); + if (UNEXPECTED(typeAliasTags.isUndef())) return zv::Val(); + + /* array_map(static fn (TypeAliasTag $tag): TypeAlias => $tag->getTypeAlias(), $typeAliasTags) */ + zv::Arr localAliases = zv::Arr::create(countOf(typeAliasTags.ref())); + if (typeAliasTags.ref().isArray()) { + for (auto entry : zv::ArrRef(typeAliasTags.raw())) { + zv::Val alias = callOn(entry.value(), PT_LC("gettypealias"), 0, NULL); + if (UNEXPECTED(alias.isUndef())) return zv::Val(); + setAtKey(localAliases, entry, std::move(alias)); + } + } + + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + zval *resolving = resolvingTypeAliasImports(); + if (UNEXPECTED(resolving == NULL)) { + zend_string_release(nameStr); + return zv::Val(); + } + if (Z_TYPE_P(resolving) == IS_ARRAY && zend_symtable_find(Z_ARRVAL_P(resolving), nameStr) != NULL) { + zend_string_release(nameStr); + if (localAliases.arrRef().size() > 0) return zv::Val(std::move(localAliases)); + throwNew(PT_CLASS_CIRCULAR_TYPE_ALIAS_DEFINITION_EXCEPTION, 0, NULL); + return zv::Val(); + } + + if (Z_TYPE_P(resolving) != IS_ARRAY) { + zval fresh; + array_init(&fresh); + zval_ptr_dtor(resolving); + ZVAL_COPY_VALUE(resolving, &fresh); + } + SEPARATE_ARRAY(resolving); + zval true_; + ZVAL_TRUE(&true_); + zend_symtable_update(Z_ARRVAL_P(resolving), nameStr, &true_); + + zv::Val importedAliases = resolveImportedTypeAliases(typeAliasImportTags.ref()); + if (UNEXPECTED(importedAliases.isUndef())) { + zend_string_release(nameStr); + return zv::Val(); + } + + resolving = resolvingTypeAliasImports(); + if (resolving != NULL && Z_TYPE_P(resolving) == IS_ARRAY) { + SEPARATE_ARRAY(resolving); + zend_symtable_del(Z_ARRVAL_P(resolving), nameStr); + } + zend_string_release(nameStr); + + /* array_filter(array_merge($importedAliases, $localAliases), fn ($a) => $a !== null) */ + zv::Val merged = arrayMerge(importedAliases.ref(), localAliases.ref()); + if (UNEXPECTED(merged.isUndef())) return zv::Val(); + zv::Arr filtered = zv::Arr::create(countOf(merged.ref())); + if (merged.ref().isArray()) { + for (auto entry : zv::ArrRef(merged.raw())) { + if (entry.value().isNull()) continue; + setAtKey(filtered, entry, zv::Val::copyOf(entry.value())); + } + } + + return zv::Val(std::move(filtered)); + } + + /* array_map(function (TypeAliasImportTag $tag): ?TypeAlias { ... }, $typeAliasImportTags) */ + zv::Val resolveImportedTypeAliases(zv::Ref typeAliasImportTags) + { + zv::Arr importedAliases = zv::Arr::create(countOf(typeAliasImportTags)); + if (!typeAliasImportTags.isArray()) return zv::Val(std::move(importedAliases)); + + for (auto entry : zv::ArrRef(typeAliasImportTags.raw())) { + zv::Val importedAlias = callOn(entry.value(), PT_LC("getimportedalias"), 0, NULL); + if (UNEXPECTED(importedAlias.isUndef())) return zv::Val(); + zv::Val importedFrom = callOn(entry.value(), PT_LC("getimportedfrom"), 0, NULL); + if (UNEXPECTED(importedFrom.isUndef())) return zv::Val(); + zend_string *importedFromStr = zval_get_string(importedFrom.raw()); + bool hasClass; + if (UNEXPECTED(!providerHasClass(importedFromStr, hasClass))) { + zend_string_release(importedFromStr); + return zv::Val(); + } + if (!hasClass) { + zend_string_release(importedFromStr); + setAtKey(importedAliases, entry, zv::Val::null()); + continue; + } + zv::Val importedFromReflection = providerGetClass(importedFromStr); + zend_string_release(importedFromStr); + if (UNEXPECTED(importedFromReflection.isUndef())) return zv::Val(); + + zv::Val typeAliases = crGetTypeAliases(importedFromReflection.ref()); + if (UNEXPECTED(typeAliases.isUndef())) { + if (!caughtCircularTypeAliasDefinitionException()) return zv::Val(); + zv::Val invalid = pt_type_call_static(PT_CLASS_TYPE_ALIAS, PT_LC("invalid"), 0, NULL); + if (UNEXPECTED(invalid.isUndef())) return zv::Val(); + setAtKey(importedAliases, entry, std::move(invalid)); + continue; + } + + zend_string *importedAliasStr = zval_get_string(importedAlias.raw()); + zval *found = typeAliases.ref().isArray() ? zend_symtable_find(typeAliases.ref().asArrayTable(), importedAliasStr) : NULL; + zend_string_release(importedAliasStr); + setAtKey(importedAliases, entry, found == NULL ? zv::Val::null() : zv::Val::copyOf(zv::Ref(found))); + } + + return zv::Val(std::move(importedAliases)); + } + + zv::Val getDeprecatedDescription() + { + if (slot(PT_CR_PROP_IS_DEPRECATED).isNull()) { + if (UNEXPECTED(!resolveDeprecation())) return zv::Val(); + } + + return copyOfSlot(PT_CR_PROP_DEPRECATED_DESCRIPTION); + } + + bool isDeprecated(bool &out) + { + if (slot(PT_CR_PROP_IS_DEPRECATED).isNull()) { + if (UNEXPECTED(!resolveDeprecation())) return false; + } + + out = slot(PT_CR_PROP_IS_DEPRECATED).isTrue(); + return true; + } + + /* private */ + bool resolveDeprecation() + { + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + if (UNEXPECTED(reflection.isUndef())) { + (void) uninitializedProperty("reflection"); + return false; + } + zval reflectionArg; + ZVAL_COPY_VALUE(&reflectionArg, reflection.raw()); + zv::Val deprecation = callService(PT_CR_PROP_DEPRECATION_PROVIDER, "deprecationProvider", PT_LC("getclassdeprecation"), 1, &reflectionArg); + if (UNEXPECTED(deprecation.isUndef())) return false; + if (!deprecation.isNull()) { + zv::Val description = callOn(deprecation.ref(), PT_LC("getdescription"), 0, NULL); + if (UNEXPECTED(description.isUndef())) return false; + writeSlot(PT_CR_PROP_IS_DEPRECATED, zv::Val::boolean(true)); + writeSlot(PT_CR_PROP_DEPRECATED_DESCRIPTION, std::move(description)); + return true; + } + + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return false; + if (!resolvedPhpDoc.isNull()) { + bool deprecated; + if (UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isdeprecated"), 0, NULL, deprecated))) return false; + if (deprecated) { + zv::Val deprecatedTag = callOn(resolvedPhpDoc.ref(), PT_LC("getdeprecatedtag"), 0, NULL); + if (UNEXPECTED(deprecatedTag.isUndef())) return false; + zv::Val description = zv::Val::null(); + if (!deprecatedTag.isNull()) { + description = callOn(deprecatedTag.ref(), PT_LC("getmessage"), 0, NULL); + if (UNEXPECTED(description.isUndef())) return false; + } + writeSlot(PT_CR_PROP_IS_DEPRECATED, zv::Val::boolean(true)); + writeSlot(PT_CR_PROP_DEPRECATED_DESCRIPTION, std::move(description)); + return true; + } + } + + bool isTrait_; + if (UNEXPECTED(!isTrait(isTrait_))) return false; + if (isTrait_) { + zv::Val nativeReflection = getNativeReflection(); + if (UNEXPECTED(nativeReflection.isUndef())) return false; + static zend_string *deprecatedLiteral = nullptr; + zval arg; + ZVAL_STR(&arg, literal(deprecatedLiteral, PT_LC("Deprecated"))); + zv::Val attributes = callOn(nativeReflection.ref(), PT_LC("getattributes"), 1, &arg); + if (UNEXPECTED(attributes.isUndef())) return false; + if (countOf(attributes.ref()) > 0) { + writeSlot(PT_CR_PROP_IS_DEPRECATED, zv::Val::boolean(true)); + writeSlot(PT_CR_PROP_DEPRECATED_DESCRIPTION, zv::Val::null()); + return true; + } + } + + writeSlot(PT_CR_PROP_IS_DEPRECATED, zv::Val::boolean(false)); + writeSlot(PT_CR_PROP_DEPRECATED_DESCRIPTION, zv::Val::null()); + return true; + } + + bool isBuiltin(bool &out) const { return reflectionCallBool(PT_LC("isinternal"), out); } + + bool isInternal(bool &out) + { + if (slot(PT_CR_PROP_IS_INTERNAL).isNull()) { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return false; + bool internal = false; + if (!resolvedPhpDoc.isNull() && UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isinternal"), 0, NULL, internal))) return false; + writeSlot(PT_CR_PROP_IS_INTERNAL, zv::Val::boolean(internal)); + } + + out = slot(PT_CR_PROP_IS_INTERNAL).isTrue(); + return true; + } + + bool isImmutable(bool &out) + { + if (slot(PT_CR_PROP_IS_IMMUTABLE).isNull()) { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return false; + bool immutable = false; + if (!resolvedPhpDoc.isNull()) { + if (UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isimmutable"), 0, NULL, immutable))) return false; + if (!immutable && UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isreadonly"), 0, NULL, immutable))) return false; + } + writeSlot(PT_CR_PROP_IS_IMMUTABLE, zv::Val::boolean(immutable)); + + zv::Val parentClass = getParentClass(); + if (UNEXPECTED(parentClass.isUndef())) return false; + if (!parentClass.isNull() && !immutable) { + bool parentImmutable; + if (UNEXPECTED(!crIsImmutable(parentClass.ref(), parentImmutable))) return false; + writeSlot(PT_CR_PROP_IS_IMMUTABLE, zv::Val::boolean(parentImmutable)); + } + } + + out = slot(PT_CR_PROP_IS_IMMUTABLE).isTrue(); + return true; + } + + bool hasConsistentConstructor(bool &out) + { + if (slot(PT_CR_PROP_HAS_CONSISTENT_CONSTRUCTOR).isNull()) { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return false; + bool consistent = false; + if (!resolvedPhpDoc.isNull() && UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("hasconsistentconstructor"), 0, NULL, consistent))) return false; + writeSlot(PT_CR_PROP_HAS_CONSISTENT_CONSTRUCTOR, zv::Val::boolean(consistent)); + } + + out = slot(PT_CR_PROP_HAS_CONSISTENT_CONSTRUCTOR).isTrue(); + return true; + } + + bool acceptsNamedArguments(bool &out) + { + if (slot(PT_CR_PROP_ACCEPTS_NAMED_ARGUMENTS).isNull()) { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return false; + bool accepts = true; + if (!resolvedPhpDoc.isNull() && UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("acceptsnamedarguments"), 0, NULL, accepts))) return false; + writeSlot(PT_CR_PROP_ACCEPTS_NAMED_ARGUMENTS, zv::Val::boolean(accepts)); + } + + out = slot(PT_CR_PROP_ACCEPTS_NAMED_ARGUMENTS).isTrue(); + return true; + } + + bool isAttributeClass(bool &out) + { + zv::Val flags = findAttributeFlags(); + if (UNEXPECTED(flags.isUndef())) return false; + out = !flags.isNull(); + return true; + } + + /* private; ?int */ + zv::Val findAttributeFlags() + { + bool is; + if (UNEXPECTED(!isInterface(is))) return zv::Val(); + if (!is && UNEXPECTED(!isTrait(is))) return zv::Val(); + if (!is && UNEXPECTED(!isEnum(is))) return zv::Val(); + if (is) return zv::Val::null(); + + static zend_string *attributeLiteral = nullptr; + zend_string *attributeName = literal(attributeLiteral, PT_LC("Attribute")); + zval attributeArg; + ZVAL_STR(&attributeArg, attributeName); + zv::Val nativeAttributes = reflectionCall(PT_LC("getattributes"), 1, &attributeArg); + if (UNEXPECTED(nativeAttributes.isUndef())) return zv::Val(); + if (countOf(nativeAttributes.ref()) != 1) return zv::Val::null(); + + bool hasAttributeClass; + if (UNEXPECTED(!providerHasClass(attributeName, hasAttributeClass))) return zv::Val(); + if (!hasAttributeClass) return zv::Val::null(); + zv::Val attributeClass = providerGetClass(attributeName); + if (UNEXPECTED(attributeClass.isUndef())) return zv::Val(); + + zval *firstAttribute = zend_hash_index_find(nativeAttributes.ref().asArrayTable(), 0); + if (UNEXPECTED(firstAttribute == NULL)) return zv::Val::null(); + zv::Val argumentsExpressions = callOn(zv::Ref(firstAttribute), PT_LC("getargumentsexpressions"), 0, NULL); + if (UNEXPECTED(argumentsExpressions.isUndef())) return zv::Val(); + + zv::Arr arguments = zv::Arr::create(countOf(argumentsExpressions.ref())); + if (argumentsExpressions.ref().isArray()) { + for (auto entry : zv::ArrRef(argumentsExpressions.raw())) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL && ZSTR_LEN(key) == 0) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + zv::Val name = zv::Val::null(); + if (key != NULL) { + zval identifierArg; + ZVAL_STR(&identifierArg, key); + name = pt_type_new(PT_CLASS_IDENTIFIER, 1, &identifierArg); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + } + zval argArgs[5]; + ZVAL_COPY_VALUE(&argArgs[0], entry.value().raw()); + ZVAL_FALSE(&argArgs[1]); + ZVAL_FALSE(&argArgs[2]); + ZVAL_EMPTY_ARRAY(&argArgs[3]); + ZVAL_COPY_VALUE(&argArgs[4], name.raw()); + zv::Val arg = pt_type_new(PT_CLASS_ARG, 5, argArgs); + if (UNEXPECTED(arg.isUndef())) return zv::Val(); + arguments.push(std::move(arg)); + } + } + + bool hasConstructor_; + if (UNEXPECTED(!crHasConstructor(attributeClass.ref(), hasConstructor_))) return zv::Val(); + if (!hasConstructor_) return zv::Val::null(); + zv::Val attributeConstructor = crGetConstructor(attributeClass.ref()); + if (UNEXPECTED(attributeConstructor.isUndef())) return zv::Val(); + zv::Val attributeConstructorVariant = callOn(attributeConstructor.ref(), PT_LC("getonlyvariant"), 0, NULL); + if (UNEXPECTED(attributeConstructorVariant.isUndef())) return zv::Val(); + + zv::Val flagType; + if (arguments.arrRef().size() == 0) { + zv::Val parameters = callOn(attributeConstructorVariant.ref(), PT_LC("getparameters"), 0, NULL); + if (UNEXPECTED(parameters.isUndef())) return zv::Val(); + zval *firstParameter = parameters.ref().isArray() ? zend_hash_index_find(parameters.ref().asArrayTable(), 0) : NULL; + if (firstParameter == NULL) return zv::Val::null(); + flagType = callOn(zv::Ref(firstParameter), PT_LC("getdefaultvalue"), 0, NULL); + if (UNEXPECTED(flagType.isUndef())) return zv::Val(); + } else { + zval classNameArg; + ZVAL_STR(&classNameArg, attributeName); + zv::Val class_ = pt_type_new(PT_CLASS_FULLY_QUALIFIED, 1, &classNameArg); + if (UNEXPECTED(class_.isUndef())) return zv::Val(); + zv::Val constructorName = callOn(attributeConstructor.ref(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(constructorName.isUndef())) return zv::Val(); + zv::Args staticCallArgs{class_.raw(), constructorName.raw(), arguments.raw()}; + zv::Val staticCallNode = pt_type_new(PT_CLASS_STATIC_CALL, 3, staticCallArgs); + if (UNEXPECTED(staticCallNode.isUndef())) return zv::Val(); + zv::Args reorderArgs{attributeConstructorVariant.raw(), staticCallNode.raw()}; + zv::Val staticCall = pt_type_call_static(PT_CLASS_ARGUMENTS_NORMALIZER, PT_LC("reorderstaticcallarguments"), 2, reorderArgs); + if (UNEXPECTED(staticCall.isUndef())) return zv::Val(); + if (staticCall.isNull()) return zv::Val::null(); + + zv::Val callArgs = callOn(staticCall.ref(), PT_LC("getargs"), 0, NULL); + if (UNEXPECTED(callArgs.isUndef())) return zv::Val(); + zval *firstArg = callArgs.ref().isArray() ? zend_hash_index_find(callArgs.ref().asArrayTable(), 0) : NULL; + if (UNEXPECTED(firstArg == NULL || Z_TYPE_P(firstArg) != IS_OBJECT)) return zv::Val::null(); + zv::Ref flagExpr = zv::ObjRef(Z_OBJ_P(firstArg)).prop(PT_LC("value")); + if (UNEXPECTED(flagExpr.raw() == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: %s has no property $value", ZSTR_VAL(Z_OBJCE_P(firstArg)->name)); + return zv::Val(); + } + zv::Val context = initializerExprContextFromClassReflection(); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Args typeArgs{flagExpr.raw(), context.raw()}; + flagType = callService(PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, "initializerExprTypeResolver", PT_LC("gettype"), 2, typeArgs); + if (UNEXPECTED(flagType.isUndef())) return zv::Val(); + } + + if (!instanceOfShadowed(flagType.ref(), pt_ce_constant_integer_type, PT_LC(PT_CR_CONSTANT_INTEGER_TYPE_NAME))) return zv::Val::null(); + + return callOn(flagType.ref(), PT_LC("getvalue"), 0, NULL); + } + + /* list */ + zv::Val getAttributes() + { + zv::Val attributes = reflectionCall(PT_LC("getattributes"), 0, NULL); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + zv::Val context = initializerExprContextFromClass(); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Args args{attributes.raw(), context.raw()}; + return callService(PT_CR_PROP_ATTRIBUTE_REFLECTION_FACTORY, "attributeReflectionFactory", PT_LC("fromnativereflection"), 2, args); + } + + zv::Val getAttributeClassFlags() + { + zv::Val flags = findAttributeFlags(); + if (UNEXPECTED(flags.isUndef())) return zv::Val(); + if (flags.isNull()) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + + return flags; + } + + zv::Val getObjectType() + { + bool generic; + if (UNEXPECTED(!isGeneric(generic))) return zv::Val(); + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *nameStr = zval_get_string(name.raw()); + + if (!generic) { + zval out; + bool ok = pt_object_type_new(&out, nameStr); + zend_string_release(nameStr); + return ok ? zv::Val::adopt(out) : zv::Val(); + } + + zv::Val activeTemplateTypeMap = getActiveTemplateTypeMap(); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) { + zend_string_release(nameStr); + return zv::Val(); + } + zv::Val types = typeMapToList(activeTemplateTypeMap.ref()); + if (UNEXPECTED(types.isUndef())) { + zend_string_release(nameStr); + return zv::Val(); + } + zv::Val callSiteVarianceMap = getCallSiteVarianceMap(); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) { + zend_string_release(nameStr); + return zv::Val(); + } + zv::Val variances = varianceMapToList(callSiteVarianceMap.ref()); + if (UNEXPECTED(variances.isUndef())) { + zend_string_release(nameStr); + return zv::Val(); + } + + zval out; + bool ok = pt_generic_object_type_new(&out, nameStr, types.raw(), NULL, NULL, variances.raw()); + zend_string_release(nameStr); + return ok ? zv::Val::adopt(out) : zv::Val(); + } + + zv::Val getTemplateTypeMap() + { + zv::Ref memo = slot(PT_CR_PROP_TEMPLATE_TYPE_MAP); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) { + zval empty; + if (UNEXPECTED(!pt_template_type_map_empty(&empty))) return zv::Val(); + writeSlot(PT_CR_PROP_TEMPLATE_TYPE_MAP, zv::Val::copyOf(zv::Ref(&empty))); + return zv::Val::adopt(empty); + } + + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Val templateTypeScope = templateTypeScopeWithClass(name.raw()); + if (UNEXPECTED(templateTypeScope.isUndef())) return zv::Val(); + + zv::Val templateTags = getTemplateTags(); + if (UNEXPECTED(templateTags.isUndef())) return zv::Val(); + zv::Arr types = zv::Arr::create(countOf(templateTags.ref())); + if (templateTags.ref().isArray()) { + for (auto entry : zv::ArrRef(templateTags.raw())) { + zv::Val templateType = templateTypeFactoryFromTemplateTag(templateTypeScope.raw(), entry.value().raw()); + if (UNEXPECTED(templateType.isUndef())) return zv::Val(); + setAtKey(types, entry, std::move(templateType)); + } + } + + zval map; + if (UNEXPECTED(!pt_template_type_map_new(&map, types.raw(), NULL))) return zv::Val(); + writeSlot(PT_CR_PROP_TEMPLATE_TYPE_MAP, zv::Val::copyOf(zv::Ref(&map))); + + return zv::Val::adopt(map); + } + + zv::Val getActiveTemplateTypeMap() + { + zv::Ref memo = slot(PT_CR_PROP_ACTIVE_TEMPLATE_TYPE_MAP); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Ref resolved = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolved.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + if (resolved.isNull()) { + zv::Val templateTypeMap = getTemplateTypeMap(); + if (UNEXPECTED(templateTypeMap.isUndef())) return zv::Val(); + writeSlot(PT_CR_PROP_ACTIVE_TEMPLATE_TYPE_MAP, zv::Val::copyOf(templateTypeMap.ref())); + return templateTypeMap; + } + + zv::Val templateTypeMap = getTemplateTypeMap(); + if (UNEXPECTED(templateTypeMap.isUndef())) return zv::Val(); + + /* $resolved->map(fn ($name, $type) => $type instanceof ErrorType && + * ($t = $templateTypeMap->getType($name)) !== null + * ? TemplateTypeHelper::resolveToDefaults($t) : $type) */ + zv::Val types = callOn(resolved, PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(types.isUndef())) return zv::Val(); + zv::Arr mapped = zv::Arr::create(countOf(types.ref())); + if (types.ref().isArray()) { + for (auto entry : zv::ArrRef(types.raw())) { + zv::Val result; + if (!instanceOfShadowed(entry.value(), pt_ce_error_type, PT_LC(PT_CR_ERROR_TYPE_NAME))) { + result = zv::Val::copyOf(entry.value()); + } else { + zv::Val name = entryKeyAsValue(entry); + zv::Val templateType = callOn(templateTypeMap.ref(), PT_LC("gettype"), 1, name.raw()); + if (UNEXPECTED(templateType.isUndef())) return zv::Val(); + if (templateType.isNull()) { + result = zv::Val::copyOf(entry.value()); + } else { + result = pt_type_template_type_helper_resolve_to_defaults(templateType.raw()); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + } + } + setAtKey(mapped, entry, std::move(result)); + } + } + + zval out; + if (UNEXPECTED(!pt_template_type_map_new(&out, mapped.raw(), NULL))) return zv::Val(); + writeSlot(PT_CR_PROP_ACTIVE_TEMPLATE_TYPE_MAP, zv::Val::copyOf(zv::Ref(&out))); + + return zv::Val::adopt(out); + } + + zv::Val getPossiblyIncompleteActiveTemplateTypeMap() + { + zv::Ref resolved = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolved.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + if (!resolved.isNull()) return zv::Val::copyOf(resolved); + + return getTemplateTypeMap(); + } + + /* private */ + zv::Val getDefaultCallSiteVarianceMap() + { + zv::Ref memo = slot(PT_CR_PROP_DEFAULT_CALL_SITE_VARIANCE_MAP); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) { + zval empty; + if (UNEXPECTED(!pt_template_type_variance_map_empty(&empty))) return zv::Val(); + writeSlot(PT_CR_PROP_DEFAULT_CALL_SITE_VARIANCE_MAP, zv::Val::copyOf(zv::Ref(&empty))); + return zv::Val::adopt(empty); + } + + zv::Val templateTags = getTemplateTags(); + if (UNEXPECTED(templateTags.isUndef())) return zv::Val(); + zv::Arr map = zv::Arr::create(countOf(templateTags.ref())); + if (templateTags.ref().isArray()) { + for (auto entry : zv::ArrRef(templateTags.raw())) { + zv::Val tagName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(tagName.isUndef())) return zv::Val(); + zv::Val invariant = templateTypeVarianceInvariant(); + if (UNEXPECTED(invariant.isUndef())) return zv::Val(); + zend_string *key = zval_get_string(tagName.raw()); + map.set(key, std::move(invariant)); + zend_string_release(key); + } + } + + zval out; + if (UNEXPECTED(!pt_template_type_variance_map_new(&out, map.raw()))) return zv::Val(); + writeSlot(PT_CR_PROP_DEFAULT_CALL_SITE_VARIANCE_MAP, zv::Val::copyOf(zv::Ref(&out))); + + return zv::Val::adopt(out); + } + + zv::Val getCallSiteVarianceMap() + { + zv::Ref memo = slot(PT_CR_PROP_CALL_SITE_VARIANCE_MAP); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Ref resolved = slot(PT_CR_PROP_RESOLVED_CALL_SITE_VARIANCE_MAP); + if (UNEXPECTED(resolved.isUndef())) return uninitializedProperty("resolvedCallSiteVarianceMap"); + zv::Val map; + if (!resolved.isNull()) { + map = zv::Val::copyOf(resolved); + } else { + map = getDefaultCallSiteVarianceMap(); + if (UNEXPECTED(map.isUndef())) return zv::Val(); + } + writeSlot(PT_CR_PROP_CALL_SITE_VARIANCE_MAP, zv::Val::copyOf(map.ref())); + + return map; + } + + zv::Val typeMapFromList(zv::Ref types) + { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) { + zval empty; + return pt_template_type_map_empty(&empty) ? zv::Val::adopt(empty) : zv::Val(); + } + + zv::Val templateTags = callOn(resolvedPhpDoc.ref(), PT_LC("gettemplatetags"), 0, NULL); + if (UNEXPECTED(templateTags.isUndef())) return zv::Val(); + zv::Val className = getName(); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + + zv::Arr map = zv::Arr::create(countOf(templateTags.ref())); + zend_ulong i = 0; + if (templateTags.ref().isArray()) { + for (auto entry : zv::ArrRef(templateTags.raw())) { + zv::Val type = tagTypeAt(types, i, entry.value()); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + + /* TypeTraverser::map($type, static function (Type $type, callable + * $traverse) use ($map, $className): Type { ... }) */ + zval mapState; + ZVAL_COPY_VALUE(&mapState, map.raw()); + zv::Val callback = pt_type_native_callback(typeMapFromListVisitor, &mapState, className.raw()); + if (UNEXPECTED(callback.isUndef())) return zv::Val(); + zv::Val mapped = pt_type_traverser_map_of(type.raw(), callback.raw()); + if (UNEXPECTED(mapped.isUndef())) return zv::Val(); + + zv::Val tagName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(tagName.isUndef())) return zv::Val(); + zend_string *key = zval_get_string(tagName.raw()); + map.set(key, std::move(mapped)); + zend_string_release(key); + i++; + } + } + + zval out; + return pt_template_type_map_new(&out, map.raw(), NULL) ? zv::Val::adopt(out) : zv::Val(); + } + + /* $types[$i] ?? $tag->getDefault() ?? $tag->getBound() */ + static zv::Val tagTypeAt(zv::Ref types, zend_ulong i, zv::Ref tag) + { + if (types.isArray()) { + zval *found = zend_hash_index_find(types.asArrayTable(), i); + if (found != NULL && Z_TYPE_P(found) != IS_NULL) return zv::Val::copyOf(zv::Ref(found)); + } + zv::Val default_ = callOn(tag, PT_LC("getdefault"), 0, NULL); + if (UNEXPECTED(default_.isUndef())) return zv::Val(); + if (!default_.isNull()) return default_; + return callOn(tag, PT_LC("getbound"), 0, NULL); + } + + /* the `use ($map, $className)` closure of typeMapFromList() */ + static void typeMapFromListVisitor(zval *map, zval *className, uint32_t argc, zval *argv, zval *return_value) + { + if (UNEXPECTED(argc < 2 || Z_TYPE(argv[0]) != IS_OBJECT)) { + zend_argument_count_error("Too few arguments to function ClassReflection::{closure}(), %u passed and exactly 2 expected", argc); + return; + } + zval *type = &argv[0]; + + bool isTemplate; + if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_TEMPLATE_TYPE, isTemplate))) return; + if (!isTemplate) { + zv::Val traversed = pt_type_call_callable(&argv[1], 1, type); + if (UNEXPECTED(traversed.isUndef())) return; + traversed.intoReturnValue(return_value); + return; + } + + zv::Val scope = pt_type_call(Z_OBJ_P(type), PT_LC("getscope"), 0, NULL); + if (UNEXPECTED(scope.isUndef())) return; + zv::Val scopeClassName = callOn(scope.ref(), PT_LC("getclassname"), 0, NULL); + if (UNEXPECTED(scopeClassName.isUndef())) return; + bool sameClass = Z_TYPE_P(scopeClassName.raw()) == IS_STRING + && Z_TYPE_P(className) == IS_STRING + && zend_string_equals(Z_STR_P(scopeClassName.raw()), Z_STR_P(className)); + if (!sameClass) { + ZVAL_COPY(return_value, type); + return; + } + + zv::Val name = pt_type_call(Z_OBJ_P(type), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return; + zend_string *nameStr = zval_get_string(name.raw()); + zval *resolved = Z_TYPE_P(map) == IS_ARRAY ? zend_symtable_find(Z_ARRVAL_P(map), nameStr) : NULL; + zend_string_release(nameStr); + if (resolved != NULL && Z_TYPE_P(resolved) != IS_NULL) { + bool resolvedIsTemplate; + if (UNEXPECTED(!pt_type_instanceof(resolved, PT_CLASS_TEMPLATE_TYPE, resolvedIsTemplate))) return; + if (!resolvedIsTemplate) { + ZVAL_COPY(return_value, resolved); + return; + } + } + + ZVAL_COPY(return_value, type); + } + + zv::Val varianceMapFromList(zv::Ref variances) + { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) { + zval empty; + ZVAL_EMPTY_ARRAY(&empty); + zval out; + return pt_template_type_variance_map_new(&out, &empty) ? zv::Val::adopt(out) : zv::Val(); + } + + zv::Val templateTags = callOn(resolvedPhpDoc.ref(), PT_LC("gettemplatetags"), 0, NULL); + if (UNEXPECTED(templateTags.isUndef())) return zv::Val(); + zv::Arr map = zv::Arr::create(countOf(templateTags.ref())); + zend_ulong i = 0; + if (templateTags.ref().isArray()) { + for (auto entry : zv::ArrRef(templateTags.raw())) { + zv::Val variance; + zval *found = variances.isArray() ? zend_hash_index_find(variances.asArrayTable(), i) : NULL; + if (found != NULL && Z_TYPE_P(found) != IS_NULL) { + variance = zv::Val::copyOf(zv::Ref(found)); + } else { + variance = templateTypeVarianceInvariant(); + if (UNEXPECTED(variance.isUndef())) return zv::Val(); + } + zv::Val tagName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(tagName.isUndef())) return zv::Val(); + zend_string *key = zval_get_string(tagName.raw()); + map.set(key, std::move(variance)); + zend_string_release(key); + i++; + } + } + + zval out; + return pt_template_type_variance_map_new(&out, map.raw()) ? zv::Val::adopt(out) : zv::Val(); + } + + /* list */ + zv::Val typeMapToList(zv::Ref typeMap) + { + zv::Val templateTags = resolvedPhpDocTemplateTags(); + if (UNEXPECTED(templateTags.isUndef())) return zv::Val(); + if (templateTags.isNull()) return zv::Val(zv::Arr::empty()); + + zv::Arr list = zv::Arr::create(countOf(templateTags.ref())); + if (templateTags.ref().isArray()) { + for (auto entry : zv::ArrRef(templateTags.raw())) { + zv::Val tagName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(tagName.isUndef())) return zv::Val(); + zv::Val type = callOn(typeMap, PT_LC("gettype"), 1, tagName.raw()); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (type.isNull()) { + type = callOn(entry.value(), PT_LC("getdefault"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + } + if (type.isNull()) { + type = callOn(entry.value(), PT_LC("getbound"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + } + list.push(std::move(type)); + } + } + + return zv::Val(std::move(list)); + } + + /* list */ + zv::Val varianceMapToList(zv::Ref varianceMap) + { + zv::Val templateTags = resolvedPhpDocTemplateTags(); + if (UNEXPECTED(templateTags.isUndef())) return zv::Val(); + if (templateTags.isNull()) return zv::Val(zv::Arr::empty()); + + zv::Arr list = zv::Arr::create(countOf(templateTags.ref())); + if (templateTags.ref().isArray()) { + for (auto entry : zv::ArrRef(templateTags.raw())) { + zv::Val tagName = callOn(entry.value(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(tagName.isUndef())) return zv::Val(); + zv::Val variance = callOn(varianceMap, PT_LC("getvariance"), 1, tagName.raw()); + if (UNEXPECTED(variance.isUndef())) return zv::Val(); + if (variance.isNull()) { + variance = templateTypeVarianceInvariant(); + if (UNEXPECTED(variance.isUndef())) return zv::Val(); + } + list.push(std::move(variance)); + } + } + + return zv::Val(std::move(list)); + } + + /* $this->getResolvedPhpDoc()?->getTemplateTags(); null = no PHPDoc (the + * twin's `return []` of the *ToList() methods) */ + zv::Val resolvedPhpDocTemplateTags() + { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) return zv::Val::null(); + return callOn(resolvedPhpDoc.ref(), PT_LC("gettemplatetags"), 0, NULL); + } + + zv::Val withTypes(zv::Ref types) + { + zv::Val typeMap = typeMapFromList(types); + if (UNEXPECTED(typeMap.isUndef())) return zv::Val(); + zv::Ref resolvedCallSiteVarianceMap = slot(PT_CR_PROP_RESOLVED_CALL_SITE_VARIANCE_MAP); + if (UNEXPECTED(resolvedCallSiteVarianceMap.isUndef())) return uninitializedProperty("resolvedCallSiteVarianceMap"); + zv::Ref finalByKeywordOverride = slot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE); + if (UNEXPECTED(finalByKeywordOverride.isUndef())) return uninitializedProperty("finalByKeywordOverride"); + + return factoryCreate(typeMap.ref(), resolvedCallSiteVarianceMap, finalByKeywordOverride); + } + + zv::Val withVariances(zv::Ref variances) + { + zv::Val varianceMap = varianceMapFromList(variances); + if (UNEXPECTED(varianceMap.isUndef())) return zv::Val(); + zv::Ref resolvedTemplateTypeMap = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolvedTemplateTypeMap.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + zv::Ref finalByKeywordOverride = slot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE); + if (UNEXPECTED(finalByKeywordOverride.isUndef())) return uninitializedProperty("finalByKeywordOverride"); + + return factoryCreate(resolvedTemplateTypeMap, varianceMap.ref(), finalByKeywordOverride); + } + + zv::Val asFinal() { return withFinality(true); } + + zv::Val withoutFinalByKeywordOverride() + { + zv::Ref finalByKeywordOverride = slot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE); + if (UNEXPECTED(finalByKeywordOverride.isUndef())) return uninitializedProperty("finalByKeywordOverride"); + if (finalByKeywordOverride.isNull()) return thisValue(); + + zv::Ref resolvedTemplateTypeMap = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolvedTemplateTypeMap.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + zv::Ref resolvedCallSiteVarianceMap = slot(PT_CR_PROP_RESOLVED_CALL_SITE_VARIANCE_MAP); + if (UNEXPECTED(resolvedCallSiteVarianceMap.isUndef())) return uninitializedProperty("resolvedCallSiteVarianceMap"); + zval null_; + ZVAL_NULL(&null_); + + return factoryCreate(resolvedTemplateTypeMap, resolvedCallSiteVarianceMap, zv::Ref(&null_)); + } + + zv::Val removeFinalKeywordOverride() { return withFinality(false); } + + /* asFinal() / removeFinalKeywordOverride(): the same guards, the + * override the only difference */ + zv::Val withFinality(bool override_) + { + bool finalByKeyword = false; + if (UNEXPECTED(!reflectionCallBool(PT_LC("isfinal"), finalByKeyword))) return zv::Val(); + if (finalByKeyword) return thisValue(); + + zv::Ref finalByKeywordOverride = slot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE); + if (UNEXPECTED(finalByKeywordOverride.isUndef())) return uninitializedProperty("finalByKeywordOverride"); + if (finalByKeywordOverride.isBool() && zend_is_true(finalByKeywordOverride.raw()) == override_) return thisValue(); + + bool isClass_; + if (UNEXPECTED(!isClass(isClass_))) return zv::Val(); + if (!isClass_) return thisValue(); + bool isAbstract_; + if (UNEXPECTED(!isAbstract(isAbstract_))) return zv::Val(); + if (isAbstract_) return thisValue(); + + zv::Ref resolvedTemplateTypeMap = slot(PT_CR_PROP_RESOLVED_TEMPLATE_TYPE_MAP); + if (UNEXPECTED(resolvedTemplateTypeMap.isUndef())) return uninitializedProperty("resolvedTemplateTypeMap"); + zv::Ref resolvedCallSiteVarianceMap = slot(PT_CR_PROP_RESOLVED_CALL_SITE_VARIANCE_MAP); + if (UNEXPECTED(resolvedCallSiteVarianceMap.isUndef())) return uninitializedProperty("resolvedCallSiteVarianceMap"); + zval flag = {}; + ZVAL_BOOL(&flag, override_); + + return factoryCreate(resolvedTemplateTypeMap, resolvedCallSiteVarianceMap, zv::Ref(&flag)); + } + + /* $this->classReflectionFactory->create($this->displayName, $this->reflection, + * $this->anonymousFilename, $resolvedTemplateTypeMap, $this->stubPhpDocBlockCallback, + * null, $resolvedCallSiteVarianceMap, $finalByKeywordOverride) */ + zv::Val factoryCreate(zv::Ref resolvedTemplateTypeMap, zv::Ref resolvedCallSiteVarianceMap, zv::Ref finalByKeywordOverride) + { + zv::Ref displayName_ = slot(PT_CR_PROP_DISPLAY_NAME); + if (UNEXPECTED(displayName_.isUndef())) return uninitializedProperty("displayName"); + zv::Ref reflection = slot(PT_CR_PROP_REFLECTION); + if (UNEXPECTED(reflection.isUndef())) return uninitializedProperty("reflection"); + zv::Ref anonymousFilename = slot(PT_CR_PROP_ANONYMOUS_FILENAME); + if (UNEXPECTED(anonymousFilename.isUndef())) return uninitializedProperty("anonymousFilename"); + zv::Ref stubPhpDocBlockCallback = slot(PT_CR_PROP_STUB_PHP_DOC_BLOCK_CALLBACK); + if (UNEXPECTED(stubPhpDocBlockCallback.isUndef())) return uninitializedProperty("stubPhpDocBlockCallback"); + + zval args[8]; + ZVAL_COPY_VALUE(&args[0], displayName_.raw()); + ZVAL_COPY_VALUE(&args[1], reflection.raw()); + ZVAL_COPY_VALUE(&args[2], anonymousFilename.raw()); + ZVAL_COPY_VALUE(&args[3], resolvedTemplateTypeMap.raw()); + ZVAL_COPY_VALUE(&args[4], stubPhpDocBlockCallback.raw()); + ZVAL_NULL(&args[5]); + ZVAL_COPY_VALUE(&args[6], resolvedCallSiteVarianceMap.raw()); + ZVAL_COPY_VALUE(&args[7], finalByKeywordOverride.raw()); + + return callService(PT_CR_PROP_CLASS_REFLECTION_FACTORY, "classReflectionFactory", PT_LC("create"), 8, args); + } + + /* ?ResolvedPhpDocBlock */ + zv::Val getResolvedPhpDoc() + { + zv::Ref stubPhpDocBlockCallback = slot(PT_CR_PROP_STUB_PHP_DOC_BLOCK_CALLBACK); + if (UNEXPECTED(stubPhpDocBlockCallback.isUndef())) return uninitializedProperty("stubPhpDocBlockCallback"); + if (!stubPhpDocBlockCallback.isNull()) { + if (slot(PT_CR_PROP_STUB_PHP_DOC_BLOCK).isFalse()) { + zv::Val block = callCallable(stubPhpDocBlockCallback, 0, NULL); + if (UNEXPECTED(block.isUndef())) return zv::Val(); + writeSlot(PT_CR_PROP_STUB_PHP_DOC_BLOCK, std::move(block)); + } + zv::Ref stubPhpDocBlock = slot(PT_CR_PROP_STUB_PHP_DOC_BLOCK); + if (!stubPhpDocBlock.isNull()) return zv::Val::copyOf(stubPhpDocBlock); + } + + zv::Val fileName = getFileName(); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + if (UNEXPECTED(!resolveReflectionDocComment())) return zv::Val(); + zv::Ref reflectionDocComment = slot(PT_CR_PROP_REFLECTION_DOC_COMMENT); + if (reflectionDocComment.isNull()) return zv::Val::null(); + + zv::Ref resolvedPhpDocBlock = slot(PT_CR_PROP_RESOLVED_PHP_DOC_BLOCK); + if (!resolvedPhpDocBlock.isFalse()) return zv::Val::copyOf(resolvedPhpDocBlock); + + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Args args{fileName.raw(), name.raw(), zv::null, zv::null, slot(PT_CR_PROP_REFLECTION_DOC_COMMENT).raw()}; + zv::Val resolved = callService(PT_CR_PROP_FILE_TYPE_MAPPER, "fileTypeMapper", PT_LC("getresolvedphpdoc"), 5, args); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + writeSlot(PT_CR_PROP_RESOLVED_PHP_DOC_BLOCK, zv::Val::copyOf(resolved.ref())); + + return resolved; + } + + /* ?ResolvedPhpDocBlock */ + zv::Val getTraitContextResolvedPhpDoc(zv::Ref implementingClass) + { + bool isTrait_; + if (UNEXPECTED(!isTrait(isTrait_))) return zv::Val(); + if (!isTrait_) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + bool implementingIsTrait; + if (UNEXPECTED(!crIsTrait(implementingClass, implementingIsTrait))) return zv::Val(); + if (implementingIsTrait) { + throwNew(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + return zv::Val(); + } + + zv::Val fileName = getFileName(); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + if (UNEXPECTED(!resolveReflectionDocComment())) return zv::Val(); + if (slot(PT_CR_PROP_REFLECTION_DOC_COMMENT).isNull()) return zv::Val::null(); + + zv::Ref traitContextResolvedPhpDocBlock = slot(PT_CR_PROP_TRAIT_CONTEXT_RESOLVED_PHP_DOC_BLOCK); + if (!traitContextResolvedPhpDocBlock.isFalse()) return zv::Val::copyOf(traitContextResolvedPhpDocBlock); + + zv::Val implementingName = crGetName(implementingClass); + if (UNEXPECTED(implementingName.isUndef())) return zv::Val(); + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Args args{fileName.raw(), implementingName.raw(), name.raw(), zv::null, slot(PT_CR_PROP_REFLECTION_DOC_COMMENT).raw()}; + zv::Val resolved = callService(PT_CR_PROP_FILE_TYPE_MAPPER, "fileTypeMapper", PT_LC("getresolvedphpdoc"), 5, args); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + writeSlot(PT_CR_PROP_TRAIT_CONTEXT_RESOLVED_PHP_DOC_BLOCK, zv::Val::copyOf(resolved.ref())); + + return resolved; + } + + /* if (is_bool($this->reflectionDocComment)) { $c = $this->reflection->getDocComment(); + * $this->reflectionDocComment = $c !== false ? $c : null; } */ + bool resolveReflectionDocComment() + { + if (!slot(PT_CR_PROP_REFLECTION_DOC_COMMENT).isBool()) return true; + zv::Val docComment = reflectionCall(PT_LC("getdoccomment"), 0, NULL); + if (UNEXPECTED(docComment.isUndef())) return false; + if (docComment.ref().isFalse()) { + writeSlot(PT_CR_PROP_REFLECTION_DOC_COMMENT, zv::Val::null()); + } else { + writeSlot(PT_CR_PROP_REFLECTION_DOC_COMMENT, std::move(docComment)); + } + return true; + } + + /* the tag getters: $this->getResolvedPhpDoc()?->getTags() ?? [] */ + zv::Val resolvedPhpDocTags(const char *lcname, size_t len) + { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + if (resolvedPhpDoc.isNull()) return zv::Val(zv::Arr::empty()); + return callOn(resolvedPhpDoc.ref(), lcname, len, 0, NULL); + } + + zv::Val getExtendsTags() { return resolvedPhpDocTags(PT_LC("getextendstags")); } + zv::Val getImplementsTags() { return resolvedPhpDocTags(PT_LC("getimplementstags")); } + zv::Val getTemplateTags() { return resolvedPhpDocTags(PT_LC("gettemplatetags")); } + zv::Val getMixinTags() { return resolvedPhpDocTags(PT_LC("getmixintags")); } + zv::Val getRequireExtendsTags() { return resolvedPhpDocTags(PT_LC("getrequireextendstags")); } + zv::Val getRequireImplementsTags() { return resolvedPhpDocTags(PT_LC("getrequireimplementstags")); } + zv::Val getSealedTags() { return resolvedPhpDocTags(PT_LC("getsealedtags")); } + zv::Val getPropertyTags() { return resolvedPhpDocTags(PT_LC("getpropertytags")); } + zv::Val getMethodTags() { return resolvedPhpDocTags(PT_LC("getmethodtags")); } + + /* array */ + zv::Val getAncestors() + { + zv::Ref memo = slot(PT_CR_PROP_ANCESTORS); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Val name = getName(); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zv::Arr ancestors = zv::Arr::create(8); + zend_string *nameStr = zval_get_string(name.raw()); + zval self_; + ZVAL_OBJ(&self_, self); + ancestors.set(nameStr, zv::Val::copyOf(zv::Ref(&self_))); + zend_string_release(nameStr); + + if (UNEXPECTED(!collectAncestors(ancestors))) return zv::Val(); + + writeSlot(PT_CR_PROP_ANCESTORS, zv::Val::copyOf(ancestors.ref())); + + return zv::Val(std::move(ancestors)); + } + + /* private: descends into the interfaces, traits and parent class, the + * collected ancestors doubling as the set of already visited classes - + * traits can use each other in a cycle, a fatal error in PHP that must + * not make this walk run forever; false = pending exception */ + [[nodiscard]] bool collectAncestors(zv::Arr &ancestors) + { + zv::Val interfaces = getInterfaces(); + if (UNEXPECTED(interfaces.isUndef())) return false; + if (UNEXPECTED(!addAllToAncestors(ancestors, interfaces.ref()))) return false; + + zv::Val traits = getTraits(false); + if (UNEXPECTED(traits.isUndef())) return false; + if (UNEXPECTED(!addAllToAncestors(ancestors, traits.ref()))) return false; + + zv::Val parent = getParentClass(); + if (UNEXPECTED(parent.isUndef())) return false; + if (parent.isNull()) return true; + + return addToAncestors(ancestors, parent.ref()); + } + + /* $classReflection->collectAncestors($ancestors): the native body for + * exactly this class, the same walk through the public methods of any + * other ClassReflection (the PHP twins of the differential harness) */ + static bool collectAncestorsOf(zv::Ref classReflection, zv::Arr &ancestors) + { + zv::Ref value = classReflection.deref(); + if (UNEXPECTED(!value.isObject())) { + zend_throw_error(NULL, "Call to a member function collectAncestors() on %s", zend_zval_value_name(value.raw())); + return false; + } + if (EXPECTED(isNative(value.asObject()))) return ClassReflection(value.asObject()).collectAncestors(ancestors); + + zv::Val interfaces = pt_type_call(value.asObject(), PT_LC("getinterfaces"), 0, NULL); + if (UNEXPECTED(interfaces.isUndef())) return false; + if (UNEXPECTED(!addAllToAncestors(ancestors, interfaces.ref()))) return false; + + zv::Val traits = pt_type_call(value.asObject(), PT_LC("gettraits"), 0, NULL); + if (UNEXPECTED(traits.isUndef())) return false; + if (UNEXPECTED(!addAllToAncestors(ancestors, traits.ref()))) return false; + + zv::Val parent = pt_type_call(value.asObject(), PT_LC("getparentclass"), 0, NULL); + if (UNEXPECTED(parent.isUndef())) return false; + if (parent.isNull()) return true; + + return addToAncestors(ancestors, parent.ref()); + } + + /* foreach ($classReflections as $classReflection) $addToAncestors($classReflection) */ + static bool addAllToAncestors(zv::Arr &ancestors, zv::Ref classReflections) + { + if (!classReflections.isArray()) return true; + for (auto entry : zv::ArrRef(classReflections.raw())) { + if (UNEXPECTED(!addToAncestors(ancestors, entry.value()))) return false; + } + return true; + } + + /* $addToAncestors($classReflection): a class not collected yet is added + * and descended into */ + static bool addToAncestors(zv::Arr &ancestors, zv::Ref classReflection) + { + zv::Val name = crGetName(classReflection); + if (UNEXPECTED(name.isUndef())) return false; + zv::Str nameStr = zv::Str::adopt(zval_get_string(name.raw())); + if (ancestors.arrRef().exists(nameStr.get())) return true; + + ancestors.set(nameStr.get(), zv::Val::copyOf(classReflection)); + return collectAncestorsOf(classReflection, ancestors); + } + + zv::Val getAncestorWithClassName(zend_string *className) + { + zv::Val ancestors = getAncestors(); + if (UNEXPECTED(ancestors.isUndef())) return zv::Val(); + if (!ancestors.ref().isArray()) return zv::Val::null(); + zval *found = zend_symtable_find(ancestors.ref().asArrayTable(), className); + if (found == NULL) return zv::Val::null(); + + return zv::Val::copyOf(zv::Ref(found)); + } + + /* list */ + zv::Val getResolvedMixinTypes() + { + zv::Val mixinTags = getMixinTags(); + if (UNEXPECTED(mixinTags.isUndef())) return zv::Val(); + zv::Arr types = zv::Arr::create(countOf(mixinTags.ref())); + if (mixinTags.ref().isArray()) { + for (auto entry : zv::ArrRef(mixinTags.raw())) { + zv::Val type = callOn(entry.value(), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + bool generic; + if (UNEXPECTED(!isGeneric(generic))) return zv::Val(); + if (!generic) { + types.push(std::move(type)); + continue; + } + + zv::Val activeTemplateTypeMap = getActiveTemplateTypeMap(); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = getCallSiteVarianceMap(); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val staticVariance = templateTypeVarianceStatic(); + if (UNEXPECTED(staticVariance.isUndef())) return zv::Val(); + zv::Val resolved = pt_type_template_type_helper_resolve_template_types(type.raw(), activeTemplateTypeMap.raw(), callSiteVarianceMap.raw(), staticVariance.raw(), false); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + types.push(std::move(resolved)); + } + } + + return zv::Val(std::move(types)); + } + + /* array|null, memoized in $allowedSubTypes once resolved */ + zv::Val getAllowedSubTypes() + { + if (slot(PT_CR_PROP_ALLOWED_SUB_TYPES_RESOLVED).isTrue()) return zv::Val::copyOf(slot(PT_CR_PROP_ALLOWED_SUB_TYPES)); + + writeSlot(PT_CR_PROP_ALLOWED_SUB_TYPES_RESOLVED, zv::Val::boolean(true)); + zv::Val extensions = registryGet(PT_REGISTRY_ALLOWED_SUB_TYPES_EXTENSIONS); + if (UNEXPECTED(extensions.isUndef())) return zv::Val(); + if (extensions.ref().isArray()) { + for (auto entry : zv::ArrRef(extensions.raw())) { + zval arg; + ZVAL_OBJ(&arg, self); + bool supports; + if (UNEXPECTED(!callBool(entry.value(), PT_LC("supports"), 1, &arg, supports))) return zv::Val(); + if (supports) { + zv::Val allowedSubTypes = callOn(entry.value(), PT_LC("getallowedsubtypes"), 1, &arg); + if (UNEXPECTED(allowedSubTypes.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(allowedSubTypes.raw()) != IS_ARRAY && Z_TYPE_P(allowedSubTypes.raw()) != IS_NULL)) { + zend_type_error("Cannot assign %s to property PHPStan\\Reflection\\ClassReflection::$allowedSubTypes of type ?array", zend_zval_value_name(allowedSubTypes.raw())); + return zv::Val(); + } + writeSlot(PT_CR_PROP_ALLOWED_SUB_TYPES, zv::Val::copyOf(zv::Ref(allowedSubTypes.raw()))); + return allowedSubTypes; + } + } + } + + return zv::Val::null(); + } + + /* }}} */ + + /* {{{ out of the twin's file order: the finality and + * genericness queries getCacheKey() / getParentClass() / + * isSubclassOfClass() need, and the private ancestor-resolution + * helpers of getParentClass() / getImmediateInterfaces() */ + + bool isFinal(bool &out) + { + bool finalByKeyword = false; + if (UNEXPECTED(!isFinalByKeyword(finalByKeyword))) return false; + if (finalByKeyword) { + out = true; + return true; + } + + zv::Ref memo = slot(PT_CR_PROP_IS_FINAL); + if (memo.isNull()) { + zv::Val resolvedPhpDoc = getResolvedPhpDoc(); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return false; + bool isFinal_ = false; + if (!resolvedPhpDoc.isNull() && UNEXPECTED(!callBool(resolvedPhpDoc.ref(), PT_LC("isfinal"), 0, NULL, isFinal_))) return false; + writeSlot(PT_CR_PROP_IS_FINAL, zv::Val::boolean(isFinal_)); + } + + out = slot(PT_CR_PROP_IS_FINAL).isTrue(); + return true; + } + + bool hasFinalByKeywordOverride(bool &out) const + { + zv::Ref override_ = slot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE); + if (UNEXPECTED(override_.isUndef())) { + (void) uninitializedProperty("finalByKeywordOverride"); + return false; + } + out = !override_.isNull(); + return true; + } + + bool isFinalByKeyword(bool &out) const + { + bool anonymous = false; + if (UNEXPECTED(!isAnonymous(anonymous))) return false; + if (anonymous) { + out = true; + return true; + } + + zv::Ref override_ = slot(PT_CR_PROP_FINAL_BY_KEYWORD_OVERRIDE); + if (UNEXPECTED(override_.isUndef())) { + (void) uninitializedProperty("finalByKeywordOverride"); + return false; + } + if (!override_.isNull()) { + out = zend_is_true(override_.raw()); + return true; + } + + return reflectionCallBool(PT_LC("isfinal"), out); + } + + bool isGeneric(bool &out) + { + zv::Ref memo = slot(PT_CR_PROP_IS_GENERIC); + if (memo.isNull()) { + bool isEnum_; + if (UNEXPECTED(!isEnum(isEnum_))) return false; + if (isEnum_) { + writeSlot(PT_CR_PROP_IS_GENERIC, zv::Val::boolean(false)); + out = false; + return true; + } + + zv::Val templateTags = getTemplateTags(); + if (UNEXPECTED(templateTags.isUndef())) return false; + writeSlot(PT_CR_PROP_IS_GENERIC, zv::Val::boolean(countOf(templateTags.ref()) > 0)); + } + + out = slot(PT_CR_PROP_IS_GENERIC).isTrue(); + return true; + } + + /* private; the first @extends tag or null */ + zv::Val getFirstExtendsTag() + { + zv::Val tags = getExtendsTags(); + if (UNEXPECTED(tags.isUndef())) return zv::Val(); + if (tags.ref().isArray()) { + for (auto entry : zv::ArrRef(tags.raw())) { + return zv::Val::copyOf(entry.value()); + } + } + + return zv::Val::null(); + } + + /* private; whether $type is a generic object type of one of the + * ancestor classes (the twin's list holds the one parent name here) */ + bool isValidAncestorType(zv::Ref type, zend_string *ancestorClass, bool &out) + { + if (!instanceOfShadowed(type, pt_ce_generic_object_type, PT_LC(PT_CR_GENERIC_OBJECT_TYPE_NAME))) { + out = false; + return true; + } + + zv::Val reflection = typeGetClassReflection(type); + if (UNEXPECTED(reflection.isUndef())) return false; + if (reflection.isNull()) { + out = false; + return true; + } + + zv::Val name = crGetName(reflection.ref()); + if (UNEXPECTED(name.isUndef())) return false; + out = Z_TYPE_P(name.raw()) == IS_STRING && zend_string_equals(Z_STR_P(name.raw()), ancestorClass); + return true; + } + + /* private; the possibly incomplete active map with its ErrorType + * entries resolved to the template types' defaults where the bound is + * not mixed — the twin's map() closure, expanded */ + zv::Val getActiveTemplateTypeMapForAncestorResolution() + { + zv::Val map = getPossiblyIncompleteActiveTemplateTypeMap(); + if (UNEXPECTED(map.isUndef())) return zv::Val(); + zv::Val templateTypeMap = getTemplateTypeMap(); + if (UNEXPECTED(templateTypeMap.isUndef())) return zv::Val(); + zv::Val types = callOn(map.ref(), PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(types.isUndef())) return zv::Val(); + + zv::Arr mapped = zv::Arr::create(countOf(types.ref())); + if (types.ref().isArray()) { + for (auto entry : zv::ArrRef(types.raw())) { + zv::Val result; + if (!instanceOfShadowed(entry.value(), pt_ce_error_type, PT_LC(PT_CR_ERROR_TYPE_NAME))) { + result = zv::Val::copyOf(entry.value()); + } else { + zv::Val name = entry.stringKeyOrNull() != NULL ? zv::Val::string(entry.stringKeyOrNull()) : zv::Val::adoptString(zend_long_to_str((zend_long) entry.indexKey())); + zv::Val templateType = callOn(templateTypeMap.ref(), PT_LC("gettype"), 1, name.raw()); + if (UNEXPECTED(templateType.isUndef())) return zv::Val(); + bool isTemplate; + if (UNEXPECTED(!pt_type_instanceof(templateType.raw(), PT_CLASS_TEMPLATE_TYPE, isTemplate))) return zv::Val(); + if (!isTemplate) { + result = zv::Val::copyOf(entry.value()); + } else { + zv::Val bound = callOn(templateType.ref(), PT_LC("getbound"), 0, NULL); + if (UNEXPECTED(bound.isUndef())) return zv::Val(); + if (instanceOfShadowed(bound.ref(), pt_ce_mixed_type, PT_LC(PT_CR_MIXED_TYPE_NAME))) { + result = zv::Val::copyOf(entry.value()); + } else { + result = pt_type_template_type_helper_resolve_to_defaults(templateType.raw()); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + } + } + } + if (entry.stringKeyOrNull() != NULL) { + mapped.set(entry.stringKeyOrNull(), std::move(result)); + } else { + mapped.arrRef().setIndex(entry.indexKey(), result.ref()); + } + } + } + + zval out; + if (UNEXPECTED(!pt_template_type_map_new(&out, mapped.raw(), NULL))) return zv::Val(); + return zv::Val::adopt(out); + } + + /* }}} */ + +private: + zend_object *self; + zval selfZval; +}; + +} // namespace phpstanturbo + +using phpstanturbo::ClassReflection; + +/* {{{ the getters the Type kernel calls millions of times per run + * + * getName() 1.3M, isGeneric() 1.6M, hasMethod() 0.8M, getCacheKey() 0.6M in + * a self-analysis of src/Analyser, src/Rules and src/Type. The shadowing + * class is final, so an object of exactly pt_ce_class_reflection runs the + * native body directly — no zend_call_function, no frame. A foreign object + * (a test double, a class reflection built by something else) still goes + * through the PHP method, which is what the twin's callers would do. */ + +/* $classReflection->getName(); UNDEF = pending exception */ +zv::Val pt_class_reflection_get_name(zend_object *classReflection) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection)) return ClassReflection(classReflection).getName(); + return pt_type_call(classReflection, PT_LC("getname"), 0, NULL); +} + +/* $classReflection->getCacheKey(); UNDEF = pending exception */ +zv::Val pt_class_reflection_get_cache_key(zend_object *classReflection) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection)) return ClassReflection(classReflection).getCacheKey(); + return pt_type_call(classReflection, PT_LC("getcachekey"), 0, NULL); +} + +/* $classReflection->getNativeReflection(); UNDEF = pending exception */ +zv::Val pt_class_reflection_get_native_reflection(zend_object *classReflection) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection)) return ClassReflection(classReflection).getNativeReflection(); + return pt_type_call(classReflection, PT_LC("getnativereflection"), 0, NULL); +} + +/* $method(...) on a foreign object, coerced to bool; false = pending + * exception */ +[[nodiscard]] static bool pt_cr_foreign_bool(zend_object *classReflection, const char *lcname, size_t len, uint32_t argc, zval *argv, bool &out) +{ + zv::Val result = pt_type_call(classReflection, lcname, len, argc, argv); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; +} + +/* $classReflection->isGeneric(); false = pending exception */ +[[nodiscard]] bool pt_class_reflection_is_generic(zend_object *classReflection, bool &out) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection)) return ClassReflection(classReflection).isGeneric(out); + return pt_cr_foreign_bool(classReflection, PT_LC("isgeneric"), 0, NULL, out); +} + +/* $classReflection->hasMethod($methodName); false = pending exception */ +[[nodiscard]] bool pt_class_reflection_has_method(zend_object *classReflection, zval *methodName, bool &out) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection && Z_TYPE_P(methodName) == IS_STRING)) { + return ClassReflection(classReflection).hasMethod(Z_STR_P(methodName), out); + } + return pt_cr_foreign_bool(classReflection, PT_LC("hasmethod"), 1, methodName, out); +} + +/* $classReflection->hasFinalByKeywordOverride(); false = pending exception */ +[[nodiscard]] bool pt_class_reflection_has_final_by_keyword_override(zend_object *classReflection, bool &out) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection)) return ClassReflection(classReflection).hasFinalByKeywordOverride(out); + return pt_cr_foreign_bool(classReflection, PT_LC("hasfinalbykeywordoverride"), 0, NULL, out); +} + +/* $classReflection->isEnum(); false = pending exception */ +[[nodiscard]] bool pt_class_reflection_is_enum(zend_object *classReflection, bool &out) +{ + if (EXPECTED(classReflection->ce == pt_ce_class_reflection)) return ClassReflection(classReflection).isEnum(out); + return pt_cr_foreign_bool(classReflection, PT_LC("isenum"), 0, NULL, out); +} + +/* }}} */ + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#define PT_THIS ClassReflection(Z_OBJ_P(ZEND_THIS)) + +/* a `bool method(bool &out)` body into return_value */ +#define PT_CR_RETURN_BOOL(expr) \ + do { \ + bool out_; \ + if (UNEXPECTED(!(expr))) { \ + RETURN_THROWS(); \ + } \ + RETURN_BOOL(out_); \ + } while (0) + +namespace pt_cr { +/* the twin's parameter and return class names (persistent literals) */ +inline constexpr const char *self = "PHPStan\\Reflection\\ClassReflection"; +inline constexpr const char *classReflectionFactory = "PHPStan\\Reflection\\ClassReflectionFactory"; +inline constexpr const char *initializerExprTypeResolver = "PHPStan\\Reflection\\InitializerExprTypeResolver"; +inline constexpr const char *fileTypeMapper = "PHPStan\\Type\\FileTypeMapper"; +inline constexpr const char *stubPhpDocProvider = "PHPStan\\PhpDoc\\StubPhpDocProvider"; +inline constexpr const char *phpDocInheritanceResolver = "PHPStan\\PhpDoc\\PhpDocInheritanceResolver"; +inline constexpr const char *signatureMapProvider = "PHPStan\\Reflection\\SignatureMap\\SignatureMapProvider"; +inline constexpr const char *deprecationProvider = "PHPStan\\Reflection\\Deprecation\\DeprecationProvider"; +inline constexpr const char *attributeReflectionFactory = "PHPStan\\Reflection\\AttributeReflectionFactory"; +inline constexpr const char *classReflectionExtensionRegistryProvider = "PHPStan\\DependencyInjection\\Reflection\\ClassReflectionExtensionRegistryProvider"; +inline constexpr const char *coreReflectionClass = "ReflectionClass"; +inline constexpr const char *templateTypeMap = "PHPStan\\Type\\Generic\\TemplateTypeMap"; +inline constexpr const char *templateTypeVarianceMap = "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"; +inline constexpr const char *closure = "Closure"; +inline constexpr const char *extendsTag = "PHPStan\\PhpDoc\\Tag\\ExtendsTag"; +inline constexpr const char *resolvedPhpDocBlock = "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"; + +} // namespace pt_cr + +void pt_register_class_reflection() +{ + using namespace pt_cr; + + reg::Class cls("PHPStan\\Reflection\\ClassReflection"); + ptdecl::ClassReflection::declareClass(cls); + + /* {{{ the slots, in the twin's declaration order (the PT_CR_PROP_* + * enum): the class-body properties with their defaults, the static + * one in its place, then the promoted constructor properties, + * uninitialized until the constructor writes them */ + cls.property("methods", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("properties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("instanceProperties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("staticProperties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("constants", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("enumCases", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_ARRAY | MAY_BE_NULL); + cls.property("classHierarchyDistances", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_ARRAY | MAY_BE_NULL); + cls.property("deprecatedDescription", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_STRING | MAY_BE_NULL); + cls.property("isDeprecated", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("allowedSubTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_ARRAY | MAY_BE_NULL); + cls.property("allowedSubTypesResolved", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); + cls.property("isGeneric", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("isInternal", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("isFinal", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("isImmutable", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("hasConsistentConstructor", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("acceptsNamedArguments", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_BOOL | MAY_BE_NULL); + cls.property("templateTypeMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, templateTypeMap); + cls.property("activeTemplateTypeMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, templateTypeMap); + cls.property("defaultCallSiteVarianceMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, templateTypeVarianceMap); + cls.property("callSiteVarianceMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, templateTypeVarianceMap); + cls.property("ancestors", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_ARRAY | MAY_BE_NULL); + cls.property("cacheKey", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_STRING | MAY_BE_NULL); + cls.property("subclasses", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("filename", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_STRING | MAY_BE_FALSE | MAY_BE_NULL); + cls.property("reflectionDocComment", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_STRING | MAY_BE_FALSE | MAY_BE_NULL); + cls.property("stubPhpDocBlock", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_FALSE | MAY_BE_NULL, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); + cls.property("resolvedPhpDocBlock", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_FALSE, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); + cls.property("traitContextResolvedPhpDocBlock", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_FALSE, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); + cls.property("cachedInterfaces", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_ARRAY | MAY_BE_NULL); + cls.property("cachedParentClass", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_FALSE | MAY_BE_NULL, "self"); + cls.property("circularParentClassName", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_STRING | MAY_BE_FALSE | MAY_BE_NULL); + cls.property("typeAliases", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_ARRAY | MAY_BE_NULL); + cls.privateStaticTypedArrayPropertyDefaultEmpty("resolvingTypeAliasImports"); + cls.property("hasMethodCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasPropertyCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasInstancePropertyCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasStaticPropertyCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("name", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_STRING | MAY_BE_NULL); + cls.property("classReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, classReflectionFactory); + cls.property("reflectionProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, ptcls::reflectionProvider); + cls.property("initializerExprTypeResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, initializerExprTypeResolver); + cls.property("fileTypeMapper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, fileTypeMapper); + cls.property("stubPhpDocProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, stubPhpDocProvider); + cls.property("phpDocInheritanceResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, phpDocInheritanceResolver); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, ptcls::phpVersion); + cls.property("signatureMapProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, signatureMapProvider); + cls.property("deprecationProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, deprecationProvider); + cls.property("attributeReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, attributeReflectionFactory); + cls.property("classReflectionExtensionRegistryProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, classReflectionExtensionRegistryProvider); + cls.property("displayName", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING); + cls.property("reflection", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, coreReflectionClass); + cls.property("anonymousFilename", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING | MAY_BE_NULL); + cls.property("resolvedTemplateTypeMap", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, templateTypeMap); + cls.property("stubPhpDocBlockCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, closure); + cls.property("extraCacheKey", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING | MAY_BE_NULL); + cls.property("resolvedCallSiteVarianceMap", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, templateTypeVarianceMap); + cls.property("finalByKeywordOverride", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL | MAY_BE_NULL); + /* }}} */ + + cls.method("__construct", reg::Public, 16, { + reg::obj("classReflectionFactory", classReflectionFactory), + reg::obj("reflectionProvider", ptcls::reflectionProvider), + reg::obj("initializerExprTypeResolver", initializerExprTypeResolver), + reg::obj("fileTypeMapper", fileTypeMapper), + reg::obj("stubPhpDocProvider", stubPhpDocProvider), + reg::obj("phpDocInheritanceResolver", phpDocInheritanceResolver), + reg::obj("phpVersion", ptcls::phpVersion), + reg::obj("signatureMapProvider", signatureMapProvider), + reg::obj("deprecationProvider", deprecationProvider), + reg::obj("attributeReflectionFactory", attributeReflectionFactory), + reg::obj("classReflectionExtensionRegistryProvider", classReflectionExtensionRegistryProvider), + reg::stringArg("displayName"), + reg::obj("reflection", coreReflectionClass), + reg::stringArg("anonymousFilename", true), + reg::obj("resolvedTemplateTypeMap", templateTypeMap, true), + reg::obj("stubPhpDocBlockCallback", closure, true), + reg::withDefault(reg::stringArg("extraCacheKey", true), "null"), + reg::withDefault(reg::obj("resolvedCallSiteVarianceMap", templateTypeVarianceMap, true), "null"), + reg::withDefault({ "finalByKeywordOverride", MAY_BE_BOOL | MAY_BE_NULL | reg::detail::flagBits(false, false), nullptr }, "null"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + ClassReflection::ConstructArgs a = {}; + a.finalByKeywordOverrideIsNull = true; + ZEND_PARSE_PARAMETERS_START(16, 19) + Z_PARAM_OBJECT(a.classReflectionFactory) + Z_PARAM_OBJECT(a.reflectionProvider) + Z_PARAM_OBJECT(a.initializerExprTypeResolver) + Z_PARAM_OBJECT(a.fileTypeMapper) + Z_PARAM_OBJECT(a.stubPhpDocProvider) + Z_PARAM_OBJECT(a.phpDocInheritanceResolver) + Z_PARAM_OBJECT(a.phpVersion) + Z_PARAM_OBJECT(a.signatureMapProvider) + Z_PARAM_OBJECT(a.deprecationProvider) + Z_PARAM_OBJECT(a.attributeReflectionFactory) + Z_PARAM_OBJECT(a.classReflectionExtensionRegistryProvider) + Z_PARAM_STR(a.displayName) + Z_PARAM_OBJECT(a.reflection) + Z_PARAM_STR_OR_NULL(a.anonymousFilename) + Z_PARAM_OBJECT_OR_NULL(a.resolvedTemplateTypeMap) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(a.stubPhpDocBlockCallback, zend_ce_closure) + Z_PARAM_OPTIONAL + Z_PARAM_STR_OR_NULL(a.extraCacheKey) + Z_PARAM_OBJECT_OR_NULL(a.resolvedCallSiteVarianceMap) + Z_PARAM_BOOL_OR_NULL(a.finalByKeywordOverride, a.finalByKeywordOverrideIsNull) + ZEND_PARSE_PARAMETERS_END(); + PT_THIS.construct(a); + }); + + cls.method<&ClassReflection::getNativeReflection>(sigs::getNativeReflection); + + cls.method<&ClassReflection::getFileName>(sigs::getFileName); + + cls.method<&ClassReflection::getParentClass>(sigs::getParentClass); + + cls.method<&ClassReflection::getName>(sigs::getName); + + cls.method(sigs::getDisplayName, [](INTERNAL_FUNCTION_PARAMETERS) { + bool withTemplateTypes = true; + if (!zp::parse>(execute_data, withTemplateTypes)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getDisplayName(withTemplateTypes)); + }); + + cls.method<&ClassReflection::getCacheKey>(sigs::getCacheKey); + + cls.method<&ClassReflection::getClassHierarchyDistances>(sigs::getClassHierarchyDistances); + + cls.method(sigs::findCircularParentClassName, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *parentClassName; + if (!zp::parse(execute_data, parentClassName)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.findCircularParentClassName(parentClassName)); + }); + + cls.method(sigs::collectAncestors, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *ancestorsArg; + if (!zp::parse(execute_data, ancestorsArg)) RETURN_THROWS(); + zval *ancestorsZv = ancestorsArg; + ZVAL_DEREF(ancestorsZv); + if (UNEXPECTED(Z_TYPE_P(ancestorsZv) != IS_ARRAY)) { + zend_argument_type_error(1, "must be of type array, %s given", zend_zval_value_name(ancestorsZv)); + RETURN_THROWS(); + } + zv::Arr ancestors = zv::Arr::copyOfTable(Z_ARRVAL_P(ancestorsZv)); + bool collected = PT_THIS.collectAncestors(ancestors); + zval written = ancestors.take(); + zval_ptr_dtor(ancestorsZv); + ZVAL_COPY_VALUE(ancestorsZv, &written); + if (UNEXPECTED(!collected)) RETURN_THROWS(); + }); + + cls.method(sigs::collectTraits, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + if (!zp::parse(execute_data, classReflection)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.collectTraits(zv::Ref(classReflection))); + }); + + cls.method<&ClassReflection::allowsDynamicProperties>(sigs::allowsDynamicProperties); + + cls.method<&ClassReflection::hasProperty, zp::Str>(sigs::hasProperty); + + cls.method<&ClassReflection::hasInstanceProperty, zp::Str>(sigs::hasInstanceProperty); + + cls.method<&ClassReflection::hasStaticProperty, zp::Str>(sigs::hasStaticProperty); + + cls.method<&ClassReflection::hasMethod, zp::Str>(sigs::hasMethod); + + cls.method<&ClassReflection::getMethod, zp::Str, zp::Obj>(sigs::getMethod); + + cls.method(sigs::wrapExtendedMethod, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *method; + if (!zp::parse(execute_data, method)) RETURN_THROWS(); + PT_RETURN_VAL(ClassReflection::wrapExtendedMethod(zv::Val::copyOf(zv::Ref(method)))); + }); + + cls.method(sigs::wrapExtendedProperty, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *propertyName; + zval *property; + if (!zp::parse(execute_data, propertyName, property)) RETURN_THROWS(); + PT_RETURN_VAL(ClassReflection::wrapExtendedProperty(propertyName, zv::Val::copyOf(zv::Ref(property)))); + }); + + cls.method<&ClassReflection::hasNativeMethod, zp::Str>(sigs::hasNativeMethod); + + cls.method<&ClassReflection::getNativeMethod, zp::Str>(sigs::getNativeMethod); + + cls.method<&ClassReflection::hasConstructor>(sigs::hasConstructor); + + cls.method<&ClassReflection::getConstructor>(sigs::getConstructor); + + cls.method<&ClassReflection::findConstructor>(sigs::findConstructor); + + cls.method(sigs::evictPrivateSymbols, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + if (UNEXPECTED(!PT_THIS.evictPrivateSymbols())) RETURN_THROWS(); + }); + + cls.method<&ClassReflection::getProperty, zp::Str, zp::Obj>(sigs::getProperty); + + cls.method<&ClassReflection::getInstanceProperty, zp::Str, zp::Obj>(sigs::getInstanceProperty); + + cls.method<&ClassReflection::getStaticProperty, zp::Str>(sigs::getStaticProperty); + + cls.method<&ClassReflection::hasNativeProperty, zp::Str>(sigs::hasNativeProperty); + + cls.method<&ClassReflection::getNativeProperty, zp::Str>(sigs::getNativeProperty); + + cls.method<&ClassReflection::isAbstract>(sigs::isAbstract); + + cls.method<&ClassReflection::isInterface>(sigs::isInterface); + + cls.method<&ClassReflection::isTrait>(sigs::isTrait); + + cls.method<&ClassReflection::isEnum>(sigs::isEnum); + + cls.method<&ClassReflection::getClassTypeDescription>(sigs::getClassTypeDescription); + + cls.method<&ClassReflection::isReadOnly>(sigs::isReadOnly); + + cls.method<&ClassReflection::isBackedEnum>(sigs::isBackedEnum); + + cls.method<&ClassReflection::getBackedEnumType>(sigs::getBackedEnumType); + + cls.method<&ClassReflection::hasEnumCase, zp::Str>(sigs::hasEnumCase); + + cls.method<&ClassReflection::getEnumCases>(sigs::getEnumCases); + + cls.method<&ClassReflection::getEnumCase, zp::Str>(sigs::getEnumCase); + + cls.method<&ClassReflection::isClass>(sigs::isClass); + + cls.method<&ClassReflection::isAnonymous>(sigs::isAnonymous); + + cls.method<&ClassReflection::is, zp::Str>(sigs::is); + + cls.method<&ClassReflection::isSubclassOf, zp::Str>(sigs::isSubclassOf); + + cls.method(sigs::isSubclassOfClass, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + if (!zp::parse(execute_data, classReflection)) RETURN_THROWS(); + PT_CR_RETURN_BOOL(PT_THIS.isSubclassOfClass(zv::Ref(classReflection), out_)); + }); + + cls.method<&ClassReflection::implementsInterface, zp::Str>(sigs::implementsInterface); + + cls.method<&ClassReflection::getParents>(sigs::getParents); + + cls.method<&ClassReflection::getInterfaces>(sigs::getInterfaces); + + cls.method(sigs::collectInterfaces, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *interface; + if (!zp::parse(execute_data, interface)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.collectInterfaces(zv::Ref(interface))); + }); + + cls.method<&ClassReflection::getImmediateInterfaces>(sigs::getImmediateInterfaces); + + cls.method(sigs::getTraits, [](INTERNAL_FUNCTION_PARAMETERS) { + bool recursive = false; + if (!zp::parse>(execute_data, recursive)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getTraits(recursive)); + }); + + cls.method<&ClassReflection::getParentClassesNames>(sigs::getParentClassesNames); + + cls.method<&ClassReflection::hasConstant, zp::Str>(sigs::hasConstant); + + cls.method<&ClassReflection::getConstant, zp::Str>(sigs::getConstant); + + cls.method<&ClassReflection::getConstantPhpDocType, zp::Str>(sigs::getConstantPhpDocType); + + cls.method(sigs::findConstantResolvedPhpDoc, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *reflectionConstant; + if (!zp::parse(execute_data, reflectionConstant)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.findConstantResolvedPhpDoc(zv::Ref(reflectionConstant))); + }); + + cls.method(sigs::resolveConstantVarPhpDocType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *resolvedPhpDoc, *nativeType, *declaringClass; + if (!zp::parse(execute_data, resolvedPhpDoc, nativeType, declaringClass)) RETURN_THROWS(); + zval nullType; + ZVAL_NULL(&nullType); + PT_RETURN_VAL(ClassReflection::resolveConstantVarPhpDocType(zv::Ref(resolvedPhpDoc), zv::Ref(nativeType == NULL ? &nullType : nativeType), zv::Ref(declaringClass))); + }); + + cls.method<&ClassReflection::hasTraitUse, zp::Str>(sigs::hasTraitUse); + + cls.method<&ClassReflection::getTraitNames>(sigs::getTraitNames); + + cls.method<&ClassReflection::getTypeAliases>(sigs::getTypeAliases); + + cls.method<&ClassReflection::getDeprecatedDescription>(sigs::getDeprecatedDescription); + + cls.method<&ClassReflection::isDeprecated>(sigs::isDeprecated); + + cls.method(sigs::resolveDeprecation, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + if (UNEXPECTED(!PT_THIS.resolveDeprecation())) RETURN_THROWS(); + }); + + cls.method<&ClassReflection::isBuiltin>(sigs::isBuiltin); + + cls.method<&ClassReflection::isInternal>(sigs::isInternal); + + cls.method<&ClassReflection::isImmutable>(sigs::isImmutable); + + cls.method<&ClassReflection::hasConsistentConstructor>(sigs::hasConsistentConstructor); + + cls.method<&ClassReflection::acceptsNamedArguments>(sigs::acceptsNamedArguments); + + cls.method<&ClassReflection::isAttributeClass>(sigs::isAttributeClass); + + cls.method<&ClassReflection::findAttributeFlags>(sigs::findAttributeFlags); + + cls.method<&ClassReflection::getAttributes>(sigs::getAttributes); + + cls.method<&ClassReflection::getAttributeClassFlags>(sigs::getAttributeClassFlags); + + cls.method<&ClassReflection::getObjectType>(sigs::getObjectType); + + cls.method<&ClassReflection::getTemplateTypeMap>(sigs::getTemplateTypeMap); + + cls.method<&ClassReflection::getActiveTemplateTypeMap>(sigs::getActiveTemplateTypeMap); + + cls.method<&ClassReflection::getPossiblyIncompleteActiveTemplateTypeMap>(sigs::getPossiblyIncompleteActiveTemplateTypeMap); + + cls.method<&ClassReflection::getDefaultCallSiteVarianceMap>(sigs::getDefaultCallSiteVarianceMap); + + cls.method<&ClassReflection::getCallSiteVarianceMap>(sigs::getCallSiteVarianceMap); + + cls.method(sigs::typeMapFromList, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *types; + if (!zp::parse(execute_data, types)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.typeMapFromList(zv::Ref(types))); + }); + + cls.method(sigs::varianceMapFromList, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *variances; + if (!zp::parse(execute_data, variances)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.varianceMapFromList(zv::Ref(variances))); + }); + + cls.method(sigs::typeMapToList, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *typeMap; + if (!zp::parse(execute_data, typeMap)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.typeMapToList(zv::Ref(typeMap))); + }); + + cls.method(sigs::varianceMapToList, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *varianceMap; + if (!zp::parse(execute_data, varianceMap)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.varianceMapToList(zv::Ref(varianceMap))); + }); + + cls.method(sigs::withTypes, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *types; + if (!zp::parse(execute_data, types)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.withTypes(zv::Ref(types))); + }); + + cls.method(sigs::withVariances, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *variances; + if (!zp::parse(execute_data, variances)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.withVariances(zv::Ref(variances))); + }); + + cls.method<&ClassReflection::asFinal>(sigs::asFinal); + + cls.method<&ClassReflection::withoutFinalByKeywordOverride>(sigs::withoutFinalByKeywordOverride); + + cls.method<&ClassReflection::removeFinalKeywordOverride>(sigs::removeFinalKeywordOverride); + + cls.method<&ClassReflection::getResolvedPhpDoc>(sigs::getResolvedPhpDoc); + + cls.method(sigs::getTraitContextResolvedPhpDoc, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *implementingClass; + if (!zp::parse(execute_data, implementingClass)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getTraitContextResolvedPhpDoc(zv::Ref(implementingClass))); + }); + + cls.method<&ClassReflection::getExtendsTags>(sigs::getExtendsTags); + + cls.method<&ClassReflection::getImplementsTags>(sigs::getImplementsTags); + + cls.method<&ClassReflection::getTemplateTags>(sigs::getTemplateTags); + + cls.method<&ClassReflection::getAncestors>(sigs::getAncestors); + + cls.method<&ClassReflection::getAncestorWithClassName, zp::Str>(sigs::getAncestorWithClassName); + + cls.method<&ClassReflection::getMixinTags>(sigs::getMixinTags); + + cls.method<&ClassReflection::getRequireExtendsTags>(sigs::getRequireExtendsTags); + + cls.method<&ClassReflection::getRequireImplementsTags>(sigs::getRequireImplementsTags); + + cls.method<&ClassReflection::getSealedTags>(sigs::getSealedTags); + + cls.method<&ClassReflection::getPropertyTags>(sigs::getPropertyTags); + + cls.method<&ClassReflection::getMethodTags>(sigs::getMethodTags); + + cls.method<&ClassReflection::getResolvedMixinTypes>(sigs::getResolvedMixinTypes); + + cls.method<&ClassReflection::getAllowedSubTypes>(sigs::getAllowedSubTypes); + + /* out of the twin's file order (see the handle class) */ + cls.method<&ClassReflection::isFinal>(sigs::isFinal); + + cls.method<&ClassReflection::hasFinalByKeywordOverride>(sigs::hasFinalByKeywordOverride); + + cls.method<&ClassReflection::isFinalByKeyword>(sigs::isFinalByKeyword); + + cls.method<&ClassReflection::isGeneric>(sigs::isGeneric); + + cls.method<&ClassReflection::getFirstExtendsTag>(sigs::getFirstExtendsTag); + + cls.method(sigs::isValidAncestorType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type, *ancestorClasses; + if (!zp::parse(execute_data, type, ancestorClasses)) RETURN_THROWS(); + /* in_array($reflection->getName(), $ancestorClasses, true) over the + * whole list: the C++ body takes the one name every twin site + * passes, so the private method walks the list here */ + if (!ClassReflection::instanceOfShadowed(zv::Ref(type), pt_ce_generic_object_type, PT_LC(PT_CR_GENERIC_OBJECT_TYPE_NAME))) { + RETURN_FALSE; + } + zv::Val reflection = ClassReflection::typeGetClassReflection(zv::Ref(type)); + if (UNEXPECTED(reflection.isUndef())) RETURN_THROWS(); + if (reflection.isNull()) { + RETURN_FALSE; + } + zv::Val name = ClassReflection::crGetName(reflection.ref()); + if (UNEXPECTED(name.isUndef())) RETURN_THROWS(); + for (auto entry : zv::ArrRef(ancestorClasses)) { + if (Z_TYPE_P(name.raw()) == IS_STRING && entry.value().isString() && zend_string_equals(Z_STR_P(name.raw()), entry.value().asString())) { + RETURN_TRUE; + } + } + RETURN_FALSE; + }); + + cls.method<&ClassReflection::getActiveTemplateTypeMapForAncestorResolution>(sigs::getActiveTemplateTypeMapForAncestorResolution); + + cls.shadow(&pt_ce_class_reflection); +} + +/* }}} */ diff --git a/turbo-ext/src/ClassReflectionAccess.cpp b/turbo-ext/src/ClassReflectionAccess.cpp deleted file mode 100644 index 3c3d94eb89b..00000000000 --- a/turbo-ext/src/ClassReflectionAccess.cpp +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Native readers of PHPStan\Reflection\ClassReflection's memo slots and of - * a MutatingScope's ScopeContext. - * - * ClassReflection stays a PHP class (final, userland); the Type kernel - * calls a handful of its trivial memoized getters millions of times per run - * (getName() 1.3M, isGeneric() 1.6M, hasMethod() 0.8M, getCacheKey() 0.6M - * in a self-analysis of src/Analyser, src/Rules and src/Type). Each getter - * reads a private property and returns it once it is computed, so the - * readers here answer straight from that property slot when it holds the - * answer and call the PHP method otherwise — lazy computation, memoization - * and the Error on an uninitialized slot stay the twin's by construction. - * Only an object of exactly the ClassReflection class entry (resolved once - * through the class map, its slot offsets cached per class entry) takes the - * fast path; anything else goes through the method as before. - * - * The same for MutatingScope::isInClass() / getClassReflection(), which - * read $this->context->getClassReflection(): when the scope is exactly a - * MutatingScope (a PHP subclass may override them) holding a native - * ScopeContext (ScopeContext.cpp), the class reflection comes out of the - * context's slot. - * - * The class entries are looked up without autoloading: an object of a class - * that is not declared cannot exist, so an undeclared ClassReflection or - * MutatingScope simply means "not this class" — no fast path, no loading - * the PHP code would not have loaded either. - */ - -#include "support.h" -#include "zv.h" -#include "TypeTraits.h" - -namespace { - -/* the property slot offsets of a class entry, resolved once; a user - * class's entry is per request without opcache, so rinit forgets them */ -struct ClassReflectionSlots -{ - zend_class_entry *ce; - uint32_t name; - uint32_t isGeneric; - uint32_t cacheKey; - uint32_t hasMethodCache; - uint32_t finalByKeywordOverride; - uint32_t reflection; -}; - -struct ScopeSlots -{ - zend_class_entry *ce; - uint32_t context; -}; - -ClassReflectionSlots pt_cr_slots = { NULL, 0, 0, 0, 0, 0, 0 }; -ScopeSlots pt_ms_slots = { NULL, 0 }; - -/* the slots of an object that is exactly a ClassReflection; NULL when it is - * of some other class (the caller then calls the method) — or, with - * `error` set and an exception pending, when the class map cannot resolve - * the class at all */ -const ClassReflectionSlots *classReflectionSlots(zend_object *object, bool &error) -{ - error = false; - if (EXPECTED(object->ce == pt_cr_slots.ce)) return &pt_cr_slots; - zend_class_entry *ce = pt_class_loaded(PT_CLASS_CLASS_REFLECTION); - if (ce == NULL) { - error = EG(exception) != NULL; - return NULL; - } - if (object->ce != ce) return NULL; - int32_t name = pt_instance_prop_offset(ce, PT_LC("name")); - int32_t isGeneric = pt_instance_prop_offset(ce, PT_LC("isGeneric")); - int32_t cacheKey = pt_instance_prop_offset(ce, PT_LC("cacheKey")); - int32_t hasMethodCache = pt_instance_prop_offset(ce, PT_LC("hasMethodCache")); - int32_t finalByKeywordOverride = pt_instance_prop_offset(ce, PT_LC("finalByKeywordOverride")); - int32_t reflection = pt_instance_prop_offset(ce, PT_LC("reflection")); - if (UNEXPECTED(name < 0 || isGeneric < 0 || cacheKey < 0 || hasMethodCache < 0 || finalByKeywordOverride < 0 || reflection < 0)) { - /* not the twin these readers know: every call goes through the method */ - return NULL; - } - pt_cr_slots = { ce, (uint32_t) name, (uint32_t) isGeneric, (uint32_t) cacheKey, (uint32_t) hasMethodCache, (uint32_t) finalByKeywordOverride, (uint32_t) reflection }; - return &pt_cr_slots; -} - -/* $object->method(...$args) through the object's own class entry, coerced - * to bool; false = pending exception */ -[[nodiscard]] bool callBool(zend_object *object, const char *lcname, size_t len, uint32_t argc, zval *argv, bool &out) -{ - zv::Val result = pt_type_call(object, lcname, len, argc, argv); - if (UNEXPECTED(result.isUndef())) return false; - out = zend_is_true(result.raw()); - return true; -} - -/* the subclass of MutatingScope last approved by the inheritance check */ -static zend_class_entry *pt_ms_inherited_ce = NULL; - -/* whether a subclass inherits isInClass() and getClassReflection() from - * MutatingScope itself (declared there, not re-declared below it) */ -static bool inheritsScopeGetters(zend_class_entry *ce, zend_class_entry *mutatingScope) -{ - static const struct { const char *name; size_t len; } methods[] = { { PT_LC("isinclass") }, { PT_LC("getclassreflection") } }; - for (const auto &method : methods) { - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, method.name, method.len); - if (fn == NULL || fn->common.scope != mutatingScope) return false; - } - return true; -} - -/* the $classReflection slot of the scope's context when the fast path - * applies (the scope exactly a MutatingScope, its context a native - * ScopeContext); NULL otherwise, with `error` set when the class map - * failed */ -zval *scopeClassReflectionSlot(zend_object *scope, bool &error) -{ - error = false; - if (scope->ce != pt_ms_slots.ce && scope->ce != pt_ms_inherited_ce) { - zend_class_entry *ce = pt_class_loaded(PT_CLASS_MUTATING_SCOPE); - if (ce == NULL) { - error = EG(exception) != NULL; - return NULL; - } - if (scope->ce != ce) { - /* a subclass (NodeCallbackScope) qualifies when it inherits both - * methods from MutatingScope unchanged: the bodies are then the - * twin's, reading the same inherited $context slot; the last - * such class is remembered (one subclass exists in practice) */ - if (!instanceof_function(scope->ce, ce) || !inheritsScopeGetters(scope->ce, ce)) return NULL; - } - int32_t context = pt_instance_prop_offset(ce, PT_LC("context")); - if (UNEXPECTED(context < 0)) return NULL; - if (scope->ce == ce) { - pt_ms_slots = { ce, (uint32_t) context }; - } else { - pt_ms_slots.context = (uint32_t) context; - pt_ms_inherited_ce = scope->ce; - } - } - zval *context = OBJ_PROP(scope, pt_ms_slots.context); - if (Z_TYPE_P(context) != IS_OBJECT || Z_OBJCE_P(context) != pt_ce_scope_context) return NULL; - return pt_scope_context_class_reflection(Z_OBJ_P(context)); -} - -} // namespace - -void pt_class_reflection_access_rinit() -{ - pt_cr_slots.ce = NULL; - pt_ms_slots.ce = NULL; - pt_ms_inherited_ce = NULL; -} - -/* {{{ ClassReflection */ - -/* return $this->name ??= $this->reflection->getName(); — the memo once it - * is a string, the method until then */ -zv::Val pt_class_reflection_get_name(zend_object *classReflection) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL) { - zval *name = OBJ_PROP(classReflection, slots->name); - if (EXPECTED(Z_TYPE_P(name) == IS_STRING)) return zv::Val::string(Z_STR_P(name)); - } else if (UNEXPECTED(error)) { - return zv::Val(); - } - return pt_type_call(classReflection, PT_LC("getname"), 0, NULL); -} - -/* $cacheKey = $this->cacheKey; if ($cacheKey !== null) return $this->cacheKey; */ -zv::Val pt_class_reflection_get_cache_key(zend_object *classReflection) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL) { - zval *cacheKey = OBJ_PROP(classReflection, slots->cacheKey); - if (EXPECTED(Z_TYPE_P(cacheKey) == IS_STRING)) return zv::Val::string(Z_STR_P(cacheKey)); - } else if (UNEXPECTED(error)) { - return zv::Val(); - } - return pt_type_call(classReflection, PT_LC("getcachekey"), 0, NULL); -} - -/* return $this->reflection; — the constructor's argument, an object once - * initialized (the method's Error before that) */ -zv::Val pt_class_reflection_get_native_reflection(zend_object *classReflection) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL) { - zval *reflection = OBJ_PROP(classReflection, slots->reflection); - if (EXPECTED(Z_TYPE_P(reflection) == IS_OBJECT)) return zv::Val::copyOf(zv::Ref(reflection)); - } else if (UNEXPECTED(error)) { - return zv::Val(); - } - return pt_type_call(classReflection, PT_LC("getnativereflection"), 0, NULL); -} - -/* if ($this->isGeneric === null) { ... } return $this->isGeneric; — the - * memo once it is a bool */ -bool pt_class_reflection_is_generic(zend_object *classReflection, bool &out) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL) { - zval *isGeneric = OBJ_PROP(classReflection, slots->isGeneric); - if (EXPECTED(Z_TYPE_P(isGeneric) == IS_TRUE || Z_TYPE_P(isGeneric) == IS_FALSE)) { - out = Z_TYPE_P(isGeneric) == IS_TRUE; - return true; - } - } else if (UNEXPECTED(error)) { - return false; - } - return callBool(classReflection, PT_LC("isgeneric"), 0, NULL, out); -} - -/* if (array_key_exists($methodName, $this->hasMethodCache)) return - * $this->hasMethodCache[$methodName]; — the cached bool (a string offset, - * so a numeric string is an integer key: the symtable lookup); the method - * computes and caches otherwise */ -bool pt_class_reflection_has_method(zend_object *classReflection, zval *methodName, bool &out) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL && Z_TYPE_P(methodName) == IS_STRING) { - zval *cache = OBJ_PROP(classReflection, slots->hasMethodCache); - if (EXPECTED(Z_TYPE_P(cache) == IS_ARRAY)) { - zval *cached = zend_symtable_find(Z_ARRVAL_P(cache), Z_STR_P(methodName)); - if (cached != NULL && (Z_TYPE_P(cached) == IS_TRUE || Z_TYPE_P(cached) == IS_FALSE)) { - out = Z_TYPE_P(cached) == IS_TRUE; - return true; - } - } - } else if (UNEXPECTED(error)) { - return false; - } - return callBool(classReflection, PT_LC("hasmethod"), 1, methodName, out); -} - -/* return $this->finalByKeywordOverride !== null; — a pure read of the - * promoted ?bool (the method's Error while uninitialized) */ -bool pt_class_reflection_has_final_by_keyword_override(zend_object *classReflection, bool &out) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL) { - zval *override = OBJ_PROP(classReflection, slots->finalByKeywordOverride); - if (Z_TYPE_P(override) == IS_NULL) { - out = false; - return true; - } - if (EXPECTED(Z_TYPE_P(override) == IS_TRUE || Z_TYPE_P(override) == IS_FALSE)) { - out = true; - return true; - } - } else if (UNEXPECTED(error)) { - return false; - } - return callBool(classReflection, PT_LC("hasfinalbykeywordoverride"), 0, NULL, out); -} - -/* return $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum(); - * — false without a call when the reflection is not a ReflectionEnum - * (`instanceof` sees an undeclared class as "no instance of it", hence the - * no-autoload lookup); the method decides otherwise */ -bool pt_class_reflection_is_enum(zend_object *classReflection, bool &out) -{ - bool error; - const ClassReflectionSlots *slots = classReflectionSlots(classReflection, error); - if (slots != NULL) { - zval *reflection = OBJ_PROP(classReflection, slots->reflection); - if (EXPECTED(Z_TYPE_P(reflection) == IS_OBJECT)) { - zend_class_entry *enumCe = pt_class_loaded(PT_CLASS_REFLECTION_ENUM); - if (enumCe == NULL) { - if (UNEXPECTED(EG(exception))) return false; - out = false; - return true; - } - if (!instanceof_function(Z_OBJCE_P(reflection), enumCe)) { - out = false; - return true; - } - } - } else if (UNEXPECTED(error)) { - return false; - } - return callBool(classReflection, PT_LC("isenum"), 0, NULL, out); -} - -/* }}} */ - -/* {{{ MutatingScope */ - -/* return $this->context->getClassReflection() !== null; */ -bool pt_scope_is_in_class(zend_object *scope, bool &out) -{ - bool error; - zval *classReflection = scopeClassReflectionSlot(scope, error); - if (classReflection != NULL) { - if (Z_TYPE_P(classReflection) == IS_NULL) { - out = false; - return true; - } - if (EXPECTED(Z_TYPE_P(classReflection) == IS_OBJECT)) { - out = true; - return true; - } - } else if (UNEXPECTED(error)) { - return false; - } - return callBool(scope, PT_LC("isinclass"), 0, NULL, out); -} - -/* return $this->context->getClassReflection(); */ -zv::Val pt_scope_get_class_reflection(zend_object *scope) -{ - bool error; - zval *classReflection = scopeClassReflectionSlot(scope, error); - if (classReflection != NULL) { - if (Z_TYPE_P(classReflection) == IS_NULL) return zv::Val::null(); - if (EXPECTED(Z_TYPE_P(classReflection) == IS_OBJECT)) return zv::Val::copyOf(zv::Ref(classReflection)); - } else if (UNEXPECTED(error)) { - return zv::Val(); - } - return pt_type_call(scope, PT_LC("getclassreflection"), 0, NULL); -} - -/* }}} */ diff --git a/turbo-ext/src/ExpressionResult.cpp b/turbo-ext/src/ExpressionResult.cpp new file mode 100644 index 00000000000..123bc918117 --- /dev/null +++ b/turbo-ext/src/ExpressionResult.cpp @@ -0,0 +1,1432 @@ +/* + * PHPStanTurbo\ExpressionResult — native implementation of + * PHPStan\Analyser\ExpressionResult. + * + * The state lives in the twin's property slots, declared here in the twin's + * declaration order (the explicit properties, then the promoted constructor + * parameters); the constructor keeps the twin's exact arginfo — the DI + * container reflects it to generate ExpressionResultFactory (parameter + * names, types and defaults are what Nette pairs the factory's create() + * parameters with, and #[AutowiredExtensions] requires the native + * ExtensionsCollection type on the first parameter). + * + * The collaborators that stay PHP (MutatingScope, the extensions collection + * and its extensions, the type/specify/create callbacks, the issetability + * descriptor) are called through the engine; the Type queries go through + * the Type ops, late-resolvable resolution and the void->null traverse + * through the native TypeUtils and TypeTraverser. + */ + +#include "support.h" +#include "generated/ExpressionResult.h" + +namespace slots = ptdecl::ExpressionResult::slot; +namespace sigs = ptdecl::ExpressionResult::sig; +#include "zv.h" +#include "TypeTraits.h" +#include "TypeOps.h" + +#include + +zend_class_entry *pt_ce_expression_result; + +/* the property slots, in declaration order */ +#define PT_ER_PROP_COUNT 30 + +namespace { + +/* the twin's READ_VARIABLE_NAMES_ATTRIBUTE, a permanent interned string */ +zend_string *pt_er_read_variable_names_attribute = nullptr; + +/* the constructor's values: NULL stands for a null argument, the bools are + * plain, the arrays and objects borrowed */ +struct ConstructorArgs +{ + zval *expressionTypeResolverExtensions; + zval *defaultNarrowingHelper; + zval *scope; + zval *beforeScope; + zval *expr; + bool hasYield; + bool isAlwaysTerminating; + zval *throwPoints; + zval *impurePoints; + zval *typeCallback; + zval *specifyTypesCallback; + bool containsNullsafe = false; + zval *issetabilityDescriptor = NULL; + zval *truthyScopeOverrideResult = NULL; + zval *falseyScopeOverrideResult = NULL; + zval *createTypesCallback = NULL; + zval *type = NULL; + zval *nativeType = NULL; + zval *argsResult = NULL; + zval *variableFlow = NULL; + zval *specifiedTypes = NULL; /* NULL = [] */ + zval *cachedType = NULL; + bool extensionsDeclined = false; + zval *cachedNativeType = NULL; + zval *resolvedType = NULL; + zval *resolvedNativeType = NULL; + zval *projectedType = NULL; + zval *projectedNativeType = NULL; + zval *readVariableNames = NULL; +}; + +/* a nullable slot value as a constructor argument: NULL for PHP null */ +zval *argOf(zval *slot) +{ + return Z_TYPE_P(slot) == IS_NULL ? NULL : slot; +} + +void writeSlot(zend_object *object, uint32_t slot, zval *value) +{ + zval *p = OBJ_PROP_NUM(object, slot); + if (value != NULL) { + ZVAL_COPY(p, value); + } else { + ZVAL_NULL(p); + } +} + +void writeBoolSlot(zend_object *object, uint32_t slot, bool value) +{ + ZVAL_BOOL(OBJ_PROP_NUM(object, slot), value); +} + +/* new ShouldNotHappenException($message) */ +void throwShouldNotHappen(const char *message) +{ + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (ce == NULL) return; + zend_throw_exception(ce, message, 0); +} + +/* $scope->nativeTypesPromoted (a public property of MutatingScope); false = + * pending exception */ +bool scopeNativeTypesPromoted(zval *scope, bool &out) +{ + zval rv; + zval *value = zend_read_property(Z_OBJCE_P(scope), Z_OBJ_P(scope), PT_LC("nativeTypesPromoted"), 0, &rv); + if (UNEXPECTED(EG(exception))) return false; + out = value != NULL && zend_is_true(value); + if (value == &rv) { + zval_ptr_dtor(&rv); + } + return true; +} + +/* $scope->doNotTreatPhpDocTypesAsCertain(); UNDEF = pending exception */ +zv::Val scopeNativeView(zval *scope) +{ + return pt_type_call(Z_OBJ_P(scope), PT_LC("donottreatphpdoctypesascertain"), 0, NULL); +} + +/* $scope->hasExpressionType($expr)->yes() / $scope->hasVariableType($name)->no(): + * the PT_TRI_* value, -1 = pending exception */ +[[nodiscard]] zend_long scopeTrinary(zval *scope, const char *lcname, size_t len, zval *argument) +{ + return pt_type_call_trinary(Z_OBJ_P(scope), lcname, len, 1, argument); +} + +/* TypeUtils::resolveLateResolvableTypes($type); UNDEF = pending exception */ +zv::Val resolveLateResolvableTypes(zval *type) +{ + return pt_type_call_static_ce(pt_ce_type_utils, PT_LC("resolvelateresolvabletypes"), 1, type); +} + +} // namespace + +namespace phpstanturbo { + +/* + * Mirrors PHPStan\Analyser\ExpressionResult. The handle wraps one object; + * methods returning zv::Val use UNDEF for a pending exception (a legitimate + * PHP null is zv::Val::null()), methods returning bool report a pending + * exception with false where noted. + */ +class ExpressionResult +{ +public: + explicit ExpressionResult(zend_object *self) : self(self) {} + + /* Mirrors __construct(): the twin's invariants, then the slots. false = + * pending exception */ + static bool construct(zend_object *object, const ConstructorArgs &a) + { + // A precomputed type and a lazy typeCallback are mutually exclusive, but + // one must be set unless both callback results have already been memoized. + // PHPDoc and native types are precomputed together or not at all. + if (a.typeCallback != NULL && a.type != NULL) { + throwShouldNotHappen("ExpressionResult cannot have both a typeCallback and a precomputed type."); + return false; + } + if (a.typeCallback == NULL && a.type == NULL && (a.resolvedType == NULL || a.resolvedNativeType == NULL)) { + throwShouldNotHappen("ExpressionResult must have precomputed types, a typeCallback, or both resolved types."); + return false; + } + if ((a.type == NULL) != (a.nativeType == NULL)) { + throwShouldNotHappen("ExpressionResult type and nativeType must both be set or both be null."); + return false; + } + + writeSlot(object, slots::expressionTypeResolverExtensions, a.expressionTypeResolverExtensions); + writeSlot(object, slots::defaultNarrowingHelper, a.defaultNarrowingHelper); + writeSlot(object, slots::scope, a.scope); + writeSlot(object, slots::beforeScope, a.beforeScope); + writeSlot(object, slots::expr, a.expr); + writeBoolSlot(object, slots::hasYield, a.hasYield); + writeBoolSlot(object, slots::isAlwaysTerminating, a.isAlwaysTerminating); + writeSlot(object, slots::throwPoints, a.throwPoints); + writeSlot(object, slots::impurePoints, a.impurePoints); + writeBoolSlot(object, slots::containsNullsafe, a.containsNullsafe); + writeSlot(object, slots::issetabilityDescriptor, a.issetabilityDescriptor); + writeSlot(object, slots::truthyScopeOverrideResult, a.truthyScopeOverrideResult); + writeSlot(object, slots::falseyScopeOverrideResult, a.falseyScopeOverrideResult); + writeSlot(object, slots::type, a.type); + writeSlot(object, slots::nativeType, a.nativeType); + writeSlot(object, slots::argsResult, a.argsResult); + writeSlot(object, slots::variableFlow, a.variableFlow); + if (a.specifiedTypes != NULL) { + writeSlot(object, slots::specifiedTypes, a.specifiedTypes); + } else { + ZVAL_EMPTY_ARRAY(OBJ_PROP_NUM(object, slots::specifiedTypes)); + } + writeSlot(object, slots::cachedType, a.cachedType); + writeSlot(object, slots::cachedNativeType, a.cachedNativeType); + writeSlot(object, slots::resolvedType, a.resolvedType); + writeSlot(object, slots::resolvedNativeType, a.resolvedNativeType); + writeSlot(object, slots::projectedType, a.projectedType); + writeSlot(object, slots::projectedNativeType, a.projectedNativeType); + writeSlot(object, slots::readVariableNames, a.readVariableNames); + + writeSlot(object, slots::typeCallback, a.typeCallback); + writeSlot(object, slots::specifyTypesCallback, a.specifyTypesCallback); + writeSlot(object, slots::createTypesCallback, a.createTypesCallback); + writeBoolSlot(object, slots::extensionsDeclined, a.extensionsDeclined); + return true; + } + + /* Mirrors finalize(); $variableFlow NULL for null. */ + zv::Val finalize(zval *scope, bool hasYield, bool isAlwaysTerminating, zval *throwPoints, zval *impurePoints, zval *variableFlow) const + { + ConstructorArgs a; + a.expressionTypeResolverExtensions = slot(slots::expressionTypeResolverExtensions); + a.defaultNarrowingHelper = slot(slots::defaultNarrowingHelper); + a.scope = scope; + a.beforeScope = slot(slots::beforeScope); + a.expr = slot(slots::expr); + a.hasYield = hasYield; + a.isAlwaysTerminating = isAlwaysTerminating; + a.throwPoints = throwPoints; + a.impurePoints = impurePoints; + a.typeCallback = argOf(slot(slots::typeCallback)); + a.specifyTypesCallback = slot(slots::specifyTypesCallback); + a.containsNullsafe = boolSlot(slots::containsNullsafe); + a.issetabilityDescriptor = argOf(slot(slots::issetabilityDescriptor)); + a.truthyScopeOverrideResult = argOf(slot(slots::truthyScopeOverrideResult)); + a.falseyScopeOverrideResult = argOf(slot(slots::falseyScopeOverrideResult)); + a.createTypesCallback = argOf(slot(slots::createTypesCallback)); + a.type = argOf(slot(slots::type)); + a.nativeType = argOf(slot(slots::nativeType)); + a.argsResult = argOf(slot(slots::argsResult)); + a.variableFlow = variableFlow; + a.specifiedTypes = slot(slots::specifiedTypes); + a.cachedType = argOf(slot(slots::cachedType)); + a.extensionsDeclined = boolSlot(slots::extensionsDeclined); + a.cachedNativeType = argOf(slot(slots::cachedNativeType)); + a.resolvedType = argOf(slot(slots::resolvedType)); + a.resolvedNativeType = argOf(slot(slots::resolvedNativeType)); + a.projectedType = argOf(slot(slots::projectedType)); + a.projectedNativeType = argOf(slot(slots::projectedNativeType)); + a.readVariableNames = argOf(slot(slots::readVariableNames)); + return newSelf(a); + } + + zv::Val getScope() const { return copySlot(slots::scope); } + zv::Val getVariableFlow() const { return copySlot(slots::variableFlow); } + + /* Mirrors withScope(). */ + zv::Val withScope(zval *scope) const + { + if (Z_OBJ_P(scope) == Z_OBJ_P(slot(slots::scope))) { + zval selfValue; + ZVAL_OBJ(&selfValue, self); + return zv::Val::copyOf(zv::Ref(&selfValue)); + } + + return finalize(scope, boolSlot(slots::hasYield), boolSlot(slots::isAlwaysTerminating), slot(slots::throwPoints), slot(slots::impurePoints), argOf(slot(slots::variableFlow))); + } + + zv::Val getBeforeScope() const { return copySlot(slots::beforeScope); } + zv::Val getExpr() const { return copySlot(slots::expr); } + zv::Val getArgsResult() const { return copySlot(slots::argsResult); } + bool hasYield() const { return boolSlot(slots::hasYield); } + bool containsNullsafe() const { return boolSlot(slots::containsNullsafe); } + + /* Mirrors getIssetabilityResolution(). */ + zv::Val getIssetabilityResolution(zval *scope, bool useNativeTypes, bool reprocessUntrackedLinks) + { + zval *descriptor = slot(slots::issetabilityDescriptor); + zval *expr = slot(slots::expr); + if (Z_TYPE_P(descriptor) == IS_OBJECT) { + zv::Args argv{scope, useNativeTypes, expr, reprocessUntrackedLinks}; + return pt_type_call(Z_OBJ_P(descriptor), PT_LC("resolve"), 4, argv); + } + + zv::Val type; + bool tracked = true; + if (reprocessUntrackedLinks) { + zend_long has = scopeTrinary(scope, PT_LC("hasexpressiontype"), expr); + if (UNEXPECTED(has < 0)) return zv::Val(); + tracked = has == PT_TRI_YES; + } + if (reprocessUntrackedLinks && !tracked) { + if (useNativeTypes) { + zv::Val nativeScope = scopeNativeView(scope); + if (UNEXPECTED(nativeScope.isUndef())) return zv::Val(); + type = pt_type_call(Z_OBJ_P(nativeScope.raw()), PT_LC("getnativetype"), 1, expr); + } else { + type = pt_type_call(Z_OBJ_P(scope), PT_LC("gettype"), 1, expr); + } + } else { + type = getTypeOnScope(scope, useNativeTypes); + } + if (UNEXPECTED(type.isUndef())) return zv::Val(); + + zend_class_entry *nullsafePropertyFetchCe = pt_class(PT_CLASS_NULLSAFE_PROPERTY_FETCH); + if (UNEXPECTED(nullsafePropertyFetchCe == NULL)) return zv::Val(); + zv::Args leafArgv{type.raw(), expr, bool(instanceof_function(Z_OBJCE_P(expr), nullsafePropertyFetchCe))}; + zv::Val link = pt_type_call_static(PT_CLASS_ISSETABILITY_LINK_INFO, PT_LC("leaf"), 3, leafArgv); + if (UNEXPECTED(link.isUndef())) return zv::Val(); + zv::Args resolutionArgv{link.raw(), zv::null}; + return pt_type_new(PT_CLASS_ISSETABILITY_RESOLUTION, 2, resolutionArgv); + } + + zv::Val getThrowPoints() const { return copySlot(slots::throwPoints); } + zv::Val getImpurePoints() const { return copySlot(slots::impurePoints); } + + /* Mirrors getTruthyScope(). */ + zv::Val getTruthyScope() + { + return branchScope(slots::truthyScope, slots::truthyScopeOverrideResult, PT_LC("gettruthyscope"), PT_LC("createtruthy")); + } + + /* Mirrors getFalseyScope(). */ + zv::Val getFalseyScope() + { + return branchScope(slots::falseyScope, slots::falseyScopeOverrideResult, PT_LC("getfalseyscope"), PT_LC("createfalsey")); + } + + bool isAlwaysTerminating() const { return boolSlot(slots::isAlwaysTerminating); } + + /* Mirrors getType(). */ + zv::Val getType() + { + zval *cached = slot(slots::cachedType); + if (Z_TYPE_P(cached) == IS_OBJECT) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val extensionType = consultExpressionTypeResolverExtensions(slot(slots::beforeScope)); + if (UNEXPECTED(extensionType.isUndef())) return zv::Val(); + if (Z_TYPE_P(extensionType.raw()) == IS_OBJECT) return memoize(slots::cachedType, std::move(extensionType)); + + zval *type = slot(slots::type); + if (Z_TYPE_P(type) == IS_OBJECT) return zv::Val::copyOf(zv::Ref(type)); + + if (hasOwnLazyResolution()) { + zend_long tracked = hasTrackedExpressionType(slot(slots::beforeScope)); + if (UNEXPECTED(tracked < 0)) return zv::Val(); + if (tracked == 0) return memoize(slots::cachedType, resolveOwnType(false)); + } + + // The guard above leaves only one way here: the expression is tracked on + // beforeScope (typeCallback is set but a holder wins). Read the holder + // directly instead of re-entering MutatingScope::getType() - resolving + // its late-resolvable types the way that method would have. + zv::Val trackedType = pt_type_call(Z_OBJ_P(slot(slots::beforeScope)), PT_LC("gettrackedexpressiontype"), 1, slot(slots::expr)); + if (UNEXPECTED(trackedType.isUndef())) return zv::Val(); + return memoize(slots::cachedType, resolveLateResolvableTypes(trackedType.raw())); + } + + /* Mirrors getNativeType(). */ + zv::Val getNativeType() + { + zval *cached = slot(slots::cachedNativeType); + if (Z_TYPE_P(cached) == IS_OBJECT) return zv::Val::copyOf(zv::Ref(cached)); + + // old-world getNativeType() promoted the scope and re-entered + // resolveType(), extension hook included + zv::Val nativeScope; + if (!boolSlot(slots::extensionsDeclined)) { + nativeScope = scopeNativeView(slot(slots::beforeScope)); + if (UNEXPECTED(nativeScope.isUndef())) return zv::Val(); + zv::Val extensionType = consultExpressionTypeResolverExtensions(nativeScope.raw()); + if (UNEXPECTED(extensionType.isUndef())) return zv::Val(); + if (Z_TYPE_P(extensionType.raw()) == IS_OBJECT) return memoize(slots::cachedNativeType, std::move(extensionType)); + } + + zval *nativeType = slot(slots::nativeType); + if (Z_TYPE_P(nativeType) == IS_OBJECT) return zv::Val::copyOf(zv::Ref(nativeType)); + + if (nativeScope.isUndef()) { + nativeScope = scopeNativeView(slot(slots::beforeScope)); + if (UNEXPECTED(nativeScope.isUndef())) return zv::Val(); + } + if (hasOwnLazyResolution()) { + zend_long tracked = hasTrackedExpressionType(nativeScope.raw()); + if (UNEXPECTED(tracked < 0)) return zv::Val(); + if (tracked == 0) return memoize(slots::cachedNativeType, resolveOwnType(true)); + } + + // Tracked native holder (getNativeType() promotes the scope, so its + // expressionTypes are the native ones) - read it directly, resolving its + // late-resolvable types the way MutatingScope::getType() would have. + zv::Val trackedType = pt_type_call(Z_OBJ_P(nativeScope.raw()), PT_LC("gettrackedexpressiontype"), 1, slot(slots::expr)); + if (UNEXPECTED(trackedType.isUndef())) return zv::Val(); + return memoize(slots::cachedNativeType, resolveLateResolvableTypes(trackedType.raw())); + } + + /* Mirrors getKeepVoidType(). */ + zv::Val getKeepVoidType(bool nativeTypesPromoted) + { + zv::Val rawType = resolveOwnRawType(nativeTypesPromoted); + if (UNEXPECTED(rawType.isUndef())) return zv::Val(); + zend_long isVoid = trinaryOp(rawType.raw(), PT_OP_IS_VOID); + if (UNEXPECTED(isVoid < 0)) return zv::Val(); + if (isVoid != PT_TRI_NO) { + // there is void to keep - the raw type is the answer, and no read that + // projects it away may run + return rawType; + } + + // nothing to keep, so this is an ordinary value read: it must honour a + // holder tracked for the expression (a match arm body narrowed by its own + // condition) and the extensions, exactly like getType() does + return nativeTypesPromoted ? getNativeType() : getType(); + } + + /* Mirrors canResolveOwnType(). */ + bool canResolveOwnType() const + { + return Z_TYPE_P(slot(slots::type)) == IS_OBJECT || hasOwnLazyResolution(); + } + + /* Mirrors getSpecifiedTypesForScope(). */ + zv::Val getSpecifiedTypesForScope(zval *scope, zval *context) + { + bool nativeTypesPromoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(scope, nativeTypesPromoted))) return zv::Val(); + return getSpecifiedTypes(context, nativeTypesPromoted); + } + + /* Mirrors getSpecifiedTypes(). */ + zv::Val getSpecifiedTypes(zval *context, bool nativeTypesPromoted) + { + zend_ulong key = ((zend_ulong) Z_OBJ_HANDLE_P(context) << 1) | (nativeTypesPromoted ? 1 : 0); + zval *memo = slot(slots::specifiedTypes); + zval *found = Z_TYPE_P(memo) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(memo), key) : NULL; + if (found != NULL && Z_TYPE_P(found) != IS_NULL) return zv::Val::copyOf(zv::Ref(found)); + + zv::Args argv{context, nativeTypesPromoted}; + zv::Val specified = pt_type_call_callable(slot(slots::specifyTypesCallback), 2, argv); + if (UNEXPECTED(specified.isUndef())) return zv::Val(); + /* $this->specifiedTypes[$key] ??= ... */ + memo = slot(slots::specifiedTypes); + if (Z_TYPE_P(memo) != IS_ARRAY) { + ZVAL_EMPTY_ARRAY(memo); + } + SEPARATE_ARRAY(memo); + Z_TRY_ADDREF_P(specified.raw()); + zend_hash_index_update(Z_ARRVAL_P(memo), key, specified.raw()); + return specified; + } + + /* Mirrors getCreatedTypesForScope(). */ + zv::Val getCreatedTypesForScope(zval *scope, zval *type, zval *context) + { + bool nativeTypesPromoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(scope, nativeTypesPromoted))) return zv::Val(); + return getCreatedTypes(type, context, nativeTypesPromoted); + } + + /* Mirrors getCreatedTypes(). */ + zv::Val getCreatedTypes(zval *type, zval *context, bool nativeTypesPromoted) + { + zval *callback = slot(slots::createTypesCallback); + if (Z_TYPE_P(callback) == IS_NULL) return zv::Val::null(); + + zv::Args argv{type, context, nativeTypesPromoted}; + return pt_type_call_callable(callback, 3, argv); + } + + /* Mirrors getTypeOnScope(). */ + zv::Val getTypeOnScope(zval *scope, bool useNativeTypes) + { + // An already-promoted asking scope selects native types on its own - a + // caller that promotes the scope instead of passing the flag (the + // isset/empty/?? folds) must not fall through to the phpdoc flavour of + // the result's own type. + bool promoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(scope, promoted))) return zv::Val(); + useNativeTypes = useNativeTypes || promoted; + zv::Val readScopeHolder; + zval *readScope = scope; + if (useNativeTypes) { + readScopeHolder = scopeNativeView(scope); + if (UNEXPECTED(readScopeHolder.isUndef())) return zv::Val(); + readScope = readScopeHolder.raw(); + } + // old-world resolveType() consulted these on every ask, both flavours - + // a consumer reading a call's type here (an assign filling the target's + // holder) must see the override or it never enters the scope state + zv::Val extensionType = consultExpressionTypeResolverExtensions(readScope); + if (UNEXPECTED(extensionType.isUndef())) return zv::Val(); + if (Z_TYPE_P(extensionType.raw()) == IS_OBJECT) return extensionType; + + if (Z_TYPE_P(slot(slots::type)) == IS_NULL) { + zend_long authoritative = isScopeAuthoritative(readScope); + if (UNEXPECTED(authoritative < 0)) return zv::Val(); + if (authoritative == 1) { + // the state read is a value read: resolve late-resolvable types and + // project void to null exactly like resolveOwnType() does + zv::Val stateType = pt_type_call(Z_OBJ_P(readScope), PT_LC("getstatetype"), 1, slot(slots::expr)); + if (UNEXPECTED(stateType.isUndef())) return zv::Val(); + zv::Val resolved = resolveLateResolvableTypes(stateType.raw()); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + return projectVoidToNull(std::move(resolved), useNativeTypes); + } + } + + return resolveOwnType(useNativeTypes); + } + + /* Mirrors answersOnScope(); -1 = pending exception, else 0/1. */ + [[nodiscard]] zend_long answersOnScope(zval *scope, bool useNativeTypes) + { + if (Z_TYPE_P(slot(slots::type)) == IS_OBJECT) return 1; + + bool promoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(scope, promoted))) return -1; + useNativeTypes = useNativeTypes || promoted; + zv::Val readScopeHolder; + zval *readScope = scope; + if (useNativeTypes) { + readScopeHolder = scopeNativeView(scope); + if (UNEXPECTED(readScopeHolder.isUndef())) return -1; + readScope = readScopeHolder.raw(); + } + + zend_long authoritative = isScopeAuthoritative(readScope); + if (authoritative != 0) return authoritative; + return askScopeVariableStateMatches(scope, useNativeTypes, false); + } + + /* Mirrors askScopeVariableStateMatches(); -1 = pending exception, else 0/1. */ + [[nodiscard]] zend_long askScopeVariableStateMatches(zval *scope, bool useNativeTypes, bool ruleFacingAsk) + { + zval *beforeScope = slot(slots::beforeScope); + // same unpromoted position implies same promoted position - skip the + // flavour derivation for the common same-position ask + if (Z_OBJ_P(scope) == Z_OBJ_P(beforeScope)) return 1; + // a closure's stored result IS its (by-ref converged) walk; re-walking + // it at a foreign position would re-run the whole convergence loop. Its + // body variables are not reads of the asking position, and the + // position-sensitive TYPE is computed by getClosureType at ask sites. + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + zend_class_entry *arrowFunctionCe = pt_class(PT_CLASS_ARROW_FUNCTION); + if (UNEXPECTED(closureCe == NULL || arrowFunctionCe == NULL)) return -1; + zend_object *expr = Z_OBJ_P(slot(slots::expr)); + if (instanceof_function(expr->ce, closureCe) || instanceof_function(expr->ce, arrowFunctionCe)) return 1; + zv::Val names = getReadVariableNames(); + if (UNEXPECTED(names.isUndef())) return -1; + if (zend_hash_num_elements(Z_ARRVAL_P(names.raw())) == 0) return 1; + + zv::Val readScopeHolder, positionScopeHolder; + zval *readScope = scope; + zval *positionScope = beforeScope; + if (useNativeTypes) { + readScopeHolder = scopeNativeView(scope); + if (UNEXPECTED(readScopeHolder.isUndef())) return -1; + readScope = readScopeHolder.raw(); + positionScopeHolder = scopeNativeView(beforeScope); + if (UNEXPECTED(positionScopeHolder.isUndef())) return -1; + positionScope = positionScopeHolder.raw(); + } + if (Z_OBJ_P(readScope) == Z_OBJ_P(positionScope)) return 1; + + for (auto entry : zv::TableRef(Z_ARRVAL_P(names.raw()))) { + zval *name = entry.value().raw(); + zend_long askKnows = scopeTrinary(readScope, PT_LC("hasvariabletype"), name); + if (UNEXPECTED(askKnows < 0)) return -1; + zend_long positionKnows = scopeTrinary(positionScope, PT_LC("hasvariabletype"), name); + if (UNEXPECTED(positionKnows < 0)) return -1; + if (ruleFacingAsk) { + if (askKnows == PT_TRI_NO) continue; + if (positionKnows == PT_TRI_NO) return 0; + zv::Val askType = pt_type_call(Z_OBJ_P(readScope), PT_LC("getvariabletype"), 1, name); + if (UNEXPECTED(askType.isUndef())) return -1; + zv::Val positionType = pt_type_call(Z_OBJ_P(positionScope), PT_LC("getvariabletype"), 1, name); + if (UNEXPECTED(positionType.isUndef())) return -1; + // identity and equality short-circuit the O(keys^2) constant-array + // isSuperTypeOf() - unchanged variables are the common ask case + if (pt_types_identical_or_equal(askType.raw(), positionType.raw())) continue; + if (UNEXPECTED(EG(exception))) return -1; + zend_long superType = isSuperTypeOf(askType.raw(), positionType.raw()); + if (UNEXPECTED(superType < 0)) return -1; + if (superType == PT_TRI_YES) continue; + + return 0; + } + if (askKnows == PT_TRI_NO && positionKnows == PT_TRI_NO) continue; + if (askKnows != positionKnows) return 0; + zv::Val askType = pt_type_call(Z_OBJ_P(readScope), PT_LC("getvariabletype"), 1, name); + if (UNEXPECTED(askType.isUndef())) return -1; + zv::Val positionType = pt_type_call(Z_OBJ_P(positionScope), PT_LC("getvariabletype"), 1, name); + if (UNEXPECTED(positionType.isUndef())) return -1; + if (!pt_types_identical_or_equal(askType.raw(), positionType.raw())) { + if (UNEXPECTED(EG(exception))) return -1; + return 0; + } + } + + return 1; + } + + /* Mirrors atAskPosition(). */ + zv::Val atAskPosition(zval *scope) + { + // Scope-authoritative types must come from the asking position: the + // original callback captures the original scope. + bool fromScope = false; + if (Z_TYPE_P(slot(slots::type)) == IS_NULL) { + zend_long authoritative = isScopeAuthoritative(scope); + if (UNEXPECTED(authoritative < 0)) return zv::Val(); + fromScope = authoritative == 1; + } + + zv::Val stateType, nativeStateType; + if (fromScope) { + stateType = pt_type_call(Z_OBJ_P(scope), PT_LC("getstatetype"), 1, slot(slots::expr)); + if (UNEXPECTED(stateType.isUndef())) return zv::Val(); + zv::Val nativeScope = scopeNativeView(scope); + if (UNEXPECTED(nativeScope.isUndef())) return zv::Val(); + nativeStateType = pt_type_call(Z_OBJ_P(nativeScope.raw()), PT_LC("getstatetype"), 1, slot(slots::expr)); + if (UNEXPECTED(nativeStateType.isUndef())) return zv::Val(); + } + + ConstructorArgs a; + a.expressionTypeResolverExtensions = slot(slots::expressionTypeResolverExtensions); + a.defaultNarrowingHelper = slot(slots::defaultNarrowingHelper); + a.scope = scope; + a.beforeScope = scope; + a.expr = slot(slots::expr); + a.hasYield = boolSlot(slots::hasYield); + a.isAlwaysTerminating = boolSlot(slots::isAlwaysTerminating); + a.throwPoints = slot(slots::throwPoints); + a.impurePoints = slot(slots::impurePoints); + a.typeCallback = fromScope ? NULL : argOf(slot(slots::typeCallback)); + a.specifyTypesCallback = slot(slots::specifyTypesCallback); + a.containsNullsafe = boolSlot(slots::containsNullsafe); + a.issetabilityDescriptor = argOf(slot(slots::issetabilityDescriptor)); + a.createTypesCallback = argOf(slot(slots::createTypesCallback)); + a.type = fromScope ? stateType.raw() : argOf(slot(slots::type)); + a.nativeType = fromScope ? nativeStateType.raw() : argOf(slot(slots::nativeType)); + a.argsResult = argOf(slot(slots::argsResult)); + a.variableFlow = argOf(slot(slots::variableFlow)); + a.specifiedTypes = slot(slots::specifiedTypes); + a.extensionsDeclined = boolSlot(slots::extensionsDeclined); + a.resolvedType = fromScope ? NULL : argOf(slot(slots::resolvedType)); + a.resolvedNativeType = fromScope ? NULL : argOf(slot(slots::resolvedNativeType)); + a.projectedType = fromScope ? NULL : argOf(slot(slots::projectedType)); + a.projectedNativeType = fromScope ? NULL : argOf(slot(slots::projectedNativeType)); + a.readVariableNames = argOf(slot(slots::readVariableNames)); + return newSelf(a); + } + + /* Mirrors onNonNullabilityDevicedScopes(). */ + zv::Val onNonNullabilityDevicedScopes(zval *beforeScope, zval *scope) const + { + ConstructorArgs a; + a.expressionTypeResolverExtensions = slot(slots::expressionTypeResolverExtensions); + a.defaultNarrowingHelper = slot(slots::defaultNarrowingHelper); + a.scope = scope; + a.beforeScope = beforeScope; + a.expr = slot(slots::expr); + a.hasYield = boolSlot(slots::hasYield); + a.isAlwaysTerminating = boolSlot(slots::isAlwaysTerminating); + a.throwPoints = slot(slots::throwPoints); + a.impurePoints = slot(slots::impurePoints); + a.typeCallback = argOf(slot(slots::typeCallback)); + a.specifyTypesCallback = slot(slots::specifyTypesCallback); + a.containsNullsafe = boolSlot(slots::containsNullsafe); + a.issetabilityDescriptor = argOf(slot(slots::issetabilityDescriptor)); + a.truthyScopeOverrideResult = argOf(slot(slots::truthyScopeOverrideResult)); + a.falseyScopeOverrideResult = argOf(slot(slots::falseyScopeOverrideResult)); + a.createTypesCallback = argOf(slot(slots::createTypesCallback)); + a.type = argOf(slot(slots::type)); + a.nativeType = argOf(slot(slots::nativeType)); + a.argsResult = argOf(slot(slots::argsResult)); + a.variableFlow = argOf(slot(slots::variableFlow)); + a.specifiedTypes = slot(slots::specifiedTypes); + a.extensionsDeclined = boolSlot(slots::extensionsDeclined); + a.resolvedType = argOf(slot(slots::resolvedType)); + a.resolvedNativeType = argOf(slot(slots::resolvedNativeType)); + a.projectedType = argOf(slot(slots::projectedType)); + a.projectedNativeType = argOf(slot(slots::projectedNativeType)); + a.readVariableNames = argOf(slot(slots::readVariableNames)); + return newSelf(a); + } + +private: + zend_object *self; + + zval *slot(uint32_t index) const { return OBJ_PROP_NUM(self, index); } + bool boolSlot(uint32_t index) const { return Z_TYPE_P(slot(index)) == IS_TRUE; } + zv::Val copySlot(uint32_t index) const { return zv::Val::copyOf(zv::Ref(slot(index))); } + + /* $this->x = $value; returns a copy of it (the `return $this->x = ...` idiom) */ + zv::Val memoize(uint32_t index, zv::Val value) const + { + if (UNEXPECTED(value.isUndef())) return zv::Val(); + zv::Ref(slot(index)).assign(zv::Val::copyOf(value.ref())); + return value; + } + + /* new self(...) on the object's own (final) class */ + zv::Val newSelf(const ConstructorArgs &a) const + { + zval object; + if (UNEXPECTED(object_init_ex(&object, self->ce) != SUCCESS)) return zv::Val(); + if (UNEXPECTED(!construct(Z_OBJ(object), a))) { + zval_ptr_dtor(&object); + return zv::Val(); + } + return zv::Val::adopt(object); + } + + /* the shared body of getTruthyScope() / getFalseyScope() */ + zv::Val branchScope(uint32_t memoSlot, uint32_t overrideSlot, const char *getter, size_t getterLen, const char *contextFactory, size_t contextFactoryLen) + { + zval *memo = slot(memoSlot); + if (Z_TYPE_P(memo) == IS_OBJECT) return zv::Val::copyOf(zv::Ref(memo)); + + // see the twin: the override is held as the RESULT, derived on first use + zval *override = slot(overrideSlot); + if (Z_TYPE_P(override) == IS_OBJECT) { + zv::Val derived; + if (Z_OBJCE_P(override) == self->ce) { + derived = memoSlot == slots::truthyScope ? ExpressionResult(Z_OBJ_P(override)).getTruthyScope() : ExpressionResult(Z_OBJ_P(override)).getFalseyScope(); + } else { + derived = pt_type_call(Z_OBJ_P(override), getter, getterLen, 0, NULL); + } + return memoize(memoSlot, std::move(derived)); + } + + zval *scope = slot(slots::scope); + bool nativeTypesPromoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(scope, nativeTypesPromoted))) return zv::Val(); + zv::Val context = pt_type_call_static(PT_CLASS_TYPE_SPECIFIER_CONTEXT, contextFactory, contextFactoryLen, 0, NULL); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Val specified = getSpecifiedTypes(context.raw(), nativeTypesPromoted); + if (UNEXPECTED(specified.isUndef())) return zv::Val(); + specified = withEqualityCheckResult(std::move(specified), memoSlot == slots::truthyScope); + if (UNEXPECTED(specified.isUndef())) return zv::Val(); + return memoize(memoSlot, pt_type_call(Z_OBJ_P(scope), PT_LC("applyspecifiedtypes"), 1, specified.raw())); + } + + /* Mirrors withEqualityCheckResult() (private): an equality check narrows + * its operands without its outcome being determined by them, so the + * branch scope stores what the check itself returned - which is what + * makes a duplicate check in the branch report as always-true. */ + zv::Val withEqualityCheckResult(zv::Val specifiedTypes, bool value) + { + zv::Val isEquality = pt_type_call(Z_OBJ_P(specifiedTypes.raw()), PT_LC("isequality"), 0, NULL); + if (UNEXPECTED(isEquality.isUndef())) return zv::Val(); + if (!zend_is_true(isEquality.raw())) return specifiedTypes; + zv::Val type = getType(); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + zend_long isBoolean = pt_type_op_trinary(Z_OBJ_P(type.raw()), PT_OP_IS_BOOLEAN, 0, NULL); + if (UNEXPECTED(isBoolean < 0)) return zv::Val(); + if (isBoolean != PT_TRI_YES) return specifiedTypes; + + zval constantBoolean; + if (UNEXPECTED(!pt_constant_boolean_type_new(&constantBoolean, value))) return zv::Val(); + zv::Val booleanType = zv::Val::adopt(constantBoolean); + zv::Val context = pt_type_call_static(PT_CLASS_TYPE_SPECIFIER_CONTEXT, PT_LC("createtrue"), 0, NULL); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zval thisValue; + ZVAL_OBJ(&thisValue, self); + zval *helper = slot(slots::defaultNarrowingHelper); + if (UNEXPECTED(Z_TYPE_P(helper) != IS_OBJECT)) { + zend_throw_error(NULL, "Typed property PHPStan\\Analyser\\ExpressionResult::$defaultNarrowingHelper must not be accessed before initialization"); + return zv::Val(); + } + zval createArgs[5]; + ZVAL_COPY_VALUE(&createArgs[0], slot(slots::scope)); + ZVAL_COPY_VALUE(&createArgs[1], slot(slots::expr)); + ZVAL_COPY_VALUE(&createArgs[2], &thisValue); + ZVAL_COPY_VALUE(&createArgs[3], booleanType.raw()); + ZVAL_COPY_VALUE(&createArgs[4], context.raw()); + zv::Val subjectTypes = pt_type_call(Z_OBJ_P(helper), PT_LC("createsubjecttypes"), 5, createArgs); + if (UNEXPECTED(subjectTypes.isUndef())) return zv::Val(); + return pt_type_call(Z_OBJ_P(specifiedTypes.raw()), PT_LC("unionwith"), 1, subjectTypes.raw()); + } + + /* Mirrors consultExpressionTypeResolverExtensions(): the extension type, + * PHP null when every extension declined; UNDEF = pending exception. */ + zv::Val consultExpressionTypeResolverExtensions(zval *readScope) + { + if (boolSlot(slots::extensionsDeclined)) return zv::Val::null(); + + zv::Val extensions = pt_type_call(Z_OBJ_P(slot(slots::expressionTypeResolverExtensions)), PT_LC("getall"), 0, NULL); + if (UNEXPECTED(extensions.isUndef())) return zv::Val(); + if (Z_TYPE_P(extensions.raw()) == IS_ARRAY) { + zv::Args argv{slot(slots::expr), readScope}; + for (auto entry : zv::TableRef(Z_ARRVAL_P(extensions.raw()))) { + zv::Ref extension = entry.value().deref(); + if (UNEXPECTED(!extension.isObject())) continue; + zv::Val type = pt_type_call(extension.asObject(), PT_LC("gettype"), 2, argv); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (Z_TYPE_P(type.raw()) != IS_NULL) return type; + } + } + + writeBoolSlot(self, slots::extensionsDeclined, true); + + return zv::Val::null(); + } + + /* Mirrors resolveOwnRawType(). */ + zv::Val resolveOwnRawType(bool nativeTypesPromoted) + { + uint32_t eagerSlot = nativeTypesPromoted ? slots::nativeType : slots::type; + uint32_t resolvedSlot = nativeTypesPromoted ? slots::resolvedNativeType : slots::resolvedType; + if (Z_TYPE_P(slot(eagerSlot)) == IS_OBJECT) return copySlot(eagerSlot); + if (Z_TYPE_P(slot(resolvedSlot)) == IS_OBJECT) return copySlot(resolvedSlot); + zval *callback = slot(slots::typeCallback); + if (Z_TYPE_P(callback) == IS_NULL) { + pt_throw_should_not_happen(); + return zv::Val(); + } + + zv::Args argv{nativeTypesPromoted}; + zv::Val callbackType = pt_type_call_callable(callback, 1, argv); + if (UNEXPECTED(callbackType.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(callbackType.raw()) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: the ExpressionResult type callback did not return a Type"); + return zv::Val(); + } + zv::Val resolved = resolveLateResolvableTypes(callbackType.raw()); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + zv::Ref(slot(resolvedSlot)).assign(zv::Val::copyOf(resolved.ref())); + releaseTypeCallbackIfResolved(); + + return resolved; + } + + /* Mirrors releaseTypeCallbackIfResolved(). */ + void releaseTypeCallbackIfResolved() + { + if (Z_TYPE_P(slot(slots::resolvedType)) != IS_OBJECT || Z_TYPE_P(slot(slots::resolvedNativeType)) != IS_OBJECT) return; + + zv::Ref(slot(slots::typeCallback)).assign(zv::Val::null()); + } + + /* Mirrors resolveOwnType(). */ + zv::Val resolveOwnType(bool nativeTypesPromoted) + { + uint32_t projectedSlot = nativeTypesPromoted ? slots::projectedNativeType : slots::projectedType; + if (Z_TYPE_P(slot(projectedSlot)) == IS_OBJECT) return copySlot(projectedSlot); + zv::Val raw = resolveOwnRawType(nativeTypesPromoted); + if (UNEXPECTED(raw.isUndef())) return zv::Val(); + return memoize(projectedSlot, projectVoidToNull(std::move(raw), nativeTypesPromoted)); + } + + /* Mirrors projectVoidToNull(); $type consumed. */ + zv::Val projectVoidToNull(zv::Val type, bool nativeTypesPromoted) + { + // the overwhelmingly common non-void, non-union result skips the + // traverser entirely + if (!instanceof_function(Z_OBJCE_P(type.raw()), pt_ce_union_type)) { + zend_long isVoid = trinaryOp(type.raw(), PT_OP_IS_VOID); + if (UNEXPECTED(isVoid < 0)) return zv::Val(); + if (isVoid == PT_TRI_NO) return type; + } + + zend_long projects = projectsVoidToNull(nativeTypesPromoted); + if (UNEXPECTED(projects < 0)) return zv::Val(); + if (projects == 0) return type; + + zv::Val traverser = pt_type_new(PT_CLASS_VOID_TO_NULL_TRAVERSER, 0, NULL); + if (UNEXPECTED(traverser.isUndef())) return zv::Val(); + zval out; + if (UNEXPECTED(!pt_type_traverser_map(&out, type.raw(), traverser.raw()))) return zv::Val(); + return zv::Val::adopt(out); + } + + /* Mirrors projectsVoidToNull(); -1 = pending exception, else 0/1. */ + [[nodiscard]] zend_long projectsVoidToNull(bool nativeTypesPromoted) const + { + if (nativeTypesPromoted) return 0; + + zend_class_entry *funcCallCe = pt_class(PT_CLASS_FUNC_CALL); + zend_class_entry *nameCe = pt_class(PT_CLASS_NAME); + zend_class_entry *methodCallCe = pt_class(PT_CLASS_METHOD_CALL); + zend_class_entry *nullsafeMethodCallCe = pt_class(PT_CLASS_NULLSAFE_METHOD_CALL); + zend_class_entry *staticCallCe = pt_class(PT_CLASS_STATIC_CALL); + if (UNEXPECTED(funcCallCe == NULL || nameCe == NULL || methodCallCe == NULL || nullsafeMethodCallCe == NULL || staticCallCe == NULL)) return -1; + zend_object *expr = Z_OBJ_P(slot(slots::expr)); + if (instanceof_function(expr->ce, funcCallCe)) { + zv::Ref name = zv::ObjRef(expr).prop(PT_LC("name")); + if (name.raw() == NULL || !name.deref().instanceOf(nameCe)) return 0; + bool firstClassCallable; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, firstClassCallable))) return -1; + return firstClassCallable ? 0 : 1; + } + + if (!instanceof_function(expr->ce, methodCallCe) && !instanceof_function(expr->ce, nullsafeMethodCallCe) && !instanceof_function(expr->ce, staticCallCe)) { + return 0; + } + bool firstClassCallable; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, firstClassCallable))) return -1; + return firstClassCallable ? 0 : 1; + } + + /* Mirrors hasTrackedExpressionType(); -1 = pending exception, else 0/1. */ + [[nodiscard]] zend_long hasTrackedExpressionType(zval *scope) const + { + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + zend_class_entry *arrowFunctionCe = pt_class(PT_CLASS_ARROW_FUNCTION); + if (UNEXPECTED(variableCe == NULL || closureCe == NULL || arrowFunctionCe == NULL)) return -1; + zend_object *expr = Z_OBJ_P(slot(slots::expr)); + if (instanceof_function(expr->ce, variableCe) || instanceof_function(expr->ce, closureCe) || instanceof_function(expr->ce, arrowFunctionCe)) return 0; + zend_long has = scopeTrinary(scope, PT_LC("hasexpressiontype"), slot(slots::expr)); + if (UNEXPECTED(has < 0)) return -1; + return has == PT_TRI_YES ? 1 : 0; + } + + /* Mirrors hasOwnLazyResolution(). */ + bool hasOwnLazyResolution() const + { + return Z_TYPE_P(slot(slots::typeCallback)) != IS_NULL || Z_TYPE_P(slot(slots::resolvedType)) == IS_OBJECT; + } + + /* Mirrors isScopeAuthoritative(); -1 = pending exception, else 0/1. */ + [[nodiscard]] zend_long isScopeAuthoritative(zval *scope) const + { + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + zend_class_entry *arrowFunctionCe = pt_class(PT_CLASS_ARROW_FUNCTION); + if (UNEXPECTED(variableCe == NULL || closureCe == NULL || arrowFunctionCe == NULL)) return -1; + zend_object *expr = Z_OBJ_P(slot(slots::expr)); + if (instanceof_function(expr->ce, variableCe)) { + zv::Ref name = zv::ObjRef(expr).prop(PT_LC("name")); + if (name.raw() == NULL || !name.deref().isString()) return 0; + zend_long has = scopeTrinary(scope, PT_LC("hasvariabletype"), name.deref().raw()); + if (UNEXPECTED(has < 0)) return -1; + return has != PT_TRI_NO ? 1 : 0; + } + + if (instanceof_function(expr->ce, closureCe) || instanceof_function(expr->ce, arrowFunctionCe)) return 0; + zend_long has = scopeTrinary(scope, PT_LC("hasexpressiontype"), slot(slots::expr)); + if (UNEXPECTED(has < 0)) return -1; + return has == PT_TRI_YES ? 1 : 0; + } + + /* Mirrors getReadVariableNames(): the list (an owned array). */ + zv::Val getReadVariableNames() + { + zval *memo = slot(slots::readVariableNames); + if (Z_TYPE_P(memo) == IS_ARRAY) return zv::Val::copyOf(zv::Ref(memo)); + zv::Val names = collectReadVariableNames(Z_OBJ_P(slot(slots::expr))); + return memoize(slots::readVariableNames, std::move(names)); + } + + /* Mirrors collectReadVariableNames(): a list of the names, cached as a + * node attribute on Expr nodes; UNDEF = pending exception. */ + static zv::Val collectReadVariableNames(zend_object *node) + { + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + zend_class_entry *nodeCe = pt_class(PT_CLASS_NODE); + if (UNEXPECTED(exprCe == NULL || variableCe == NULL || closureCe == NULL || nodeCe == NULL)) return zv::Val(); + bool isExpr = instanceof_function(node->ce, exprCe); + if (isExpr) { + zval *cached = pt_node_attribute(node, pt_er_read_variable_names_attribute); + if (cached != NULL && Z_TYPE_P(cached) != IS_NULL) return zv::Val::copyOf(zv::Ref(cached)); + } + + /* $names as a set (the keys), the list is array_keys() of it */ + zv::Arr names = zv::Arr::empty(); + if (instanceof_function(node->ce, variableCe)) { + zv::Ref name = zv::ObjRef(node).prop(PT_LC("name")); + if (name.raw() != NULL && name.deref().isString() && !zend_string_equals_literal(name.deref().asString(), "this")) { + zval trueValue; + ZVAL_TRUE(&trueValue); + names.separate(); + zend_symtable_update(names.table(), name.deref().asString(), &trueValue); + } + } + if (instanceof_function(node->ce, closureCe)) { + // a closure body's variables live in its own scope - only the + // use() clause reads the enclosing position. Arrow functions + // capture implicitly and are traversed. + zv::Ref uses = zv::ObjRef(node).prop(PT_LC("uses")); + if (uses.raw() != NULL && uses.deref().isArray()) { + for (auto entry : zv::TableRef(uses.deref().asArrayTable())) { + zv::Ref use = entry.value().deref(); + if (!use.isObject()) continue; + zv::Ref var = zv::ObjRef(use.asObject()).prop(PT_LC("var")); + if (var.raw() == NULL || !var.deref().isObject()) continue; + zv::Ref useName = zv::ObjRef(var.deref().asObject()).prop(PT_LC("name")); + if (useName.raw() == NULL || !useName.deref().isString()) continue; + zval trueValue; + ZVAL_TRUE(&trueValue); + names.separate(); + zend_symtable_update(names.table(), useName.deref().asString(), &trueValue); + } + } + } else { + pt_node_class_info *info = pt_node_class_info_for_object(node); + if (info != NULL && PT_HAS_SUBNODES(info)) { + for (uint32_t i = 0; i < info->subnode_count; i++) { + zval *subNode = OBJ_PROP(node, info->subnode_offsets[i]); + ZVAL_DEINDIRECT(subNode); + ZVAL_DEREF(subNode); + if (Z_TYPE_P(subNode) == IS_OBJECT && instanceof_function(Z_OBJCE_P(subNode), nodeCe)) { + if (UNEXPECTED(!mergeNames(names, Z_OBJ_P(subNode)))) return zv::Val(); + } else if (Z_TYPE_P(subNode) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(subNode))) { + zv::Ref item = entry.value().deref(); + if (!item.isObject() || !instanceof_function(Z_OBJCE_P(item.raw()), nodeCe)) continue; + if (UNEXPECTED(!mergeNames(names, item.asObject()))) return zv::Val(); + } + } + } + } + } + + /* array_keys($names) */ + zv::Arr result = zv::Arr::create(zend_hash_num_elements(names.table())); + for (auto entry : zv::TableRef(names.table())) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + result.push(zv::Val::string(key)); + } else { + result.push(zv::Val::integer((zend_long) entry.indexKey())); + } + } + if (isExpr) { + if (UNEXPECTED(!pt_node_set_attribute(node, pt_er_read_variable_names_attribute, result.raw()))) return zv::Val(); + } + + return zv::Val(std::move(result)); + } + + /* foreach (self::collectReadVariableNames($subNode) as $name) $names[$name] = true */ + static bool mergeNames(zv::Arr &names, zend_object *subNode) + { + zv::Val subNames = collectReadVariableNames(subNode); + if (UNEXPECTED(subNames.isUndef())) return false; + for (auto entry : zv::TableRef(Z_ARRVAL_P(subNames.raw()))) { + zval trueValue; + ZVAL_TRUE(&trueValue); + names.separate(); + zv::Ref name = entry.value().deref(); + if (name.isString()) { + zend_symtable_update(names.table(), name.asString(), &trueValue); + } else if (name.isLong()) { + zend_hash_index_update(names.table(), (zend_ulong) name.asLong(), &trueValue); + } + } + return true; + } + + /* $type->isVoid() as a PT_TRI_* value; -1 = pending exception */ + [[nodiscard]] static zend_long trinaryOp(zval *type, pt_type_op_id op) + { + if (UNEXPECTED(Z_TYPE_P(type) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: expected a Type, got %s", zend_zval_value_name(type)); + return -1; + } + zv::Val result = pt_type_op(Z_OBJ_P(type), op, 0, NULL); + if (UNEXPECTED(result.isUndef())) return -1; + return pt_type_trinary_value(result.raw()); + } + + /* $a->isSuperTypeOf($b)->result as a PT_TRI_* value; -1 = pending exception */ + [[nodiscard]] static zend_long isSuperTypeOf(zval *a, zval *b) + { + zv::Val result = pt_type_op(Z_OBJ_P(a), PT_OP_IS_SUPER_TYPE_OF, 1, b); + if (UNEXPECTED(result.isUndef())) return -1; + if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: isSuperTypeOf() did not return a result object"); + return -1; + } + return pt_result_value(Z_OBJ_P(result.raw())); + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::ExpressionResult; + +zv::Val pt_expression_result_variable_flow(zval *result) +{ + /* the twin is final: an instance of the native class entry takes the + * slot, anything else (the PHP twin declared next to the native class in + * the differential tests) the method */ + if (EXPECTED(Z_OBJCE_P(result) == pt_ce_expression_result)) return ExpressionResult(Z_OBJ_P(result)).getVariableFlow(); + return pt_type_call(Z_OBJ_P(result), PT_LC("getvariableflow"), 0, NULL); +} + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define PT_ER_RETURN(expr) \ + do { \ + zv::Val pt_er_result = (expr); \ + if (UNEXPECTED(pt_er_result.isUndef())) { \ + RETURN_THROWS(); \ + } \ + pt_er_result.intoReturnValue(return_value); \ + } while (0) + +#define PT_ER_RETURN_TRINARY_BOOL(expr) \ + do { \ + zend_long pt_er_result = (expr); \ + if (UNEXPECTED(pt_er_result < 0)) { \ + RETURN_THROWS(); \ + } \ + RETURN_BOOL(pt_er_result == 1); \ + } while (0) + +namespace { + +inline constexpr const char *pt_er_self = "PHPStan\\Analyser\\ExpressionResult"; +inline constexpr const char *pt_er_scope = "PHPStan\\Analyser\\MutatingScope"; +inline constexpr const char *pt_er_type = "PHPStan\\Type\\Type"; + +/* a `?callable` argument: NULL for null, the (verified) callable otherwise; + * false with the engine's TypeError pending */ +bool pt_er_callable_arg(zval *arg, uint32_t argNum, bool nullable, zval *&out) +{ + if (arg == NULL || Z_TYPE_P(arg) == IS_NULL) { + if (nullable) { + out = NULL; + return true; + } + } else if (zend_is_callable(arg, 0, NULL)) { + out = arg; + return true; + } + zend_argument_type_error(argNum, "must be of type %scallable, %s given", nullable ? "?" : "", arg == NULL ? "null" : zend_zval_value_name(arg)); + return false; +} + +} // namespace + +void pt_register_expression_result() +{ + pt_er_read_variable_names_attribute = zend_string_init_interned(PT_LC("readVariableNames"), 1); + + reg::Class cls("PHPStan\\Analyser\\ExpressionResult"); + ptdecl::ExpressionResult::declareClass(cls); + /* the twin's properties in declaration order (the OBJ_PROP_NUM slots) */ + cls.privateNullProperty("typeCallback"); + cls.privateNullProperty("specifyTypesCallback"); + cls.privateNullProperty("createTypesCallback"); + cls.privateTypedClassPropertyDefaultNull("truthyScope", pt_er_scope); + cls.privateTypedClassPropertyDefaultNull("falseyScope", pt_er_scope); + cls.privateTypedBoolProperty("extensionsDeclined", false); + cls.privateTypedClassProperty("expressionTypeResolverExtensions", "PHPStan\\DependencyInjection\\ExtensionsCollection", false); + cls.privateTypedClassProperty("defaultNarrowingHelper", "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper", false); + cls.privateTypedClassProperty("scope", pt_er_scope, false); + cls.privateTypedClassProperty("beforeScope", pt_er_scope, false); + cls.privateTypedClassProperty("expr", "PhpParser\\Node\\Expr", false); + cls.privateTypedProperty("hasYield", MAY_BE_BOOL); + cls.privateTypedProperty("isAlwaysTerminating", MAY_BE_BOOL); + cls.privateTypedProperty("throwPoints", MAY_BE_ARRAY); + cls.privateTypedProperty("impurePoints", MAY_BE_ARRAY); + cls.privateTypedBoolProperty("containsNullsafe", false); + cls.privateTypedClassPropertyDefaultNull("issetabilityDescriptor", "PHPStan\\Analyser\\IssetabilityDescriptor"); + cls.privateTypedClassPropertyDefaultNull("truthyScopeOverrideResult", pt_er_self); + cls.privateTypedClassPropertyDefaultNull("falseyScopeOverrideResult", pt_er_self); + cls.privateTypedClassPropertyDefaultNull("type", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("nativeType", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("argsResult", "PHPStan\\Analyser\\ArgsResult"); + cls.privateTypedClassPropertyDefaultNull("variableFlow", "PHPStan\\Analyser\\VariableFlow"); + cls.privateTypedArrayPropertyDefaultEmpty("specifiedTypes"); + cls.privateTypedClassPropertyDefaultNull("cachedType", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("cachedNativeType", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("resolvedType", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("resolvedNativeType", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("projectedType", pt_er_type); + cls.privateTypedClassPropertyDefaultNull("projectedNativeType", pt_er_type); + cls.privateTypedPropertyDefaultNull("readVariableNames", MAY_BE_ARRAY); + + cls.method("__construct", reg::Public, 11, { + reg::obj("expressionTypeResolverExtensions", "PHPStan\\DependencyInjection\\ExtensionsCollection"), + reg::obj("defaultNarrowingHelper", "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper"), + reg::obj("scope", pt_er_scope), + reg::obj("beforeScope", pt_er_scope), + reg::obj("expr", "PhpParser\\Node\\Expr"), + reg::boolArg("hasYield"), + reg::boolArg("isAlwaysTerminating"), + reg::arrayArg("throwPoints"), + reg::arrayArg("impurePoints"), + reg::callableArg("typeCallback", true), + reg::callableArg("specifyTypesCallback"), + reg::withDefault(reg::boolArg("containsNullsafe"), "false"), + reg::withDefault(reg::obj("issetabilityDescriptor", "PHPStan\\Analyser\\IssetabilityDescriptor", true), "null"), + reg::withDefault(reg::obj("truthyScopeOverrideResult", pt_er_self, true), "null"), + reg::withDefault(reg::obj("falseyScopeOverrideResult", pt_er_self, true), "null"), + reg::withDefault(reg::callableArg("createTypesCallback", true), "null"), + reg::withDefault(reg::obj("type", pt_er_type, true), "null"), + reg::withDefault(reg::obj("nativeType", pt_er_type, true), "null"), + reg::withDefault(reg::obj("argsResult", "PHPStan\\Analyser\\ArgsResult", true), "null"), + reg::withDefault(reg::obj("variableFlow", "PHPStan\\Analyser\\VariableFlow", true), "null"), + reg::withDefault(reg::arrayArg("specifiedTypes"), "[]"), + reg::withDefault(reg::obj("cachedType", pt_er_type, true), "null"), + reg::withDefault(reg::boolArg("extensionsDeclined"), "false"), + reg::withDefault(reg::obj("cachedNativeType", pt_er_type, true), "null"), + reg::withDefault(reg::obj("resolvedType", pt_er_type, true), "null"), + reg::withDefault(reg::obj("resolvedNativeType", pt_er_type, true), "null"), + reg::withDefault(reg::obj("projectedType", pt_er_type, true), "null"), + reg::withDefault(reg::obj("projectedNativeType", pt_er_type, true), "null"), + reg::withDefault(reg::arrayArg("readVariableNames", true), "null"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + ConstructorArgs a; + zval *typeCallback, *specifyTypesCallback, *createTypesCallback = NULL; + bool hasYield, isAlwaysTerminating; + bool containsNullsafe = false; + bool extensionsDeclined = false; + zval *issetabilityDescriptor = NULL, *truthyScopeOverrideResult = NULL, *falseyScopeOverrideResult = NULL; + zval *type = NULL, *nativeType = NULL, *argsResult = NULL, *variableFlow = NULL; + zval *specifiedTypes = NULL, *cachedType = NULL, *cachedNativeType = NULL, *resolvedType = NULL, *resolvedNativeType = NULL, *projectedType = NULL, *projectedNativeType = NULL, *readVariableNames = NULL; + ZEND_PARSE_PARAMETERS_START(11, 29) + Z_PARAM_OBJECT(a.expressionTypeResolverExtensions) + Z_PARAM_OBJECT(a.defaultNarrowingHelper) + Z_PARAM_OBJECT(a.scope) + Z_PARAM_OBJECT(a.beforeScope) + Z_PARAM_OBJECT(a.expr) + Z_PARAM_BOOL(hasYield) + Z_PARAM_BOOL(isAlwaysTerminating) + Z_PARAM_ARRAY(a.throwPoints) + Z_PARAM_ARRAY(a.impurePoints) + Z_PARAM_ZVAL(typeCallback) + Z_PARAM_ZVAL(specifyTypesCallback) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(containsNullsafe) + Z_PARAM_OBJECT_OR_NULL(issetabilityDescriptor) + Z_PARAM_OBJECT_OR_NULL(truthyScopeOverrideResult) + Z_PARAM_OBJECT_OR_NULL(falseyScopeOverrideResult) + Z_PARAM_ZVAL(createTypesCallback) + Z_PARAM_OBJECT_OR_NULL(type) + Z_PARAM_OBJECT_OR_NULL(nativeType) + Z_PARAM_OBJECT_OR_NULL(argsResult) + Z_PARAM_OBJECT_OR_NULL(variableFlow) + Z_PARAM_ARRAY(specifiedTypes) + Z_PARAM_OBJECT_OR_NULL(cachedType) + Z_PARAM_BOOL(extensionsDeclined) + Z_PARAM_OBJECT_OR_NULL(cachedNativeType) + Z_PARAM_OBJECT_OR_NULL(resolvedType) + Z_PARAM_OBJECT_OR_NULL(resolvedNativeType) + Z_PARAM_OBJECT_OR_NULL(projectedType) + Z_PARAM_OBJECT_OR_NULL(projectedNativeType) + Z_PARAM_ARRAY_OR_NULL(readVariableNames) + ZEND_PARSE_PARAMETERS_END(); + if (!pt_er_callable_arg(typeCallback, 10, true, a.typeCallback) + || !pt_er_callable_arg(specifyTypesCallback, 11, false, a.specifyTypesCallback) + || !pt_er_callable_arg(createTypesCallback, 16, true, a.createTypesCallback)) { + RETURN_THROWS(); + } + a.hasYield = hasYield; + a.isAlwaysTerminating = isAlwaysTerminating; + a.containsNullsafe = containsNullsafe; + a.issetabilityDescriptor = issetabilityDescriptor; + a.truthyScopeOverrideResult = truthyScopeOverrideResult; + a.falseyScopeOverrideResult = falseyScopeOverrideResult; + a.type = type; + a.nativeType = nativeType; + a.argsResult = argsResult; + a.variableFlow = variableFlow; + a.specifiedTypes = specifiedTypes; + a.cachedType = cachedType; + a.extensionsDeclined = extensionsDeclined; + a.cachedNativeType = cachedNativeType; + a.resolvedType = resolvedType; + a.resolvedNativeType = resolvedNativeType; + a.projectedType = projectedType; + a.projectedNativeType = projectedNativeType; + a.readVariableNames = readVariableNames; + if (UNEXPECTED(!ExpressionResult::construct(Z_OBJ_P(ZEND_THIS), a))) RETURN_THROWS(); + }); + + cls.method(sigs::finalize, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *throwPoints, *impurePoints, *variableFlow; + bool hasYield, isAlwaysTerminating; + if (!zp::parse(execute_data, scope, hasYield, isAlwaysTerminating, throwPoints, impurePoints, variableFlow)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).finalize(scope, hasYield, isAlwaysTerminating, throwPoints, impurePoints, variableFlow)); + }); + + cls.method(sigs::getScope, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getScope()); + }); + + cls.method(sigs::getVariableFlow, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getVariableFlow()); + }); + + cls.method(sigs::withScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + if (!zp::parse(execute_data, scope)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).withScope(scope)); + }); + + cls.method(sigs::getBeforeScope, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getBeforeScope()); + }); + + cls.method(sigs::getExpr, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getExpr()); + }); + + cls.method(sigs::getArgsResult, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getArgsResult()); + }); + + cls.method(sigs::hasYield, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(ExpressionResult(Z_OBJ_P(ZEND_THIS)).hasYield()); + }); + + cls.method(sigs::containsNullsafe, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(ExpressionResult(Z_OBJ_P(ZEND_THIS)).containsNullsafe()); + }); + + cls.method(sigs::getIssetabilityResolution, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + bool useNativeTypes, reprocessUntrackedLinks = false; + if (!zp::parse>(execute_data, scope, useNativeTypes, reprocessUntrackedLinks)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getIssetabilityResolution(scope, useNativeTypes, reprocessUntrackedLinks)); + }); + + cls.method(sigs::getThrowPoints, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getThrowPoints()); + }); + + cls.method(sigs::getImpurePoints, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getImpurePoints()); + }); + + cls.method(sigs::getTruthyScope, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getTruthyScope()); + }); + + cls.method(sigs::getFalseyScope, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getFalseyScope()); + }); + + cls.method(sigs::isAlwaysTerminating, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(ExpressionResult(Z_OBJ_P(ZEND_THIS)).isAlwaysTerminating()); + }); + + cls.method(sigs::getType, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getType()); + }); + + cls.method(sigs::getNativeType, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getNativeType()); + }); + + cls.method(sigs::getKeepVoidType, [](INTERNAL_FUNCTION_PARAMETERS) { + bool nativeTypesPromoted; + if (!zp::parse(execute_data, nativeTypesPromoted)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getKeepVoidType(nativeTypesPromoted)); + }); + + cls.method(sigs::canResolveOwnType, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(ExpressionResult(Z_OBJ_P(ZEND_THIS)).canResolveOwnType()); + }); + + cls.method(sigs::getSpecifiedTypesForScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *context; + if (!zp::parse(execute_data, scope, context)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getSpecifiedTypesForScope(scope, context)); + }); + + cls.method(sigs::getSpecifiedTypes, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *context; + bool nativeTypesPromoted = false; + if (!zp::parse>(execute_data, context, nativeTypesPromoted)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getSpecifiedTypes(context, nativeTypesPromoted)); + }); + + cls.method(sigs::getCreatedTypesForScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *type, *context; + if (!zp::parse(execute_data, scope, type, context)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getCreatedTypesForScope(scope, type, context)); + }); + + cls.method(sigs::getCreatedTypes, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type, *context; + bool nativeTypesPromoted = false; + if (!zp::parse>(execute_data, type, context, nativeTypesPromoted)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getCreatedTypes(type, context, nativeTypesPromoted)); + }); + + cls.method(sigs::getTypeOnScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + bool useNativeTypes; + if (!zp::parse(execute_data, scope, useNativeTypes)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).getTypeOnScope(scope, useNativeTypes)); + }); + + cls.method(sigs::answersOnScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + bool useNativeTypes; + if (!zp::parse(execute_data, scope, useNativeTypes)) RETURN_THROWS(); + PT_ER_RETURN_TRINARY_BOOL(ExpressionResult(Z_OBJ_P(ZEND_THIS)).answersOnScope(scope, useNativeTypes)); + }); + + cls.method(sigs::askScopeVariableStateMatches, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + bool useNativeTypes, ruleFacingAsk = false; + if (!zp::parse>(execute_data, scope, useNativeTypes, ruleFacingAsk)) RETURN_THROWS(); + PT_ER_RETURN_TRINARY_BOOL(ExpressionResult(Z_OBJ_P(ZEND_THIS)).askScopeVariableStateMatches(scope, useNativeTypes, ruleFacingAsk)); + }); + + cls.method(sigs::atAskPosition, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + if (!zp::parse(execute_data, scope)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).atAskPosition(scope)); + }); + + cls.method(sigs::onNonNullabilityDevicedScopes, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *beforeScope, *scope; + if (!zp::parse(execute_data, beforeScope, scope)) RETURN_THROWS(); + PT_ER_RETURN(ExpressionResult(Z_OBJ_P(ZEND_THIS)).onNonNullabilityDevicedScopes(beforeScope, scope)); + }); + + cls.shadow(&pt_ce_expression_result); +} + +/* }}} */ diff --git a/turbo-ext/src/ExpressionResultStorage.cpp b/turbo-ext/src/ExpressionResultStorage.cpp index 9de45c0b046..77e432efba9 100644 --- a/turbo-ext/src/ExpressionResultStorage.cpp +++ b/turbo-ext/src/ExpressionResultStorage.cpp @@ -16,6 +16,7 @@ * fallback chain) into this one, like the twin's SplObjectStorage::addAll(). */ +#include "TypeTraits.h" #include "generated/ExpressionResultStorage.h" namespace slots = ptdecl::ExpressionResultStorage::slot; @@ -25,6 +26,8 @@ namespace sigs = ptdecl::ExpressionResultStorage::sig; #define PT_ERS_PROP_FALLBACK 2 +zend_class_entry *pt_ce_expression_result_storage = nullptr; + namespace phpstanturbo { /* Mirrors PHPStan\Analyser\ExpressionResultStorage. State lives in the PHP @@ -87,6 +90,17 @@ class ExpressionResultStorage using phpstanturbo::ExpressionResultStorage; +#include "TypeTraits.h" + +zv::Val pt_expression_result_storage_find(zval *storage, zval *expr) +{ + /* the twin is final: an instance of the native class entry takes the + * native path, anything else (the PHP twin declared next to the native + * class in the differential tests) the method */ + if (EXPECTED(Z_OBJCE_P(storage) == pt_ce_expression_result_storage)) return ExpressionResultStorage(storage).findExpressionResult(expr); + return pt_type_call(Z_OBJ_P(storage), "findexpressionresult", sizeof("findexpressionresult") - 1, 1, expr); +} + /* {{{ engine ABI glue: parameter parsing + registration */ #include "reg.h" @@ -132,7 +146,24 @@ void pt_register_expression_result_storage() ExpressionResultStorage(ZEND_THIS).findExpressionResult(expr).intoReturnValue(return_value); }); - cls.shadow(NULL); + cls.shadow(&pt_ce_expression_result_storage); +} + +/* }}} */ + +/* {{{ direct entries: the native bodies for a native storage, the methods + * of anything else (the PHP twin under the prefixed differential + * activation) */ + +zv::Val pt_expression_result_storage_new() +{ + return pt_type_new_ce(pt_ce_expression_result_storage, 0, NULL); +} + +zv::Val pt_expression_result_storage_duplicate(zval *storage) +{ + if (EXPECTED(Z_OBJCE_P(storage) == pt_ce_expression_result_storage)) return ExpressionResultStorage(storage).duplicate(); + return pt_type_call(Z_OBJ_P(storage), "duplicate", sizeof("duplicate") - 1, 0, NULL); } /* }}} */ diff --git a/turbo-ext/src/ExpressionResultStorageStack.cpp b/turbo-ext/src/ExpressionResultStorageStack.cpp new file mode 100644 index 00000000000..2704181d4d3 --- /dev/null +++ b/turbo-ext/src/ExpressionResultStorageStack.cpp @@ -0,0 +1,154 @@ +/* + * PHPStanTurbo\ExpressionResultStorageStack — native implementation of + * PHPStan\Analyser\ExpressionResultStorageStack. + * + * The whole state is the twin's `private array $stack` in property slot 0: + * push() appends, pop() drops the last entry (throwing the twin's + * ShouldNotHappenException on an empty stack) and getCurrent() reads + * $stack[count($stack) - 1]. + * + * getCurrent() is asked once per old-world type question the native + * MutatingScope answers (~1M crossings per self-analysis run), which is why + * pt_expression_result_storage_stack_current() below reads the slot directly + * for a native stack instead of calling the method. + */ + +#include "support.h" +#include "generated/ExpressionResultStorageStack.h" + +namespace slots = ptdecl::ExpressionResultStorageStack::slot; +#include "zv.h" + +zend_class_entry *pt_ce_expression_result_storage_stack = nullptr; + +namespace phpstanturbo { + +/* Mirrors PHPStan\Analyser\ExpressionResultStorageStack. State lives in the + * PHP object's stack property. */ +class ExpressionResultStorageStack +{ +public: + explicit ExpressionResultStorageStack(zval *self) : self(self) {} + + void push(zval *storage) { zv::ArrRef(stack()).push(zv::Ref(storage)); } + + /* false = the twin's ShouldNotHappenException on an empty stack */ + bool pop() + { + zval *table = stack(); + if (UNEXPECTED(zend_hash_num_elements(Z_ARRVAL_P(table)) == 0)) { + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (ce != NULL) { + zend_throw_exception(ce, "Unbalanced ExpressionResultStorageStack pop.", 0); + } + return false; + } + arrayPop(table); + return true; + } + + /* $this->stack[count($this->stack) - 1], or null on an empty stack; the + * stack is a list by construction (push() appends, pop() drops the last + * entry), so the count-1 index is always the last element */ + zv::Val getCurrent() const + { + zval *table = stack(); + uint32_t count = zend_hash_num_elements(Z_ARRVAL_P(table)); + if (count == 0) return zv::Val::null(); + zv::Ref found = zv::ArrRef(table).findIndex(count - 1); + if (UNEXPECTED(found.raw() == NULL)) return zv::Val::null(); + return zv::Val::copyOf(found); + } + +private: + zval *stack() const { return OBJ_PROP_NUM(Z_OBJ_P(self), slots::stack); } + + /* array_pop($array): the last element in order removed, the next free + * index pulled back when it was the last appended one */ + static void arrayPop(zval *array) + { + SEPARATE_ARRAY(array); + HashTable *ht = Z_ARRVAL_P(array); + uint32_t idx = ht->nNumUsed; + if (HT_IS_PACKED(ht)) { + while (idx > 0) { + idx--; + zval *p = &ht->arPacked[idx]; + if (Z_TYPE_P(p) != IS_UNDEF) { + if ((zend_long) idx == ht->nNextFreeElement - 1) { + ht->nNextFreeElement--; + } + zend_hash_index_del(ht, idx); + return; + } + } + return; + } + while (idx > 0) { + idx--; + Bucket *p = &ht->arData[idx]; + if (Z_TYPE(p->val) != IS_UNDEF) { + if (p->key == NULL) { + if ((zend_long) p->h == ht->nNextFreeElement - 1) { + ht->nNextFreeElement--; + } + zend_hash_index_del(ht, p->h); + } else { + zend_hash_del(ht, p->key); + } + return; + } + } + } + + zval *self; +}; + +} // namespace phpstanturbo + +using phpstanturbo::ExpressionResultStorageStack; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +void pt_register_expression_result_storage_stack() +{ + reg::Class cls("PHPStan\\Analyser\\ExpressionResultStorageStack"); + ptdecl::ExpressionResultStorageStack::declareClass(cls); + cls.privateTypedArrayPropertyDefaultEmpty("stack"); + + cls.method("push", reg::Public, 1, { reg::any("storage") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *storage; + if (!zp::parse(execute_data, storage)) RETURN_THROWS(); + ExpressionResultStorageStack(ZEND_THIS).push(storage); + }); + + cls.method("pop", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + if (UNEXPECTED(!ExpressionResultStorageStack(ZEND_THIS).pop())) RETURN_THROWS(); + }); + + cls.method("getCurrent", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + ExpressionResultStorageStack(ZEND_THIS).getCurrent().intoReturnValue(return_value); + }); + + cls.shadow(&pt_ce_expression_result_storage_stack); +} + +/* }}} */ + +/* {{{ direct entry for the native MutatingScope (MutatingScope.cpp): the + * native body for a native stack, the method for anything else (the PHP twin + * under the prefixed differential activation) */ + +zv::Val pt_expression_result_storage_stack_current(zval *stack) +{ + /* the twin is final: an instance of the native class entry takes the + * native path */ + if (EXPECTED(Z_OBJCE_P(stack) == pt_ce_expression_result_storage_stack)) return ExpressionResultStorageStack(stack).getCurrent(); + return pt_type_call(Z_OBJ_P(stack), "getcurrent", sizeof("getcurrent") - 1, 0, NULL); +} + +/* }}} */ diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp new file mode 100644 index 00000000000..26bcf6a1fc1 --- /dev/null +++ b/turbo-ext/src/MutatingScope.cpp @@ -0,0 +1,12442 @@ +/* + * PHPStanTurbo\MutatingScope — native implementation of + * PHPStan\Analyser\MutatingScope, the class PHPStan\Analyser\MutatingScope + * IS in a production run (reg::Class::shadow()); the PHP twin keeps its + * body as the reference implementation and is what the prefixed + * activation of tests/scope-family.php compares against, one process + * holding both. The design notes below follow the twin's file order, + * one family of methods per section. + * + * Design: the class, its layout, dispatch and collaborators + * --------------------------------------------------------- + * Class shape. Not final: NodeCallbackScope (final, PHP) extends it and a + * third party may too. The class carries the twin's interfaces (Scope, + * NodeCallbackInvoker, CollectedDataEmitter), so every one of their + * methods must be declared here — linking checks that. + * + * Layout. The twin's properties are declared typed property slots in the + * twin's declaration order — the five class-body properties first + * (resolvedTypes, nodeCallbackScope, namespace, scopeOutOfFirstLevelStatement, + * scopeWithPromotedNativeTypes), then the 32 promoted constructor + * properties in parameter order ($namespace is the one constructor + * parameter that is not promoted) — so the std object handlers do + * GC/clone/free and a PHP subclass's own properties follow them. Promoted + * properties never carry the parameter's default (IS_PROP_UNINIT until the + * constructor writes them); the class-body ones carry theirs. The names + * are load-bearing: ScopeOps.cpp (scopeWith(), the merge/invalidate + * bodies) and ScopeContext.cpp (the exact-class `context` fast + * path) resolve the twin's properties by name through + * pt_instance_prop_offset(), and NodeCallbackScope reads + * $this->scopeFactory, $this->context, $this->expressionTypes & co. as a + * subclass — all of which keep working on the native class unchanged. + * + * Dispatch. `$this->method()` on a non-final method goes through the + * object's class entry (thisCall(): a direct C++ call when the object is + * exactly the native class or its method is the native handler, + * pt_type_call() by name otherwise — NodeCallbackScope overrides + * toNodeCallbackScope/toWalkScope/getType/getNativeType/getParentScope/ + * getScopeType/getScopeNativeType/getKeepVoidType/ + * filterByTruthyValue/filterByFalseyValue/pushInFunctionCall/popInFunctionCall, + * and a subclass may override anything + * else). Private methods are direct C++ calls, as PHP never dispatches + * them. + * + * Collaborators. The 14 injected services (Container, InternalScopeFactory, + * ReflectionProvider, InitializerExprTypeResolver, ExtensionsCollection, + * ExprPrinter, TypeSpecifier, PropertyReflectionFinder, Parser, + * ConstantResolver, ExpressionResultStorageStack, ScopeContext, PhpVersion, + * AttributeReflectionFactory) are held as PHP objects in their slots and + * called by name (pt_type_call) — they are PHP classes today, except + * ScopeContext (native: ScopeContext.cpp; read through its slots when the + * object is exactly pt_ce_scope_context, by name otherwise — the prefixed + * harness hands the native scope a PHP context because the PHP factory's + * create() is typed with the real name). ExpressionTypeHolder (native) is + * read through its slots when it is one (pt_ce_expr_type_holder), by name + * otherwise; ScopeOps' bodies through pt_scope_ops_* direct entries; + * StaticTypeFactory and the Type constructors through their pt_* exports; + * ExpressionResultStorageStack is a PHP class (its stack is a plain array) + * and stays a by-name collaborator. `new` of PHP classes (ConstFetch, + * FullyQualified, UndefinedVariableException) and static calls + * (VolatileExpressionHelper) go through the class map (pt_type_new / + * pt_type_call_static); the by-reference array parameters of + * VolatileExpressionHelper are passed as fresh references and read back. + * + * Construction cost. LazyInternalScopeFactory::create() (1.9M/run) and + * toNodeCallbackScope() build scopes: the constructor is zpp over the 33 + * parameters plus 37 slot writes (ZVAL_COPY into the slots, no + * allocation besides the object; `''` → null for $namespace), and the + * factory's create() itself stays a PHP call from here — the twin's + * $this->scopeFactory->create(...) sites build their 18 arguments from + * the slots (CreateArgs) and call it by name. + * + * Design: the type resolution core (twin 1054–1808) + * ------------------------------------------------- + * getType() reaches ScopeOps' memo and tracked-holder fast paths through + * direct entries (pt_scope_ops_get_type_from_cache / + * pt_scope_ops_expression_type_by_key), TypeUtils::resolveLateResolvableTypes() + * through pt_type_utils_resolve_late_resolvable_types(), and writes the + * $resolvedTypes memo into its slot. The NodeScopeResolver::$guard* + * diagnostics are read as the static properties they are (class map). + * resolveType()'s ExpressionTypeResolverExtension sweep and + * ExprHandlerRegistry::resolve() stay by-name / static calls; the on-demand + * pricing (resolveTypeOfNewWorldHandlerNode & co.) goes through + * $this->container->getByType() with the twin's compile-time class-name + * strings, `new ExpressionResultStorage()` / findExpressionResult() / + * duplicate() through the native storage's direct entries (which fall back + * to the methods for a PHP twin storage — the prefixed harness's case), + * and the ExpressionResult / ExpressionResultStorageStack collaborators by + * name. $this->toWalkScope() is a dispatched call (NodeCallbackScope + * overrides it): the walk scope it returns is any MutatingScope, so its + * $nativeTypesPromoted is read through the slot when it is this class and + * by property name otherwise. `clone $this` (withTemplateArgumentConstraints) + * is the object's clone handler; the property write of + * withTemplateArgumentFrame() on the factory's result goes through the + * engine write path from the result's own class (the property is + * protected). getClosureScopeCacheKey() builds the joined parts in a + * smart_str and hashes them with the engine's MD5. resolveName() / + * resolveTypeByName() use the ClassReflection and ReflectionAccess + * readers and the StaticType / ThisType / ObjectType constructors; + * getTypeFromValue() the ConstantTypeHelper direct entry. getKeepVoidType() + * and getCurrentTypesOfSpecifiedExpr() go through the private + * getScopeStateType() / resolveScopeStateType() (twin 3473 / 3490); the + * public reflection lookups those dispatch (getInstancePropertyReflection, + * getStaticPropertyReflection, getMethodReflection) always go through the + * object's method, so a subclass override answers. + * + * Design: the in-function-call stack and the enter* families (twin 1835-2558) + * --------------------------------------------------------------------------- + * pushInFunctionCall() / popInFunctionCall() copy the call stack, push or + * array_pop() it and hand it to create(); the twin's + * `$scope->resolvedTypes = $this->resolvedTypes` goes through the engine + * write path from the result's own class (the factory answers with any + * MutatingScope). Both are named handlers, as are getParentScope() and + * (later) filterByTruthyValue/filterByFalseyValue: NodeCallbackScope + * overrides them, so a $this-dispatch must be able to identify the native + * body. isInClassExists() / isInFunctionExists() build their + * `\class_exists('X')` FuncCall from the node class map and ask + * $this->getType(); the call-stack readers filter the entries that carry a + * reflection. + * + * The enter* families are argument assembly: enterClass() and enterTrait() + * build the new ScopeContext through the context's own method (by name -- + * the differential harness hands the native scope a PHP context) and pass + * the twin's exact create() argument list. enterClassMethod(), + * enterPropertyHook() and enterFunction() assemble a + * PhpMethodFromParserNodeReflection / PhpFunctionFromParserNodeReflection + * (28 / 22 constructor arguments, an Args list of borrowed and owned + * values) out of getRealParameterTypes() / getRealParameterDefaultValues() + * / getParameterAttributes() / transformStaticType(), and hand it to + * enterFunctionLike(), which builds the parameter tables: the + * ConditionalTypeForParameter holders (native ConditionalExpressionHolders + * over the native class entry), the variadic parameter's array shape, the + * ParameterVariableOriginalValueExpr entries, and array_merge() of the + * constant types with them (PHP's semantics: string keys of the later win, + * integer keys are appended). The closure-bind family + * (enterClosureBind/restoreOriginalScopeAfterClosureBind/restoreThis/ + * enterClosureCall/withClosureBindScopeClasses) rewrites the $this entry of + * the two tables through the TablePair and reads the other scope's + * slots directly (the parameter is class-checked against + * pt_ce_mutating_scope), dispatching only its isInClass() by name. + * + * getPhpVersion(), getFunctionType() and isParameterValueNullable() sit + * with them, out of the twin's file order (the twin dispatches the first + * two through $this, so they are named handlers too); getPhpVersion() + * reads the twin's PHP_MIN_ANALYZABLE_VERSION_ID / MAX_PHP_VERSION as the + * #defines at the top of this file and builds its PhpVersions over the + * native IntegerRangeType / ConstantIntegerType. + * + * Design: the function entries, assignment and specification (twin 2560-3990) + * --------------------------------------------------------------------------- + * This is where the twin stops answering out of one scope and starts + * chaining: `$scope = $this->a()->b()`. The first call is a $this-dispatch; + * every one after it runs on whatever InternalScopeFactory::create() + * answered, which is any MutatingScope. The otherProp() / otherCall() / + * otherPrivate() helpers cover that: a table property of another scope by + * slot when the object is (a subclass of) the native class and by name + * otherwise, a public method always by name, and a private method — never + * registered, so the engine cannot find one on the native class — as the + * native body for a native object and the object's own method otherwise. + * The same split drives the in-place specification: specifyExpressionType() + * opens an unpublished working copy through the factory and writes into it, + * and assignVariable() writes holders straight into the scope the factory + * answered (writeScopeTable(), the engine's SEPARATE_ARRAY path, never the + * slot index of a foreign class). + * + * enterAnonymousFunctionWithoutReflection() and its arrow-function sibling + * assemble the parameter and use tables (getFunctionType() narrowed by + * getCallableParameterType(), the NodeFinder walks of + * invalidateStaticExpressions() and the `use` filter as + * pt_find_first_recursive() collectors), then hand them to create(); + * enterAnonymousFunction() / enterArrowFunction() only add the + * ClosureTypeResolver's reflection and rebuild the argument list from the + * scope their sibling answered. The invalidation family reaches ScopeOps + * through the new pt_scope_ops_scope_with / + * pt_scope_ops_invalidate_expression_entries / + * pt_scope_ops_invalidate_methods_on_expression / + * pt_scope_ops_intertwined_ref_root_variable_name direct entries (rule 4 + * forbids the class map for a shadowed class), and + * specifyExpressionTypeInPlace()'s offset narrowing through + * pt_static_type_factory_int_offset_accessible / + * ..._general_offset_accessible and pt_has_offset_value_type_new(). + * + * Design: the narrowing application and the scope merges (twin 3993-4773) + * ----------------------------------------------------------------------- + * applySpecifiedTypes() is the batch that turns a SpecifiedTypes into a + * scope. Its deferred augments and conditional-expression recipes are + * evaluated against THIS scope (the application point of the narrowing) + * before and after the batch respectively; the augment queue is a growing + * list walked by index, which is what the twin's array_shift()/append pair + * amounts to. The batch itself is a std::vector of TypeSpecification + * entries collected out of getSureTypes()/getSureNotTypes()/ + * getAlternativeTypes() and std::stable_sort()ed by the twin's comparator + * (shorter expression keys first, sure specifications before sure-not ones + * — PHP's usort is stable). Every entry then runs on whatever scope the + * previous one answered, so the whole loop goes through the + * foreign-scope helpers: otherPrivate() for setExpressionCertaintyKeepingType(), + * unsetExpression(), getCurrentTypesOfSpecifiedExpr(), isComplexUnionType(), + * openSpecificationScope(), specifyExpressionTypeInPlace() and + * processConditionalExpressionsAfterSpecifying(); otherProp()/otherTable() + * for the holder maps; the public getters by name. The final create() is + * built from that scope (fillCreateArgsFromOther()) and goes to ITS + * factory. The holders the batch records are built with pt_holder_create() + * — the native ExpressionTypeHolder, which is the twin's class in + * production. + * + * The conditional-expression bookkeeping and the merges reach ScopeOps + * through five new direct entries (pt_scope_ops_match_conditional_expressions, + * ..._merge_variable_holders, ..._finish_merge, + * ..._intersect_conditional_expressions, ..._create_conditional_expressions; + * ..._should_invalidate_expression for the generalization), and + * TrinaryLogic::lazyExtremeIdentity()/maxMin()/or() are the PT_TRI_* bit + * arithmetic they are (YES = 3, MAYBE = 1, NO = 0). The three private merge + * helpers (withoutPreciseClassConstantFetches, preserveVacuousConditional- + * Expressions, mergeSameGuardConditionalExpressions) answer with a new + * table, as the twin's by-value arrays do, and build their union holder + * over pt_ce_cond_expr_holder + pt_ceh_key_build() (rule 4: never the class + * map for a shadowed class). exitFirstLevelStatements() memoizes on the + * scopeOutOfFirstLevelStatement slot and carries $resolvedTypes over + * through the engine write path, like pushInFunctionCall(). + * + * Design: the closure and loop scopes, the generalization, the + * comparisons and the member-access queries (twin 4775-5884) + * --------------------------------------------------------------------- + * processClosureScope() and + * processAlwaysIterableForeachScopeWithoutPollute() are table rewrites + * over another scope's holders (the by-ref `use` list, the loop's final + * scope) whose certainty arithmetic is TrinaryLogic::and() as the bitwise + * AND it is (YES = 3, MAYBE = 1, NO = 0). generalizeWith() -> + * generalizeWithVariableState() -> generalizeVariableTypeHolders() is the + * loop fixed-point: the twin's `uksort(strlen <=> strlen)` is a + * std::stable_sort over a collected entry list (PHP's sort is stable too), + * the already-generalized expressions are re-tested through the + * pt_scope_ops_should_invalidate_expression() direct entry, and the + * writable-variable set is seeded from the + * IntertwinedVariableByReferenceWithExpr holders of both scopes. + * + * generalizeType() is the one long body of the family: it sorts + * the unions of both inputs, flattened, into the seven buckets the twin + * names (constant integers / floats / booleans / strings, constant arrays, + * general arrays, integer ranges, everything else), each bucket a pair of + * list arrays, and rebuilds a type out of them. Every bucket test that is + * a class test goes through the shadowed classes' own entries + * (pt_ce_constant_integer_type & co., rule 4) and every one that is a + * predicate through the type ops (PT_OP_IS_CONSTANT_ARRAY, PT_OP_IS_ARRAY); + * the arithmetic of the integer and range arms is plain zend_longs with + * ZEND_LONG_MIN/MAX for the open bounds, the shapes go through + * pt_constant_array_type_builder_*(), and the accessories through + * pt_non_empty_array_type_new() / pt_accessory_array_list_type_new() / + * pt_oversized_array_type_new(). `TypeCombinator::union(...$list)` is a + * std::vector of borrowed zvals handed to pt_type_combinator_union(). + * + * equals() and its two private comparators walk the tables and the + * conditional holders through the holder readers. The visibility + * queries (canAccessProperty/canReadProperty/canWriteProperty/ + * canCallMethod/canAccessConstant) share one memberAccessibleFromScope() + * that runs the twin's `$canAccessClassMember` closure over the + * closure-bind classes and the scope's own class, parameterized by the + * private predicate (isPrivate() / isPrivateSet()) the two callers pass. + * The union-filtering member lookups (filterTypeWithMethod(), + * getMethodReflection(), getNakedMethod(), getPropertyReflection(), + * getInstancePropertyReflection(), getStaticPropertyReflection(), + * getConstantReflection(), getIterableKeyType(), getIterableValueType()) + * need a PHP callable for UnionType::filterTypes(): one + * pt_type_native_callback() holder whose state is the member name and the + * lowercase predicate to ask of every inner type. debug() builds its + * descriptions with the engine's spprintf and pt_type_describe_precise(), + * and invokeNodeCallback() / emitCollectedData() enter the stored callable + * through pt_type_call_callable(). + */ + +#include "TypeTraits.h" +#include "generated/MutatingScope.h" + +namespace sigs = ptdecl::MutatingScope::sig; +#include "TypeOps.h" + +#include +#include + +/* md5.h carries no extern "C" guard of its own */ +extern "C" { +#include "ext/standard/md5.h" +} +#include "zend_smart_str.h" + +#include +#include + +zend_class_entry *pt_ce_mutating_scope = nullptr; + +/* OBJ_PROP_NUM slots, in the twin's declaration order: the class-body + * properties first, the promoted constructor properties after them */ +enum : uint32_t +{ + PT_MS_PROP_RESOLVED_TYPES = 0, + PT_MS_PROP_NODE_CALLBACK_SCOPE, + PT_MS_PROP_NAMESPACE, + PT_MS_PROP_SCOPE_OUT_OF_FIRST_LEVEL_STATEMENT, + PT_MS_PROP_SCOPE_WITH_PROMOTED_NATIVE_TYPES, + PT_MS_PROP_CONTAINER, + PT_MS_PROP_SCOPE_FACTORY, + PT_MS_PROP_REFLECTION_PROVIDER, + PT_MS_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, + PT_MS_PROP_EXPRESSION_TYPE_RESOLVER_EXTENSIONS, + PT_MS_PROP_EXPR_PRINTER, + PT_MS_PROP_TYPE_SPECIFIER, + PT_MS_PROP_PROPERTY_REFLECTION_FINDER, + PT_MS_PROP_PARSER, + PT_MS_PROP_CONSTANT_RESOLVER, + PT_MS_PROP_EXPRESSION_RESULT_STORAGE_STACK, + PT_MS_PROP_CONTEXT, + PT_MS_PROP_PHP_VERSION, + PT_MS_PROP_ATTRIBUTE_REFLECTION_FACTORY, + PT_MS_PROP_CONFIGURED_PHP_VERSION_RANGE_HELPER, + PT_MS_PROP_NODE_CALLBACK, + PT_MS_PROP_DECLARE_STRICT_TYPES, + PT_MS_PROP_FUNCTION, + PT_MS_PROP_EXPRESSION_TYPES, + PT_MS_PROP_NATIVE_EXPRESSION_TYPES, + PT_MS_PROP_CONDITIONAL_EXPRESSIONS, + PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, + PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION, + PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, + PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, + PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, + PT_MS_PROP_IN_FUNCTION_CALLS_STACK, + PT_MS_PROP_AFTER_EXTRACT_CALL, + PT_MS_PROP_PARENT_SCOPE, + PT_MS_PROP_NATIVE_TYPES_PROMOTED, + PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME, + PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, + PT_MS_PROP_COUNT, +}; + +/* private const COMPLEX_UNION_TYPE_MEMBER_LIMIT */ +#define PT_MS_COMPLEX_UNION_TYPE_MEMBER_LIMIT 8 + +/* private const GLOBAL_CONSTANT_FETCH_KEYS_LIMIT */ +#define PT_MS_GLOBAL_CONSTANT_FETCH_KEYS_LIMIT 8192 + +/* PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID and + * PHPStan\Php\PhpVersionFactory::MAX_PHP_VERSION — the one place the native + * code reads them from (getPhpVersion()) */ +#define PT_MS_PHP_MIN_ANALYZABLE_VERSION_ID 50207 +#define PT_MS_MAX_PHP_VERSION 80699 + +/* the handlers the $this-dispatch fast paths identify (a subclass may + * override any of these) */ +static void ZEND_FASTCALL msGetFile(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msIsDeclareStrictTypes(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msIsReadonlyPropertyFetch(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msIsInClass(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetClassReflection(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetFunction(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetNamespace(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msCanAnyVariableExist(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msHasVariableType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msIsInAnonymousFunction(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetNodeKey(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msHasExpressionType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msIsInFirstLevelStatement(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msToWalkScope(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetVariableType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msDuplicateWith(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msObtainResultForNode(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msWithTemplateArgumentConstraints(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msWithoutMemoizedTypes(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetNativeType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msDoNotTreatPhpDocTypesAsCertain(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msResolveName(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msResolveTypeByName(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetParentScope(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msPushInFunctionCall(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msPopInFunctionCall(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetPhpVersion(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetFunctionType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msIsParameterValueNullable(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msGetCurrentExpressionResultStorage(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msEnterAnonymousFunctionWithoutReflection(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msEnterArrowFunctionWithoutReflection(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msAssignVariable(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msAssignExpression(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msSpecifyExpressionType(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msInvalidateExpression(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msFilterByTruthyValue(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msFilterByFalseyValue(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msApplySpecifiedTypes(INTERNAL_FUNCTION_PARAMETERS); +static void ZEND_FASTCALL msFilterTypeWithMethod(INTERNAL_FUNCTION_PARAMETERS); + +/* {{{ the internal scope factory's own state (LazyInternalScopeFactory) + + * Every scope the native code derives goes through + * $this->scopeFactory->create(...), whose twin only resolves its services out + * of the container once and then news the scope class. The services live in + * the factory's own property slots after that first create(), so the whole + * method is those slots plus a `new` — which is what factoryCreate() below + * does, leaving the twin's create() to the first call of each factory (while + * the memos are still null) and to any other InternalScopeFactory. */ + +/* the services create() passes on, in the scope constructor's parameter + * order; all of them are `??=` memos the first create() fills */ +enum : uint32_t +{ + PT_ISF_REFLECTION_PROVIDER = 0, + PT_ISF_INITIALIZER_EXPR_TYPE_RESOLVER, + PT_ISF_EXPRESSION_TYPE_RESOLVER_EXTENSIONS, + PT_ISF_EXPR_PRINTER, + PT_ISF_TYPE_SPECIFIER, + PT_ISF_PROPERTY_REFLECTION_FINDER, + PT_ISF_CONSTANT_RESOLVER, + PT_ISF_PHP_VERSION, + PT_ISF_ATTRIBUTE_REFLECTION_FACTORY, + PT_ISF_CONFIGURED_PHP_VERSION_RANGE_HELPER, + PT_ISF_MEMO_COUNT +}; + +static const char *const pt_isf_memo_names[PT_ISF_MEMO_COUNT] = { + "reflectionProvider", + "initializerExprTypeResolver", + "expressionTypeResolverExtensions", + "exprPrinter", + "typeSpecifier", + "propertyReflectionFinder", + "constantResolver", + "phpVersionType", + "attributeReflectionFactory", + "configuredPhpVersionRangeHelper", +}; + +/* the instance-property slot offsets of the factory's class entry, resolved + * once (the twin is final, so there is one) and forgotten at rinit */ +struct InternalScopeFactorySlots +{ + zend_class_entry *ce; + uint32_t memos[PT_ISF_MEMO_COUNT]; + uint32_t container; + uint32_t parser; + uint32_t expressionResultStorageStack; + uint32_t nodeCallback; + uint32_t createsNodeCallbackScopes; +}; + +static InternalScopeFactorySlots pt_isf_slots = {}; + +void pt_mutating_scope_rinit() +{ + pt_isf_slots.ce = NULL; +} + +/* the slots of an object that is exactly a LazyInternalScopeFactory; NULL + * when it is of some other class (the caller then calls create()) — or, with + * `error` set and an exception pending, when the class map cannot resolve the + * class at all */ +static const InternalScopeFactorySlots *internalScopeFactorySlots(zend_object *factory, bool &error) +{ + error = false; + if (EXPECTED(factory->ce == pt_isf_slots.ce)) return &pt_isf_slots; + zend_class_entry *ce = pt_class_loaded(PT_CLASS_LAZY_INTERNAL_SCOPE_FACTORY); + if (ce == NULL) { + error = EG(exception) != NULL; + return NULL; + } + if (factory->ce != ce) return NULL; + InternalScopeFactorySlots slots; + slots.ce = ce; + int32_t offsets[PT_ISF_MEMO_COUNT + 5]; + for (uint32_t i = 0; i < PT_ISF_MEMO_COUNT; i++) { + offsets[i] = pt_instance_prop_offset(ce, pt_isf_memo_names[i], strlen(pt_isf_memo_names[i])); + } + offsets[PT_ISF_MEMO_COUNT + 0] = pt_instance_prop_offset(ce, PT_LC("container")); + offsets[PT_ISF_MEMO_COUNT + 1] = pt_instance_prop_offset(ce, PT_LC("currentSimpleVersionParser")); + offsets[PT_ISF_MEMO_COUNT + 2] = pt_instance_prop_offset(ce, PT_LC("expressionResultStorageStack")); + offsets[PT_ISF_MEMO_COUNT + 3] = pt_instance_prop_offset(ce, PT_LC("nodeCallback")); + offsets[PT_ISF_MEMO_COUNT + 4] = pt_instance_prop_offset(ce, PT_LC("createsNodeCallbackScopes")); + for (uint32_t i = 0; i < PT_ISF_MEMO_COUNT + 5; i++) { + if (UNEXPECTED(offsets[i] < 0)) { + /* not the twin this reader knows: every call goes through the method */ + return NULL; + } + } + for (uint32_t i = 0; i < PT_ISF_MEMO_COUNT; i++) { + slots.memos[i] = (uint32_t) offsets[i]; + } + slots.container = (uint32_t) offsets[PT_ISF_MEMO_COUNT + 0]; + slots.parser = (uint32_t) offsets[PT_ISF_MEMO_COUNT + 1]; + slots.expressionResultStorageStack = (uint32_t) offsets[PT_ISF_MEMO_COUNT + 2]; + slots.nodeCallback = (uint32_t) offsets[PT_ISF_MEMO_COUNT + 3]; + slots.createsNodeCallbackScopes = (uint32_t) offsets[PT_ISF_MEMO_COUNT + 4]; + pt_isf_slots = slots; + return &pt_isf_slots; +} + +/* }}} */ + +namespace phpstanturbo { + +/* the 18 arguments of InternalScopeFactory::create(), in its parameter + * order — the twin's $this->scopeFactory->create(...) sites fill them from + * the slots, a few literals and the dispatched getters (whose results the + * struct keeps alive for the call) */ +struct CreateArgs +{ + enum : uint32_t + { + CONTEXT = 0, + DECLARE_STRICT_TYPES, + FUNCTION, + NAMESPACE_, + EXPRESSION_TYPES, + NATIVE_EXPRESSION_TYPES, + CONDITIONAL_EXPRESSIONS, + IN_CLOSURE_BIND_SCOPE_CLASSES, + ANONYMOUS_FUNCTION_REFLECTION, + IN_FIRST_LEVEL_STATEMENT, + CURRENTLY_ASSIGNED_EXPRESSIONS, + CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, + IN_FUNCTION_CALLS_STACK, + AFTER_EXTRACT_CALL, + PARENT_SCOPE, + NATIVE_TYPES_PROMOTED, + TEMPLATE_ARGUMENT_FRAME, + TEMPLATE_ARGUMENT_CONSTRAINTS, + COUNT, + }; + + zval argv[COUNT]; + zv::Val owned[COUNT]; + + CreateArgs() + { + for (uint32_t i = 0; i < COUNT; i++) { + ZVAL_NULL(&argv[i]); + } + } + + void set(uint32_t i, zv::Ref borrowed) { ZVAL_COPY_VALUE(&argv[i], borrowed.raw()); } + void setBool(uint32_t i, bool value) { ZVAL_BOOL(&argv[i], value); } + void setNull(uint32_t i) { ZVAL_NULL(&argv[i]); } + void setEmptyArray(uint32_t i) { ZVAL_EMPTY_ARRAY(&argv[i]); } + void setOwned(uint32_t i, zv::Val value) + { + owned[i] = std::move(value); + ZVAL_COPY_VALUE(&argv[i], owned[i].raw()); + } +}; + +/* the argument list of a `new` of a PHP class: borrowed or owned values in + * the constructor's parameter order */ +template +struct Args +{ + zval argv[N]; + zv::Val owned[N]; + uint32_t count = 0; + + void add(zv::Ref borrowed) { ZVAL_COPY_VALUE(&argv[count++], borrowed.raw()); } + void addBool(bool value) { ZVAL_BOOL(&argv[count++], value); } + void addNull() { ZVAL_NULL(&argv[count++]); } + void addEmptyArray() { ZVAL_EMPTY_ARRAY(&argv[count++]); } + + void addOwned(zv::Val value) + { + owned[count] = std::move(value); + ZVAL_COPY_VALUE(&argv[count], owned[count].raw()); + count++; + } +}; + +/* an owned create() argument whose producer may have thrown */ +#define PT_MS_ARG_CREATE(args, index, expr) \ + do { \ + zv::Val value_ = (expr); \ + if (UNEXPECTED(value_.isUndef())) { \ + return zv::Val(); \ + } \ + (args).setOwned((index), std::move(value_)); \ + } while (0) + +/* an owned argument whose producer may have thrown */ +#define PT_MS_ARG_OWNED(args, expr) \ + do { \ + zv::Val value_ = (expr); \ + if (UNEXPECTED(value_.isUndef())) { \ + return zv::Val(); \ + } \ + (args).addOwned(std::move(value_)); \ + } while (0) + +/* Mirrors PHPStan\Analyser\MutatingScope. State lives in the PHP object's + * property slots. Methods returning zv::Val use UNDEF to signal a pending + * exception; a legitimate PHP null is zv::Val::null(). */ +class MutatingScope +{ +public: + explicit MutatingScope(zend_object *self) : self(self) {} + + /* {{{ the slots */ + + zv::Ref slot(uint32_t index) const { return zv::ObjRef(self).propAt(index); } + + /* a slot write that also clears IS_PROP_UNINIT (the promoted typed + * properties start uninitialized) */ + void writeSlot(uint32_t index, zv::Val value) + { + zval *p = OBJ_PROP_NUM(self, index); + zv::ObjRef(self).propAtWrite(index, std::move(value)); + Z_PROP_FLAG_P(p) = 0; + } + + zv::Val copyOfSlot(uint32_t index) const { return zv::Val::copyOf(slot(index)); } + + bool slotBool(uint32_t index) const { return Z_TYPE_P(slot(index).raw()) == IS_TRUE; } + + zval *thisZval() + { + ZVAL_OBJ(&selfZval, self); + return &selfZval; + } + + /* }}} */ + + /* {{{ $this->context reads: the native ScopeContext's slots when the + * context is one, its methods otherwise (the differential harness + * builds the native scope over a PHP context) */ + + zv::Val contextFile() const { return contextSlot(PT_LC("getfile"), pt_scope_context_file); } + zv::Val contextClassReflection() const { return contextSlot(PT_LC("getclassreflection"), pt_scope_context_class_reflection); } + zv::Val contextTraitReflection() const { return contextSlot(PT_LC("gettraitreflection"), pt_scope_context_trait_reflection); } + + zv::Val contextSlot(const char *lcname, size_t len, zval *(*reader)(zend_object *)) const + { + zv::Ref context = slot(PT_MS_PROP_CONTEXT); + if (UNEXPECTED(!context.isObject())) return uninitializedProperty("context"); + if (EXPECTED(context.asObject()->ce == pt_ce_scope_context)) return zv::Val::copyOf(zv::Ref(reader(context.asObject()))); + return pt_type_call(context.asObject(), lcname, len, 0, NULL); + } + + /* the Error the twin's typed-property read raises when the constructor + * never ran */ + static zv::Val uninitializedProperty(const char *name) + { + zend_throw_error(NULL, "Typed property PHPStan\\Analyser\\MutatingScope::$%s must not be accessed before initialization", name); + return zv::Val(); + } + + /* }}} */ + + /* {{{ $this-dispatch: through the object's class entry, straight to the + * C++ body when the object's method is the native one */ + + template + zv::Val thisCall(const char *lcname, size_t len, zif_handler handler, uint32_t argc, zval *argv, Direct direct) { return pt_this_call(self, self->ce == pt_ce_mutating_scope, lcname, len, handler, argc, argv, direct); } + + /* the same for a bool-returning method; false = pending exception */ + template + bool thisCallBool(const char *lcname, size_t len, zif_handler handler, uint32_t argc, zval *argv, bool &out, Direct direct) { return pt_this_call_bool(self, self->ce == pt_ce_mutating_scope, lcname, len, handler, argc, argv, out, direct); } + + zv::Val thisGetFile() { return thisCall(PT_LC("getfile"), msGetFile, 0, NULL, [&]() { return getFile(); }); } + bool thisIsDeclareStrictTypes(bool &out) { return thisCallBool(PT_LC("isdeclarestricttypes"), msIsDeclareStrictTypes, 0, NULL, out, [&](bool &o) { o = isDeclareStrictTypes(); return true; }); } + bool thisIsInClass(bool &out) { return thisCallBool(PT_LC("isinclass"), msIsInClass, 0, NULL, out, [&](bool &o) { return isInClass(o); }); } + zv::Val thisGetClassReflection() { return thisCall(PT_LC("getclassreflection"), msGetClassReflection, 0, NULL, [&]() { return getClassReflection(); }); } + zv::Val thisGetFunction() { return thisCall(PT_LC("getfunction"), msGetFunction, 0, NULL, [&]() { return getFunction(); }); } + zv::Val thisGetNamespace() { return thisCall(PT_LC("getnamespace"), msGetNamespace, 0, NULL, [&]() { return getNamespace(); }); } + bool thisCanAnyVariableExist(bool &out) { return thisCallBool(PT_LC("cananyvariableexist"), msCanAnyVariableExist, 0, NULL, out, [&](bool &o) { return canAnyVariableExist(o); }); } + bool thisIsInAnonymousFunction(bool &out) { return thisCallBool(PT_LC("isinanonymousfunction"), msIsInAnonymousFunction, 0, NULL, out, [&](bool &o) { o = isInAnonymousFunction(); return true; }); } + bool thisIsInFirstLevelStatement(bool &out) { return thisCallBool(PT_LC("isinfirstlevelstatement"), msIsInFirstLevelStatement, 0, NULL, out, [&](bool &o) { o = isInFirstLevelStatement(); return true; }); } + zv::Val thisHasVariableType(zval *variableName) { return thisCall(PT_LC("hasvariabletype"), msHasVariableType, 1, variableName, [&]() { return hasVariableType(Z_STR_P(variableName)); }); } + zv::Val thisGetNodeKey(zval *node) { return thisCall(PT_LC("getnodekey"), msGetNodeKey, 1, node, [&]() { return getNodeKey(Z_OBJ_P(node)); }); } + zv::Val thisHasExpressionType(zval *node) { return thisCall(PT_LC("hasexpressiontype"), msHasExpressionType, 1, node, [&]() { return hasExpressionType(Z_OBJ_P(node)); }); } + + bool thisIsReadonlyPropertyFetch(zval *expr, bool allowOnlyOnThis, bool &out) + { + zv::Args args{expr, allowOnlyOnThis}; + return thisCallBool(PT_LC("isreadonlypropertyfetch"), msIsReadonlyPropertyFetch, 2, args, out, [&](bool &o) { return isReadonlyPropertyFetch(Z_OBJ_P(expr), allowOnlyOnThis, o); }); + } + + zv::Val thisToWalkScope() { return thisCall(PT_LC("towalkscope"), msToWalkScope, 0, NULL, [&]() { return toWalkScope(); }); } + zv::Val thisGetVariableType(zval *variableName) { return thisCall(PT_LC("getvariabletype"), msGetVariableType, 1, variableName, [&]() { return getVariableType(Z_STR_P(variableName)); }); } + zv::Val thisGetType(zval *node) { return thisCall(PT_LC("gettype"), msGetType, 1, node, [&]() { return getType(Z_OBJ_P(node)); }); } + zv::Val thisObtainResultForNode(zval *node) { return thisCall(PT_LC("obtainresultfornode"), msObtainResultForNode, 1, node, [&]() { return obtainResultForNode(Z_OBJ_P(node)); }); } + zv::Val thisWithTemplateArgumentConstraints(zval *constraints) { return thisCall(PT_LC("withtemplateargumentconstraints"), msWithTemplateArgumentConstraints, 1, constraints, [&]() { return withTemplateArgumentConstraints(constraints); }); } + zv::Val thisWithoutMemoizedTypes() { return thisCall(PT_LC("withoutmemoizedtypes"), msWithoutMemoizedTypes, 0, NULL, [&]() { return withoutMemoizedTypes(); }); } + zv::Val thisGetNativeType(zval *expr) { return thisCall(PT_LC("getnativetype"), msGetNativeType, 1, expr, [&]() { return getNativeType(expr); }); } + zv::Val thisDoNotTreatPhpDocTypesAsCertain() { return thisCall(PT_LC("donottreatphpdoctypesascertain"), msDoNotTreatPhpDocTypesAsCertain, 0, NULL, [&]() { return doNotTreatPhpDocTypesAsCertain(); }); } + zv::Val thisResolveName(zval *name) { return thisCall(PT_LC("resolvename"), msResolveName, 1, name, [&]() { return resolveName(Z_OBJ_P(name)); }); } + zv::Val thisResolveTypeByName(zval *name) { return thisCall(PT_LC("resolvetypebyname"), msResolveTypeByName, 1, name, [&]() { return resolveTypeByName(Z_OBJ_P(name)); }); } + zv::Val thisGetPhpVersion() { return thisCall(PT_LC("getphpversion"), msGetPhpVersion, 0, NULL, [&]() { return getPhpVersion(); }); } + + zv::Val thisFilterTypeWithMethod(zval *typeWithMethod, zval *methodName) + { + zv::Args args{typeWithMethod, methodName}; + return thisCall(PT_LC("filtertypewithmethod"), msFilterTypeWithMethod, 2, args, [&]() { return filterTypeWithMethod(typeWithMethod, Z_STR_P(methodName)); }); + } + + bool thisIsParameterValueNullable(zval *parameter, bool &out) + { + return thisCallBool(PT_LC("isparametervaluenullable"), msIsParameterValueNullable, 1, parameter, out, [&](bool &o) { return isParameterValueNullable(Z_OBJ_P(parameter), o); }); + } + + zv::Val thisGetFunctionType(zval *type, bool isNullable, bool isVariadic) + { + zv::Args args{type, isNullable, isVariadic}; + return thisCall(PT_LC("getfunctiontype"), msGetFunctionType, 3, args, [&]() { return getFunctionType(type, isNullable, isVariadic); }); + } + + /* $this->duplicateWith(...) with its eight arguments in a zval array */ + zv::Val thisDuplicateWith(zval *args) + { + return thisCall(PT_LC("duplicatewith"), msDuplicateWith, 8, args, [&]() { + return duplicateWith(&args[0], &args[1], &args[2], &args[3], &args[4], &args[5], zend_is_true(&args[6]), zend_is_true(&args[7])); + }); + } + + /* a public method the twin calls on $this (getInstancePropertyReflection, + * getStaticPropertyReflection, getMethodReflection): always the + * object's method, so a subclass override answers */ + zv::Val thisCallByName(const char *lcname, size_t len, uint32_t argc, zval *argv) { return pt_type_call(self, lcname, len, argc, argv); } + + /* }}} */ + + /* {{{ ExpressionTypeHolder reads: the native holder's slots, the + * methods of anything else (the twin calls them either way) */ + + static zv::Val holderExpr(zv::Ref holder) { return holderRead(holder, PT_ETH_PROP_EXPR, PT_LC("getexpr")); } + static zv::Val holderType(zv::Ref holder) { return holderRead(holder, PT_ETH_PROP_TYPE, PT_LC("gettype")); } + + /* the PT_TRI_* value of $holder->getCertainty(); -1 = pending exception */ + [[nodiscard]] static zend_long holderCertainty(zv::Ref holder) + { + zv::Ref value = holder.deref(); + if (EXPECTED(value.isObject() && value.asObject()->ce == pt_ce_expr_type_holder)) return pt_holder_certainty_value(value.asObject()); + zv::Val certainty = holderRead(holder, PT_ETH_PROP_CERTAINTY, PT_LC("getcertainty")); + if (UNEXPECTED(certainty.isUndef())) return -1; + return pt_type_trinary_value(certainty.raw()); + } + + static zv::Val holderRead(zv::Ref holder, uint32_t slot, const char *lcname, size_t len) + { + zv::Ref value = holder.deref(); + if (EXPECTED(value.isObject())) { + if (EXPECTED(value.asObject()->ce == pt_ce_expr_type_holder)) return zv::Val::copyOf(zv::ObjRef(value.asObject()).propAt(slot)); + return pt_type_call(value.asObject(), lcname, len, 0, NULL); + } + zend_throw_error(NULL, "Call to a member function %s() on %s", lcname, zend_zval_value_name(value.raw())); + return zv::Val(); + } + + /* }}} */ + + /* {{{ small helpers */ + + /* $value instanceof ; false with an exception pending + * when the key cannot be resolved (an undeclared class is "no") */ + [[nodiscard]] static bool isInstance(zv::Ref value, int classIdx, bool &out) + { + out = false; + if (!value.isObject()) return true; + zend_class_entry *ce = pt_class_loaded(classIdx); + if (ce == NULL) return EG(exception) == NULL; + out = instanceof_function(value.asObject()->ce, ce); + return true; + } + + /* $object->name / $object->var & co.: a public property read by name + * (the node classes' declared subnodes); NULL with an Error pending + * when the class has no such property */ + static zv::Ref nodeProp(zend_object *object, const char *name, size_t len) + { + zv::Ref value = zv::ObjRef(object).prop(name, len); + if (UNEXPECTED(value.raw() == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: %s has no property $%s", ZSTR_VAL(object->ce->name), name); + } + return value; + } + + static zv::Val trinary(zend_long value) { return zv::Val::copyOf(zv::Ref(pt_trinary_singleton(value))); } + + /* + * $factory->create(...$args) — the twin's LazyInternalScopeFactory::create() + * without its frame: the memoized services out of the factory's slots and + * the scope built here, which is all that method does once its `??=` memos + * are filled. + * + * The method itself answers whenever that is not provably the same thing: + * another InternalScopeFactory implementation, a factory whose memos the + * first create() has not filled yet, and — the differential tests' + * prefixed activation, where NodeCallbackScope extends the PHP twin + * instead of the native class — a run in which the classes create() would + * instantiate are not the native ones. UNDEF = pending exception. + */ + /* $factory->create(...$args) through the method */ + static zv::Val factoryCreateCall(zend_object *factory, CreateArgs &args) + { + return pt_type_call(factory, PT_LC("create"), CreateArgs::COUNT, args.argv); + } + + static zv::Val factoryCreate(zend_object *factory, CreateArgs &args) + { + bool error; + const InternalScopeFactorySlots *slots = internalScopeFactorySlots(factory, error); + if (UNEXPECTED(error)) return zv::Val(); + zend_class_entry *nodeCallbackScope = slots != NULL ? pt_class_loaded(PT_CLASS_NODE_CALLBACK_SCOPE) : NULL; + if (UNEXPECTED(nodeCallbackScope == NULL || pt_ce_mutating_scope == NULL || nodeCallbackScope->parent != pt_ce_mutating_scope)) { + if (UNEXPECTED(slots != NULL && EG(exception) != NULL)) return zv::Val(); + return factoryCreateCall(factory, args); + } + for (uint32_t i = 0; i < PT_ISF_MEMO_COUNT; i++) { + if (UNEXPECTED(Z_TYPE_P(OBJ_PROP(factory, slots->memos[i])) != IS_OBJECT)) return factoryCreateCall(factory, args); + } + zval *container = OBJ_PROP(factory, slots->container); + zval *parser = OBJ_PROP(factory, slots->parser); + zval *storageStack = OBJ_PROP(factory, slots->expressionResultStorageStack); + if (UNEXPECTED(Z_TYPE_P(container) != IS_OBJECT || Z_TYPE_P(parser) != IS_OBJECT || Z_TYPE_P(storageStack) != IS_OBJECT)) { + /* an uninitialized promoted property — the twin's Error, raised + * by the method reading it */ + return factoryCreateCall(factory, args); + } + + /* the argument types the twin's create() signature would enforce and + * the new scope reads without checking; anything else is the method's + * TypeError to raise */ + static const uint32_t arrayArgs[] = { + CreateArgs::EXPRESSION_TYPES, + CreateArgs::NATIVE_EXPRESSION_TYPES, + CreateArgs::CONDITIONAL_EXPRESSIONS, + CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, + CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS, + CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, + CreateArgs::IN_FUNCTION_CALLS_STACK, + }; + if (UNEXPECTED(Z_TYPE(args.argv[CreateArgs::CONTEXT]) != IS_OBJECT)) return factoryCreateCall(factory, args); + for (uint32_t i = 0; i < sizeof(arrayArgs) / sizeof(arrayArgs[0]); i++) { + if (UNEXPECTED(Z_TYPE(args.argv[arrayArgs[i]]) != IS_ARRAY)) return factoryCreateCall(factory, args); + } + + /* $className = $this->createsNodeCallbackScopes ? NodeCallbackScope::class : MutatingScope::class; */ + zend_class_entry *className = Z_TYPE_P(OBJ_PROP(factory, slots->createsNodeCallbackScopes)) == IS_TRUE + ? nodeCallbackScope + : pt_ce_mutating_scope; + zval scope; + if (UNEXPECTED(object_init_ex(&scope, className) != SUCCESS)) return zv::Val(); + + ConstructArgs a = {}; + zval factoryZval; + ZVAL_OBJ(&factoryZval, factory); + a.container = container; + a.scopeFactory = &factoryZval; + a.reflectionProvider = OBJ_PROP(factory, slots->memos[PT_ISF_REFLECTION_PROVIDER]); + a.initializerExprTypeResolver = OBJ_PROP(factory, slots->memos[PT_ISF_INITIALIZER_EXPR_TYPE_RESOLVER]); + a.expressionTypeResolverExtensions = OBJ_PROP(factory, slots->memos[PT_ISF_EXPRESSION_TYPE_RESOLVER_EXTENSIONS]); + a.exprPrinter = OBJ_PROP(factory, slots->memos[PT_ISF_EXPR_PRINTER]); + a.typeSpecifier = OBJ_PROP(factory, slots->memos[PT_ISF_TYPE_SPECIFIER]); + a.propertyReflectionFinder = OBJ_PROP(factory, slots->memos[PT_ISF_PROPERTY_REFLECTION_FINDER]); + a.parser = parser; + a.constantResolver = OBJ_PROP(factory, slots->memos[PT_ISF_CONSTANT_RESOLVER]); + a.expressionResultStorageStack = storageStack; + a.context = &args.argv[CreateArgs::CONTEXT]; + a.phpVersion = OBJ_PROP(factory, slots->memos[PT_ISF_PHP_VERSION]); + a.attributeReflectionFactory = OBJ_PROP(factory, slots->memos[PT_ISF_ATTRIBUTE_REFLECTION_FACTORY]); + a.configuredPhpVersionRangeHelper = OBJ_PROP(factory, slots->memos[PT_ISF_CONFIGURED_PHP_VERSION_RANGE_HELPER]); + a.nodeCallback = OBJ_PROP(factory, slots->nodeCallback); + a.declareStrictTypes = Z_TYPE(args.argv[CreateArgs::DECLARE_STRICT_TYPES]) == IS_TRUE; + a.function = &args.argv[CreateArgs::FUNCTION]; + a.ns = Z_TYPE(args.argv[CreateArgs::NAMESPACE_]) == IS_STRING ? Z_STR(args.argv[CreateArgs::NAMESPACE_]) : NULL; + a.expressionTypes = &args.argv[CreateArgs::EXPRESSION_TYPES]; + a.nativeExpressionTypes = &args.argv[CreateArgs::NATIVE_EXPRESSION_TYPES]; + a.conditionalExpressions = &args.argv[CreateArgs::CONDITIONAL_EXPRESSIONS]; + a.inClosureBindScopeClasses = &args.argv[CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES]; + a.anonymousFunctionReflection = &args.argv[CreateArgs::ANONYMOUS_FUNCTION_REFLECTION]; + a.inFirstLevelStatement = Z_TYPE(args.argv[CreateArgs::IN_FIRST_LEVEL_STATEMENT]) == IS_TRUE; + a.currentlyAssignedExpressions = &args.argv[CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS]; + a.currentlyAllowedUndefinedExpressions = &args.argv[CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS]; + a.inFunctionCallsStack = &args.argv[CreateArgs::IN_FUNCTION_CALLS_STACK]; + a.afterExtractCall = Z_TYPE(args.argv[CreateArgs::AFTER_EXTRACT_CALL]) == IS_TRUE; + a.parentScope = &args.argv[CreateArgs::PARENT_SCOPE]; + a.nativeTypesPromoted = Z_TYPE(args.argv[CreateArgs::NATIVE_TYPES_PROMOTED]) == IS_TRUE; + a.templateArgumentFrame = &args.argv[CreateArgs::TEMPLATE_ARGUMENT_FRAME]; + a.templateArgumentConstraints = &args.argv[CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS]; + MutatingScope(Z_OBJ(scope)).construct(a); + return zv::Val::adopt(scope); + } + + /* $this->scopeFactory->create(...$args); UNDEF = pending exception */ + zv::Val scopeFactoryCreate(CreateArgs &args) + { + zv::Ref factory = slot(PT_MS_PROP_SCOPE_FACTORY); + if (UNEXPECTED(!factory.isObject())) return uninitializedProperty("scopeFactory"); + return factoryCreate(factory.asObject(), args); + } + + /* the arguments every twin site passes from the slots unchanged; the + * dispatched getters are filled by the callers that use them. false = + * the twin's Error on a promoted property the constructor never wrote */ + bool fillFromSlots(CreateArgs &a) + { + if (UNEXPECTED(slot(PT_MS_PROP_CONTEXT).isUndef())) { + (void) uninitializedProperty("context"); + return false; + } + if (UNEXPECTED(slot(PT_MS_PROP_EXPRESSION_TYPES).isUndef() || slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS).isUndef())) { + (void) uninitializedProperty(slot(PT_MS_PROP_EXPRESSION_TYPES).isUndef() ? "expressionTypes" : "templateArgumentConstraints"); + return false; + } + a.set(CreateArgs::CONTEXT, slot(PT_MS_PROP_CONTEXT)); + a.set(CreateArgs::EXPRESSION_TYPES, slot(PT_MS_PROP_EXPRESSION_TYPES)); + a.set(CreateArgs::NATIVE_EXPRESSION_TYPES, slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES)); + a.set(CreateArgs::CONDITIONAL_EXPRESSIONS, slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS)); + a.set(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES)); + a.set(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, slot(PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION)); + a.set(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS, slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS)); + a.set(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS)); + a.set(CreateArgs::IN_FUNCTION_CALLS_STACK, slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK)); + a.set(CreateArgs::AFTER_EXTRACT_CALL, slot(PT_MS_PROP_AFTER_EXTRACT_CALL)); + a.set(CreateArgs::PARENT_SCOPE, slot(PT_MS_PROP_PARENT_SCOPE)); + a.set(CreateArgs::NATIVE_TYPES_PROMOTED, slot(PT_MS_PROP_NATIVE_TYPES_PROMOTED)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return true; + } + + /* $this->isDeclareStrictTypes(), $this->getFunction(), + * $this->getNamespace(), $this->isInFirstLevelStatement() — the four + * dispatched getters most sites pass; false = pending exception */ + [[nodiscard]] bool fillDispatched(CreateArgs &a, bool withFunction, bool withFirstLevel) + { + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return false; + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + if (withFunction) { + zv::Val function = thisGetFunction(); + if (UNEXPECTED(function.isUndef())) return false; + a.setOwned(CreateArgs::FUNCTION, std::move(function)); + } + zv::Val ns = thisGetNamespace(); + if (UNEXPECTED(ns.isUndef())) return false; + a.setOwned(CreateArgs::NAMESPACE_, std::move(ns)); + if (withFirstLevel) { + bool inFirstLevelStatement; + if (UNEXPECTED(!thisIsInFirstLevelStatement(inFirstLevelStatement))) return false; + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, inFirstLevelStatement); + } + return true; + } + + /* }}} */ + + /* {{{ __construct */ + + /* the 33 constructor arguments as zpp delivers them (NULL = a defaulted + * optional parameter) */ + struct ConstructArgs + { + zval *container, *scopeFactory, *reflectionProvider, *initializerExprTypeResolver, *expressionTypeResolverExtensions, *exprPrinter, *typeSpecifier, *propertyReflectionFinder, *parser, *constantResolver, *expressionResultStorageStack, *context, *phpVersion, *attributeReflectionFactory, *configuredPhpVersionRangeHelper, *nodeCallback; + bool declareStrictTypes; + zval *function; + zend_string *ns; + zval *expressionTypes, *nativeExpressionTypes, *conditionalExpressions, *inClosureBindScopeClasses, *anonymousFunctionReflection; + bool inFirstLevelStatement; + zval *currentlyAssignedExpressions, *currentlyAllowedUndefinedExpressions, *inFunctionCallsStack; + bool afterExtractCall; + zval *parentScope; + bool nativeTypesPromoted; + zval *templateArgumentFrame, *templateArgumentConstraints; + }; + + void construct(const ConstructArgs &a) + { + writeSlot(PT_MS_PROP_CONTAINER, zv::Val::copyOf(zv::Ref(a.container))); + writeSlot(PT_MS_PROP_SCOPE_FACTORY, zv::Val::copyOf(zv::Ref(a.scopeFactory))); + writeSlot(PT_MS_PROP_REFLECTION_PROVIDER, zv::Val::copyOf(zv::Ref(a.reflectionProvider))); + writeSlot(PT_MS_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, zv::Val::copyOf(zv::Ref(a.initializerExprTypeResolver))); + writeSlot(PT_MS_PROP_EXPRESSION_TYPE_RESOLVER_EXTENSIONS, zv::Val::copyOf(zv::Ref(a.expressionTypeResolverExtensions))); + writeSlot(PT_MS_PROP_EXPR_PRINTER, zv::Val::copyOf(zv::Ref(a.exprPrinter))); + writeSlot(PT_MS_PROP_TYPE_SPECIFIER, zv::Val::copyOf(zv::Ref(a.typeSpecifier))); + writeSlot(PT_MS_PROP_PROPERTY_REFLECTION_FINDER, zv::Val::copyOf(zv::Ref(a.propertyReflectionFinder))); + writeSlot(PT_MS_PROP_PARSER, zv::Val::copyOf(zv::Ref(a.parser))); + writeSlot(PT_MS_PROP_CONSTANT_RESOLVER, zv::Val::copyOf(zv::Ref(a.constantResolver))); + writeSlot(PT_MS_PROP_EXPRESSION_RESULT_STORAGE_STACK, zv::Val::copyOf(zv::Ref(a.expressionResultStorageStack))); + writeSlot(PT_MS_PROP_CONTEXT, zv::Val::copyOf(zv::Ref(a.context))); + writeSlot(PT_MS_PROP_PHP_VERSION, zv::Val::copyOf(zv::Ref(a.phpVersion))); + writeSlot(PT_MS_PROP_ATTRIBUTE_REFLECTION_FACTORY, zv::Val::copyOf(zv::Ref(a.attributeReflectionFactory))); + writeSlot(PT_MS_PROP_CONFIGURED_PHP_VERSION_RANGE_HELPER, zv::Val::copyOf(zv::Ref(a.configuredPhpVersionRangeHelper))); + writeSlot(PT_MS_PROP_NODE_CALLBACK, optional(a.nodeCallback)); + writeSlot(PT_MS_PROP_DECLARE_STRICT_TYPES, zv::Val::boolean(a.declareStrictTypes)); + writeSlot(PT_MS_PROP_FUNCTION, optional(a.function)); + /* if ($namespace === '') { $namespace = null; } $this->namespace = $namespace; */ + writeSlot(PT_MS_PROP_NAMESPACE, a.ns == NULL || ZSTR_LEN(a.ns) == 0 ? zv::Val::null() : zv::Val::string(a.ns)); + writeSlot(PT_MS_PROP_EXPRESSION_TYPES, optionalArray(a.expressionTypes)); + writeSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, optionalArray(a.nativeExpressionTypes)); + writeSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, optionalArray(a.conditionalExpressions)); + writeSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, optionalArray(a.inClosureBindScopeClasses)); + writeSlot(PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION, optional(a.anonymousFunctionReflection)); + writeSlot(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, zv::Val::boolean(a.inFirstLevelStatement)); + writeSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, optionalArray(a.currentlyAssignedExpressions)); + writeSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, optionalArray(a.currentlyAllowedUndefinedExpressions)); + writeSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, optionalArray(a.inFunctionCallsStack)); + writeSlot(PT_MS_PROP_AFTER_EXTRACT_CALL, zv::Val::boolean(a.afterExtractCall)); + writeSlot(PT_MS_PROP_PARENT_SCOPE, optional(a.parentScope)); + writeSlot(PT_MS_PROP_NATIVE_TYPES_PROMOTED, zv::Val::boolean(a.nativeTypesPromoted)); + writeSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME, optional(a.templateArgumentFrame)); + writeSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, optional(a.templateArgumentConstraints)); + } + + static zv::Val optional(zval *value) { return value == NULL ? zv::Val::null() : zv::Val::copyOf(zv::Ref(value)); } + static zv::Val optionalArray(zval *value) { return value == NULL ? zv::Val(zv::Arr::empty()) : zv::Val::copyOf(zv::Ref(value)); } + + /* }}} */ + + zv::Val toNodeCallbackScope() + { + zv::Ref memo = slot(PT_MS_PROP_NODE_CALLBACK_SCOPE); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + zv::Ref factory = slot(PT_MS_PROP_SCOPE_FACTORY); + if (UNEXPECTED(!factory.isObject())) return uninitializedProperty("scopeFactory"); + zv::Val nodeCallbackScopeFactory = pt_type_call(factory.asObject(), PT_LC("tonodecallbackscopefactory"), 0, NULL); + if (UNEXPECTED(nodeCallbackScopeFactory.isUndef())) return zv::Val(); + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(nodeCallbackScopeFactory.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function create() on %s", zend_zval_value_name(nodeCallbackScopeFactory.raw())); + return zv::Val(); + } + zv::Val nodeCallbackScope = factoryCreate(Z_OBJ_P(nodeCallbackScopeFactory.raw()), a); + if (UNEXPECTED(nodeCallbackScope.isUndef())) return zv::Val(); + bool isNodeCallbackScope; + if (UNEXPECTED(!isInstance(nodeCallbackScope.ref(), PT_CLASS_NODE_CALLBACK_SCOPE, isNodeCallbackScope))) return zv::Val(); + if (isNodeCallbackScope) { + zv::Val seeded = pt_type_call(Z_OBJ_P(nodeCallbackScope.raw()), PT_LC("seedwalkscope"), 1, thisZval()); + if (UNEXPECTED(seeded.isUndef())) return zv::Val(); + } + + writeSlot(PT_MS_PROP_NODE_CALLBACK_SCOPE, zv::Val::copyOf(nodeCallbackScope.ref())); + return nodeCallbackScope; + } + + zv::Val toWalkScope() { return self_(); } + + /** @deprecated */ + zv::Val toMutatingScope() { return self_(); } + + zv::Val self_() + { + zval z; + ZVAL_OBJ_COPY(&z, self); + return zv::Val::adopt(z); + } + + zv::Val getFile() { return contextFile(); } + + zv::Val getFileDescription() + { + zv::Val traitReflection = contextTraitReflection(); + if (UNEXPECTED(traitReflection.isUndef())) return zv::Val(); + if (traitReflection.isNull()) return thisGetFile(); + + zv::Val classReflection = contextClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getDisplayName() on %s", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + + zv::Val className = pt_type_call(Z_OBJ_P(classReflection.raw()), PT_LC("getdisplayname"), 0, NULL); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Val isAnonymous = pt_type_call(Z_OBJ_P(classReflection.raw()), PT_LC("isanonymous"), 0, NULL); + if (UNEXPECTED(isAnonymous.isUndef())) return zv::Val(); + if (!zend_is_true(isAnonymous.raw())) { + zend_string *name = zval_get_string(className.raw()); + className = zv::Val::adoptString(zend_strpprintf(0, "class %s", ZSTR_VAL(name))); + zend_string_release(name); + } + + /* $traitReflection = $this->context->getTraitReflection(); — read again, as the twin does */ + traitReflection = contextTraitReflection(); + if (UNEXPECTED(traitReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(traitReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getFileName() on %s", zend_zval_value_name(traitReflection.raw())); + return zv::Val(); + } + zv::Val fileName = pt_type_call(Z_OBJ_P(traitReflection.raw()), PT_LC("getfilename"), 0, NULL); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + if (fileName.isNull()) { + pt_throw_should_not_happen(); + return zv::Val(); + } + + zend_string *file = zval_get_string(fileName.raw()); + zend_string *name = zval_get_string(className.raw()); + zv::Val result = zv::Val::adoptString(zend_strpprintf(0, "%s (in context of %s)", ZSTR_VAL(file), ZSTR_VAL(name))); + zend_string_release(name); + zend_string_release(file); + return result; + } + + bool isDeclareStrictTypes() const { return slotBool(PT_MS_PROP_DECLARE_STRICT_TYPES); } + + zv::Val enterDeclareStrictTypes() + { + /* create($this->context, true, null, null, $this->expressionTypes, + * $this->nativeExpressionTypes, templateArgumentFrame: ..., + * templateArgumentConstraints: ...) — the skipped parameters at + * the interface's defaults */ + CreateArgs a; + a.set(CreateArgs::CONTEXT, slot(PT_MS_PROP_CONTEXT)); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, true); + a.setNull(CreateArgs::FUNCTION); + a.setNull(CreateArgs::NAMESPACE_); + a.set(CreateArgs::EXPRESSION_TYPES, slot(PT_MS_PROP_EXPRESSION_TYPES)); + a.set(CreateArgs::NATIVE_EXPRESSION_TYPES, slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES)); + a.setEmptyArray(CreateArgs::CONDITIONAL_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES); + a.setNull(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.setNull(CreateArgs::PARENT_SCOPE); + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, false); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + /* private; UNDEF = pending exception */ + zv::Val rememberConstructorExpressions(zv::Ref currentExpressionTypes) + { + bool hasCustomSerialization; + if (UNEXPECTED(!classHasCustomSerialization(hasCustomSerialization))) return zv::Val(); + bool rememberPropertyState = !hasCustomSerialization; + zv::Arr expressionTypes = zv::Arr::create(0); + for (auto entry : zv::ArrRef(currentExpressionTypes.raw())) { + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + bool is; + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_FUNC_CALL, is))) return zv::Val(); + if (is) { + zv::Ref name = nodeProp(Z_OBJ_P(expr.raw()), PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return zv::Val(); + bool isName; + if (UNEXPECTED(!isInstance(name.deref(), PT_CLASS_NAME, isName))) return zv::Val(); + if (!isName) continue; + /* interface_exists() etc. imply class_exists() therefore not listed here */ + zv::Ref functionName = nodeProp(name.deref().asObject(), PT_LC("name")); + if (UNEXPECTED(functionName.raw() == NULL)) return zv::Val(); + zv::Ref fn = functionName.deref(); + if (!fn.stringEquals("class_exists") && !fn.stringEquals("function_exists")) continue; + } else { + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_PROPERTY_FETCH, is))) return zv::Val(); + if (is) { + bool isReadonly = false; + if (!rememberPropertyState || !thisIsReadonlyPropertyFetch(expr.raw(), true, isReadonly)) { + if (UNEXPECTED(EG(exception))) return zv::Val(); + continue; + } + if (!isReadonly) continue; + } else { + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_PROPERTY_INITIALIZATION_EXPR, is))) return zv::Val(); + if (is) { + if (!rememberPropertyState) continue; + } else { + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_CONST_FETCH, is))) return zv::Val(); + if (!is) continue; + } + } + } + + zval copy; + ZVAL_COPY(©, entry.value().raw()); + pt_ht_update(expressionTypes.table(), entry.stringKeyOrNull(), entry.indexKey(), ©); + } + + zval *thisHolder = zend_hash_str_find(Z_ARRVAL_P(currentExpressionTypes.raw()), PT_LC("$this")); + if (thisHolder != NULL) { + zval copy; + ZVAL_COPY(©, thisHolder); + zend_hash_str_update(expressionTypes.table(), PT_LC("$this"), ©); + } + + return zv::Val(std::move(expressionTypes)); + } + + /* private; false = pending exception */ + [[nodiscard]] bool classHasCustomSerialization(bool &out) + { + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return false; + if (!inClass) { + out = false; + return true; + } + + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function hasNativeMethod() on %s", zend_zval_value_name(classReflection.raw())); + return false; + } + zend_object *reflection = Z_OBJ_P(classReflection.raw()); + /* self::CUSTOM_SERIALIZATION_METHODS */ + static const char *const methodNames[] = { "__sleep", "__serialize", "__unserialize" }; + for (const char *methodName : methodNames) { + bool has; + if (UNEXPECTED(!hasNativeMethod(reflection, methodName, has))) return false; + if (has) { + out = true; + return true; + } + } + + zval interfaceName; + ZVAL_STR(&interfaceName, zend_ce_serializable->name); + zv::Val implements = pt_type_call(reflection, PT_LC("implementsinterface"), 1, &interfaceName); + if (UNEXPECTED(implements.isUndef())) return false; + if (!zend_is_true(implements.raw())) { + out = false; + return true; + } + return hasNativeMethod(reflection, "unserialize", out); + } + + static bool hasNativeMethod(zend_object *classReflection, const char *methodName, bool &out) + { + zval name; + ZVAL_STRING(&name, methodName); + zv::Val result = pt_type_call(classReflection, PT_LC("hasnativemethod"), 1, &name); + zval_ptr_dtor(&name); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + zv::Val rememberConstructorScope() + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, false, false))) return zv::Val(); + a.setNull(CreateArgs::FUNCTION); + zv::Val expressionTypes = rememberConstructorExpressions(slot(PT_MS_PROP_EXPRESSION_TYPES)); + if (UNEXPECTED(expressionTypes.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::EXPRESSION_TYPES, std::move(expressionTypes)); + zv::Val nativeExpressionTypes = rememberConstructorExpressions(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES)); + if (UNEXPECTED(nativeExpressionTypes.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, std::move(nativeExpressionTypes)); + a.set(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slot(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + return scopeFactoryCreate(a); + } + + /** @internal called by ScopeOps; false = pending exception */ + [[nodiscard]] bool isReadonlyPropertyFetch(zend_object *exprObject, bool allowOnlyOnThis, bool &out) + { + zv::Ref phpVersion = slot(PT_MS_PROP_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) { + (void) uninitializedProperty("phpVersion"); + return false; + } + zv::Val supports = pt_type_call(phpVersion.asObject(), PT_LC("supportsreadonlyproperties"), 0, NULL); + if (UNEXPECTED(supports.isUndef())) return false; + if (!zend_is_true(supports.raw())) { + out = false; + return true; + } + + zend_class_entry *propertyFetchCe = pt_class(PT_CLASS_PROPERTY_FETCH); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *identifierCe = pt_class(PT_CLASS_IDENTIFIER); + if (UNEXPECTED(propertyFetchCe == NULL || variableCe == NULL || identifierCe == NULL)) return false; + /* the loop variable owns each $expr = $expr->var step */ + zval exprZv; + ZVAL_OBJ_COPY(&exprZv, exprObject); + zv::Val expr = zv::Val::adopt(exprZv); + while (Z_TYPE_P(expr.raw()) == IS_OBJECT && instanceof_function(Z_OBJCE_P(expr.raw()), propertyFetchCe)) { + zend_object *fetch = Z_OBJ_P(expr.raw()); + zv::Ref var = nodeProp(fetch, PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return false; + var = var.deref(); + if (var.instanceOf(variableCe)) { + if (allowOnlyOnThis) { + zv::Ref name = nodeProp(fetch, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + zv::Ref varName = nodeProp(var.asObject(), PT_LC("name")); + if (UNEXPECTED(varName.raw() == NULL)) return false; + varName = varName.deref(); + if (!name.deref().instanceOf(identifierCe) || !varName.isString() || !varName.stringEquals("this")) { + out = false; + return true; + } + } + } else if (!var.instanceOf(propertyFetchCe)) { + out = false; + return true; + } + + zv::Ref finder = slot(PT_MS_PROP_PROPERTY_REFLECTION_FINDER); + if (UNEXPECTED(!finder.isObject())) { + (void) uninitializedProperty("propertyReflectionFinder"); + return false; + } + zv::Args args{expr.raw(), self}; + zv::Val propertyReflection = pt_type_call(finder.asObject(), PT_LC("findpropertyreflectionfromnode"), 2, args); + if (UNEXPECTED(propertyReflection.isUndef())) return false; + if (propertyReflection.isNull()) { + out = false; + return true; + } + if (UNEXPECTED(Z_TYPE_P(propertyReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getNativeReflection() on %s", zend_zval_value_name(propertyReflection.raw())); + return false; + } + + zv::Val nativePropertyReflection = pt_type_call(Z_OBJ_P(propertyReflection.raw()), PT_LC("getnativereflection"), 0, NULL); + if (UNEXPECTED(nativePropertyReflection.isUndef())) return false; + if (nativePropertyReflection.isNull()) { + out = false; + return true; + } + if (UNEXPECTED(Z_TYPE_P(nativePropertyReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isReadOnly() on %s", zend_zval_value_name(nativePropertyReflection.raw())); + return false; + } + zv::Val isReadOnly = pt_type_call(Z_OBJ_P(nativePropertyReflection.raw()), PT_LC("isreadonly"), 0, NULL); + if (UNEXPECTED(isReadOnly.isUndef())) return false; + if (!zend_is_true(isReadOnly.raw())) { + out = false; + return true; + } + + expr = zv::Val::copyOf(var); + } + + out = true; + return true; + } + + /* false = pending exception */ + [[nodiscard]] bool isInClass(bool &out) + { + zv::Val classReflection = contextClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return false; + out = !classReflection.isNull(); + return true; + } + + bool isInTrait(bool &out) + { + zv::Val traitReflection = contextTraitReflection(); + if (UNEXPECTED(traitReflection.isUndef())) return false; + out = !traitReflection.isNull(); + return true; + } + + zv::Val getClassReflection() { return contextClassReflection(); } + zv::Val getTraitReflection() { return contextTraitReflection(); } + zv::Val getFunction() const { return copyOfSlot(PT_MS_PROP_FUNCTION); } + + zv::Val getFunctionName() + { + zv::Ref function = slot(PT_MS_PROP_FUNCTION); + if (function.isNull()) return zv::Val::null(); + if (UNEXPECTED(!function.isObject())) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(function.raw())); + return zv::Val(); + } + return pt_type_call(function.asObject(), PT_LC("getname"), 0, NULL); + } + + zv::Val getNamespace() const { return copyOfSlot(PT_MS_PROP_NAMESPACE); } + zv::Val getParentScope() const { return copyOfSlot(PT_MS_PROP_PARENT_SCOPE); } + + /* false = pending exception */ + [[nodiscard]] bool canAnyVariableExist(bool &out) + { + /* ($this->function === null && !$this->isInAnonymousFunction()) || $this->afterExtractCall */ + if (slot(PT_MS_PROP_FUNCTION).isNull()) { + bool inAnonymousFunction; + if (UNEXPECTED(!thisIsInAnonymousFunction(inAnonymousFunction))) return false; + if (!inAnonymousFunction) { + out = true; + return true; + } + } + out = slotBool(PT_MS_PROP_AFTER_EXTRACT_CALL); + return true; + } + + zv::Val afterExtractCall() + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + a.setEmptyArray(CreateArgs::CONDITIONAL_EXPRESSIONS); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, true); + return scopeFactoryCreate(a); + } + + /* the tables with $exprString dropped from both; the twin's + * `$expressionTypes = $this->expressionTypes; unset(...)` copy-on-write + * pair */ + struct TablePair + { + zv::Arr expressionTypes; + zv::Arr nativeExpressionTypes; + bool changed = false; + + explicit TablePair(const MutatingScope &scope) + : expressionTypes(zv::Arr::copyOfTable(Z_ARRVAL_P(scope.slot(PT_MS_PROP_EXPRESSION_TYPES).raw()))), + nativeExpressionTypes(zv::Arr::copyOfTable(Z_ARRVAL_P(scope.slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()))) + { + } + + void unset(zend_string *key) + { + expressionTypes.separate(); + zend_symtable_del(expressionTypes.table(), key); + nativeExpressionTypes.separate(); + zend_symtable_del(nativeExpressionTypes.table(), key); + } + + bool existsInEither(const char *key, size_t len) + { + return zend_hash_str_exists(expressionTypes.table(), key, len) + || zend_hash_str_exists(nativeExpressionTypes.table(), key, len); + } + }; + + /* create(...) with the twin's "everything from $this, the tables + * replaced" argument list; consumes the pair's tables */ + zv::Val createWithTables(TablePair &tables) + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + a.setOwned(CreateArgs::EXPRESSION_TYPES, zv::Val(std::move(tables.expressionTypes))); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Val(std::move(tables.nativeExpressionTypes))); + return scopeFactoryCreate(a); + } + + zv::Val afterClearstatcacheCall() + { + /* list from https://www.php.net/manual/en/function.clearstatcache.php */ + static const char *const functionNames[] = { + "stat", "lstat", "file_exists", "is_writable", "is_writeable", "is_readable", "is_executable", "is_file", "is_dir", "is_link", + "filectime", "fileatime", "filemtime", "fileinode", "filegroup", "fileowner", "filesize", "filetype", "fileperms", + }; + TablePair tables(*this); + /* foreach (array_keys($expressionTypes) as $exprString): the keys of + * the copy are the keys of the slot's table, walked there */ + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())) { + zend_string *exprString = entry.stringKeyOrNull(); + if (exprString == NULL) { + /* an integer key never starts with a function name */ + continue; + } + for (const char *functionName : functionNames) { + size_t len = strlen(functionName); + const char *s = ZSTR_VAL(exprString); + size_t n = ZSTR_LEN(exprString); + bool plain = n > len && memcmp(s, functionName, len) == 0 && s[len] == '('; + bool qualified = n > len + 1 && s[0] == '\\' && memcmp(s + 1, functionName, len) == 0 && s[len + 1] == '('; + if (!plain && !qualified) continue; + tables.unset(exprString); + tables.changed = true; + break; + } + } + + if (!tables.changed) return self_(); + return createWithTables(tables); + } + + zv::Val afterOpenSslCall(zend_string *openSslFunctionName) + { + TablePair tables(*this); + + if (!tables.existsInEither(PT_LC("\\openssl_error_string()"))) return self_(); + + static const char *const invalidating[] = { + "openssl_cipher_iv_length", "openssl_cms_decrypt", "openssl_cms_encrypt", "openssl_cms_read", "openssl_cms_sign", "openssl_cms_verify", + "openssl_csr_export_to_file", "openssl_csr_export", "openssl_csr_get_public_key", "openssl_csr_get_subject", "openssl_csr_new", "openssl_csr_sign", + "openssl_decrypt", "openssl_dh_compute_key", "openssl_digest", "openssl_encrypt", "openssl_get_curve_names", "openssl_get_privatekey", + "openssl_get_publickey", "openssl_open", "openssl_pbkdf2", "openssl_pkcs12_export_to_file", "openssl_pkcs12_export", "openssl_pkcs12_read", + "openssl_pkcs7_decrypt", "openssl_pkcs7_encrypt", "openssl_pkcs7_read", "openssl_pkcs7_sign", "openssl_pkcs7_verify", "openssl_pkey_derive", + "openssl_pkey_export_to_file", "openssl_pkey_export", "openssl_pkey_get_private", "openssl_pkey_get_public", "openssl_pkey_new", + "openssl_private_decrypt", "openssl_private_encrypt", "openssl_public_decrypt", "openssl_public_encrypt", "openssl_random_pseudo_bytes", + "openssl_seal", "openssl_sign", "openssl_spki_export_challenge", "openssl_spki_export", "openssl_spki_new", "openssl_spki_verify", + "openssl_verify", "openssl_x509_checkpurpose", "openssl_x509_export_to_file", "openssl_x509_export", "openssl_x509_fingerprint", + "openssl_x509_read", "openssl_x509_verify", + }; + for (const char *name : invalidating) { + if (zend_string_equals_cstr(openSslFunctionName, name, strlen(name))) { + zend_string *key = zend_string_init(PT_LC("\\openssl_error_string()"), 0); + tables.unset(key); + zend_string_release(key); + tables.changed = true; + break; + } + } + + if (!tables.changed) return self_(); + return createWithTables(tables); + } + + /* {{{ VolatileExpressionHelper::(...): the twin passes the + * two tables by reference — fresh references around the pair's + * arrays, read back after the call; -1 = pending exception, else the + * bool result */ + int volatileHelperCall(const char *lcname, size_t len, TablePair &tables, uint32_t extraArgc, zval *extraArgv, bool withThis) + { + zval argv[5]; + uint32_t argc = 0; + if (withThis) { + ZVAL_OBJ(&argv[argc++], self); + } + zval exprRef, nativeRef; + ZVAL_NEW_REF(&exprRef, tables.expressionTypes.raw()); + ZVAL_UNDEF(tables.expressionTypes.raw()); + ZVAL_NEW_REF(&nativeRef, tables.nativeExpressionTypes.raw()); + ZVAL_UNDEF(tables.nativeExpressionTypes.raw()); + ZVAL_COPY_VALUE(&argv[argc++], &exprRef); + ZVAL_COPY_VALUE(&argv[argc++], &nativeRef); + for (uint32_t i = 0; i < extraArgc; i++) { + ZVAL_COPY_VALUE(&argv[argc++], &extraArgv[i]); + } + zv::Val result = pt_type_call_static_ce(pt_ce_volatile_expression_helper, lcname, len, argc, argv); + /* the tables as the helper left them (unwrapped, the references dropped) */ + zval *exprInner = Z_REFVAL(exprRef); + zval *nativeInner = Z_REFVAL(nativeRef); + Z_TRY_ADDREF_P(exprInner); + Z_TRY_ADDREF_P(nativeInner); + tables.expressionTypes = zv::Arr::adoptVal(zv::Val::adopt(*exprInner)); + tables.nativeExpressionTypes = zv::Arr::adoptVal(zv::Val::adopt(*nativeInner)); + zval_ptr_dtor(&exprRef); + zval_ptr_dtor(&nativeRef); + if (UNEXPECTED(result.isUndef())) return -1; + return zend_is_true(result.raw()) ? 1 : 0; + } + /* }}} */ + + zv::Val invalidateVolatileExpressions() + { + TablePair tables(*this); + + int changed = volatileHelperCall(PT_LC("invalidatevolatilefunctioncalls"), tables, 0, NULL, false); + if (UNEXPECTED(changed < 0)) return zv::Val(); + int superglobals = volatileHelperCall(PT_LC("invalidatesuperglobals"), tables, 0, NULL, false); + if (UNEXPECTED(superglobals < 0)) return zv::Val(); + changed = superglobals || changed; + int existence = volatileHelperCall(PT_LC("invalidatenegativeexistencechecks"), tables, 0, NULL, true); + if (UNEXPECTED(existence < 0)) return zv::Val(); + changed = existence || changed; + + if (!changed) return self_(); + return createWithTables(tables); + } + + zv::Val invalidateExistenceCheckExpressions(zval *functionNames, zval *declaredSymbolName) + { + TablePair tables(*this); + + zv::Args extra{functionNames, declaredSymbolName}; + int changed = volatileHelperCall(PT_LC("invalidatenegativeexistencechecks"), tables, 2, extra, true); + if (UNEXPECTED(changed < 0)) return zv::Val(); + if (!changed) return self_(); + return createWithTables(tables); + } + + zv::Val hasVariableType(zend_string *variableName) { return pt_scope_ops_has_variable_type(thisZval(), variableName); } + + zv::Val getVariableType(zend_string *variableName) + { + zval nameZv; + ZVAL_STR(&nameZv, variableName); + zv::Val hasVariableTypeResult = thisHasVariableType(&nameZv); + if (UNEXPECTED(hasVariableTypeResult.isUndef())) return zv::Val(); + zend_long hasVariableType = pt_type_trinary_value(hasVariableTypeResult.raw()); + if (UNEXPECTED(hasVariableType < 0)) return zv::Val(); + + if (hasVariableType == PT_TRI_MAYBE) { + if (zend_string_equals_literal(variableName, "argc")) return pt_static_type_factory_argc(); + if (zend_string_equals_literal(variableName, "argv")) return pt_static_type_factory_argv(); + bool canAnyVariableExist; + if (UNEXPECTED(!thisCanAnyVariableExist(canAnyVariableExist))) return zv::Val(); + if (canAnyVariableExist) return newMixedType(false); + } + + if (hasVariableType == PT_TRI_NO) { + zv::Args args{self, variableName}; + zv::Val exception = pt_type_new(PT_CLASS_UNDEFINED_VARIABLE_EXCEPTION, 2, args); + if (UNEXPECTED(exception.isUndef())) return zv::Val(); + zval exceptionZv = exception.take(); + zend_throw_exception_object(&exceptionZv); + return zv::Val(); + } + + zv::Str varExprString = zv::Str::adopt(zend_strpprintf(0, "$%s", ZSTR_VAL(variableName))); + zval *holder = zend_symtable_find(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), varExprString.get()); + if (holder == NULL) { + if (isGlobalVariable(variableName)) { + /* new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)) */ + zval integer, string, mixed, benevolent, array; + if (UNEXPECTED(!pt_integer_type_new(&integer))) return zv::Val(); + if (UNEXPECTED(!pt_string_type_new(&string))) { + zval_ptr_dtor(&integer); + return zv::Val(); + } + zv::Arr members = zv::Arr::create(2); + members.push(zv::Val::adopt(integer)); + members.push(zv::Val::adopt(string)); + if (UNEXPECTED(!pt_benevolent_union_type_new(&benevolent, members.raw()))) return zv::Val(); + zv::Val benevolentVal = zv::Val::adopt(benevolent); + if (UNEXPECTED(!pt_mixed_type_new(&mixed, true))) return zv::Val(); + zv::Val mixedVal = zv::Val::adopt(mixed); + if (UNEXPECTED(!pt_array_type_new(&array, benevolentVal.raw(), mixedVal.raw()))) return zv::Val(); + return zv::Val::adopt(array); + } + return newMixedType(false); + } + + return holderType(zv::Ref(holder)); + } + + static zv::Val newMixedType(bool isExplicitMixed) + { + zval mixed; + if (UNEXPECTED(!pt_mixed_type_new(&mixed, isExplicitMixed))) return zv::Val(); + return zv::Val::adopt(mixed); + } + + /* getDefinedVariables() (certainty yes) / getMaybeDefinedVariables() + * (certainty maybe): one body, parametrized by the certainty value */ + zv::Val definedVariables(zend_long certainty) + { + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + if (UNEXPECTED(variableCe == NULL)) return zv::Val(); + zv::Arr variables = zv::Arr::create(0); + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())) { + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + if (!expr.ref().instanceOf(variableCe)) continue; + zend_long holderCertaintyValue = holderCertainty(entry.value()); + if (UNEXPECTED(holderCertaintyValue < 0)) return zv::Val(); + if (holderCertaintyValue != certainty) continue; + + /* substr($exprString, 1) */ + zend_string *exprString = entry.stringKeyOrNull(); + zv::Str owned; + if (exprString == NULL) { + owned = zv::Str::adopt(zend_long_to_str((zend_long) entry.indexKey())); + exprString = owned.get(); + } + variables.push(zv::Val::adoptString(zend_string_init(ZSTR_VAL(exprString) + 1, ZSTR_LEN(exprString) - 1, 0))); + } + + return zv::Val(std::move(variables)); + } + + zv::Val getDefinedVariables() { return definedVariables(PT_TRI_YES); } + zv::Val getMaybeDefinedVariables() { return definedVariables(PT_TRI_MAYBE); } + + /* {{{ findPossiblyImpureCallDescriptions(): the NodeFinder::findFirst() + * walks natively (pt_find_first_recursive visits the same pre-order), + * the filter closure's `$this->getNodeKey($node) === $key` in the + * matcher; the walker's failure flag lives in the embedded pt_find_ctx */ + struct KeyMatchCtx + { + pt_find_ctx base; + MutatingScope *scope; + zend_class_entry *exprCe; + zend_string *key; + }; + + static bool keyMatcher(zend_object *node, void *vctx) + { + KeyMatchCtx *ctx = (KeyMatchCtx *) vctx; + if (!instanceof_function(node->ce, ctx->exprCe)) return false; + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + zv::Val key = ctx->scope->thisGetNodeKey(&nodeZv); + if (UNEXPECTED(key.isUndef())) { + ctx->base.failed = true; + return false; + } + return Z_TYPE_P(key.raw()) == IS_STRING && zend_string_equals(Z_STR_P(key.raw()), ctx->key); + } + + /* $nodeFinder->findFirst([$expr], fn => getNodeKey($node) === $key); + * NULL for no match — with `failed` set when an exception is pending */ + zend_object *findFirstByKey(zend_object *expr, zend_string *key, bool &failed) + { + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) { + failed = true; + return NULL; + } + KeyMatchCtx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.scope = this; + ctx.exprCe = exprCe; + ctx.key = key; + zend_object *found = pt_find_first_recursive(expr, keyMatcher, &ctx); + failed = ctx.base.failed; + return found; + } + + /* array_values(array_unique($descriptions)) for a list of strings */ + static zv::Val uniqueValues(zv::Arr &descriptions) + { + zv::Arr result = zv::Arr::create(descriptions.arrRef().size()); + zv::ScratchTable seen(descriptions.arrRef().size()); + for (auto entry : descriptions.arrRef()) { + zend_string *description = Z_STR_P(entry.value().raw()); + if (zend_hash_exists(seen.table(), description)) continue; + zval marker; + ZVAL_TRUE(&marker); + zend_hash_add_new(seen.table(), description, &marker); + result.push(entry.value()); + } + return zv::Val(std::move(result)); + } + + /* the key of $holderExpr->callExpr / ->impactedExpr through + * $this->getNodeKey(); UNDEF = pending exception */ + zv::Val impureExprKey(zend_object *holderExpr, const char *prop, size_t len) + { + zv::Ref expr = nodeProp(holderExpr, prop, len); + if (UNEXPECTED(expr.raw() == NULL)) return zv::Val(); + return thisGetNodeKey(expr.deref().raw()); + } + + /* the string of a getNodeKey() result (an Error when a subclass + * returned something else) */ + static zend_string *keyString(zv::Val &key) + { + if (UNEXPECTED(Z_TYPE_P(key.raw()) != IS_STRING)) { + zend_type_error("MutatingScope::getNodeKey(): Return value must be of type string, %s returned", zend_zval_value_name(key.raw())); + return NULL; + } + return Z_STR_P(key.raw()); + } + + zv::Val findPossiblyImpureCallDescriptions(zend_object *expr) + { + zend_class_entry *impureCe = pt_class(PT_CLASS_POSSIBLY_IMPURE_CALL_EXPR); + if (UNEXPECTED(impureCe == NULL)) return zv::Val(); + zv::Arr callExprDescriptions = zv::Arr::create(0); + bool foundCallExprMatch = false; + zv::ScratchTable matchedCallExprKeys(0); + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())) { + zv::Val holderExprVal = holderExpr(entry.value()); + if (UNEXPECTED(holderExprVal.isUndef())) return zv::Val(); + if (!holderExprVal.ref().instanceOf(impureCe)) continue; + zend_object *holderExprObject = Z_OBJ_P(holderExprVal.raw()); + + zv::Val callExprKeyVal = impureExprKey(holderExprObject, PT_LC("callExpr")); + if (UNEXPECTED(callExprKeyVal.isUndef())) return zv::Val(); + zend_string *callExprKey = keyString(callExprKeyVal); + if (UNEXPECTED(callExprKey == NULL)) return zv::Val(); + + bool failed; + zend_object *found = findFirstByKey(expr, callExprKey, failed); + if (UNEXPECTED(failed)) return zv::Val(); + if (found == NULL) continue; + + foundCallExprMatch = true; + zval marker; + ZVAL_TRUE(&marker); + zend_symtable_update(matchedCallExprKeys.table(), callExprKey, &marker); + + /* Only show the tip when the scope's type for the call expression + * differs from the declared return type, meaning control flow + * narrowing affected the type (the cached value was narrowed). */ + zval foundZv; + ZVAL_OBJ(&foundZv, found); + zv::Val scopeType = thisGetType(&foundZv); + if (UNEXPECTED(scopeType.isUndef())) return zv::Val(); + zv::Val declaredReturnType = holderType(entry.value()); + if (UNEXPECTED(declaredReturnType.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(scopeType.raw()) != IS_OBJECT || Z_TYPE_P(declaredReturnType.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isSuperTypeOf() on a non-object"); + return zv::Val(); + } + bool declaredAccepts, scopeAccepts; + if (UNEXPECTED(!isSuperTypeOfYes(Z_OBJ_P(declaredReturnType.raw()), scopeType.raw(), declaredAccepts))) return zv::Val(); + if (declaredAccepts) { + if (UNEXPECTED(!isSuperTypeOfYes(Z_OBJ_P(scopeType.raw()), declaredReturnType.raw(), scopeAccepts))) return zv::Val(); + if (scopeAccepts) continue; + } + + zv::Val description = pt_type_call(holderExprObject, PT_LC("getcalldescription"), 0, NULL); + if (UNEXPECTED(description.isUndef())) return zv::Val(); + callExprDescriptions.push(std::move(description)); + } + + /* If the first pass found a callExpr in the error expression but + * filtered it out (return type wasn't narrowed), the error is + * explained by the return type alone - skip the fallback. */ + if (foundCallExprMatch && callExprDescriptions.arrRef().size() == 0) return pt_op_empty_array(); + + /* Second pass: match by impactedExpr for cases where a maybe-impure method + * on an object didn't invalidate it, but a different method's return + * value was narrowed on that object. + * Skip when the expression itself is a direct method/static call - + * those are passed by ImpossibleCheckType rules where the error is + * about the call's arguments, not about object state. */ + zend_class_entry *methodCallCe = pt_class(PT_CLASS_METHOD_CALL); + zend_class_entry *staticCallCe = pt_class(PT_CLASS_STATIC_CALL); + if (UNEXPECTED(methodCallCe == NULL || staticCallCe == NULL)) return zv::Val(); + if (!(instanceof_function(expr->ce, methodCallCe) || instanceof_function(expr->ce, staticCallCe))) { + zv::Arr impactedExprDescriptions = zv::Arr::create(0); + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())) { + zv::Val holderExprVal = holderExpr(entry.value()); + if (UNEXPECTED(holderExprVal.isUndef())) return zv::Val(); + if (!holderExprVal.ref().instanceOf(impureCe)) continue; + zend_object *holderExprObject = Z_OBJ_P(holderExprVal.raw()); + + zv::Val impactedExprKeyVal = impureExprKey(holderExprObject, PT_LC("impactedExpr")); + if (UNEXPECTED(impactedExprKeyVal.isUndef())) return zv::Val(); + zend_string *impactedExprKey = keyString(impactedExprKeyVal); + if (UNEXPECTED(impactedExprKey == NULL)) return zv::Val(); + + /* Skip if impactedExpr is the same as callExpr (function calls) */ + zv::Val callExprKeyVal = impureExprKey(holderExprObject, PT_LC("callExpr")); + if (UNEXPECTED(callExprKeyVal.isUndef())) return zv::Val(); + zend_string *callExprKey = keyString(callExprKeyVal); + if (UNEXPECTED(callExprKey == NULL)) return zv::Val(); + if (zend_string_equals(impactedExprKey, callExprKey)) continue; + + /* Skip if this entry's callExpr was already matched in the first pass + * ($callExprKey = $this->getNodeKey($holderExpr->callExpr) again) */ + zv::Val callExprKeyAgain = impureExprKey(holderExprObject, PT_LC("callExpr")); + if (UNEXPECTED(callExprKeyAgain.isUndef())) return zv::Val(); + zend_string *callExprKey2 = keyString(callExprKeyAgain); + if (UNEXPECTED(callExprKey2 == NULL)) return zv::Val(); + if (zend_symtable_exists(matchedCallExprKeys.table(), callExprKey2)) continue; + + bool failed; + zend_object *found = findFirstByKey(expr, impactedExprKey, failed); + if (UNEXPECTED(failed)) return zv::Val(); + if (found == NULL) continue; + + zv::Val description = pt_type_call(holderExprObject, PT_LC("getcalldescription"), 0, NULL); + if (UNEXPECTED(description.isUndef())) return zv::Val(); + impactedExprDescriptions.push(std::move(description)); + } + + /* Prefer impactedExpr matches (intermediate calls that could have + * invalidated the object) over callExpr matches */ + if (impactedExprDescriptions.arrRef().size() > 0) return uniqueValues(impactedExprDescriptions); + } + + if (callExprDescriptions.arrRef().size() > 0) return uniqueValues(callExprDescriptions); + + return pt_op_empty_array(); + } + + /* $a->isSuperTypeOf($b)->yes(); false = pending exception */ + [[nodiscard]] static bool isSuperTypeOfYes(zend_object *a, zval *b, bool &out) + { + zv::Val result = pt_type_op(a, PT_OP_IS_SUPER_TYPE_OF, 1, b); + if (UNEXPECTED(result.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function yes() on %s", zend_zval_value_name(result.raw())); + return false; + } + zv::Val yes = pt_type_call(Z_OBJ_P(result.raw()), PT_LC("yes"), 0, NULL); + if (UNEXPECTED(yes.isUndef())) return false; + out = zend_is_true(yes.raw()); + return true; + } + + /* }}} */ + + /* private */ + static bool isGlobalVariable(zend_string *variableName) { return pt_is_superglobal_name(variableName); } + + /* false = pending exception */ + [[nodiscard]] bool hasConstant(zend_object *name, bool &out) + { + zv::Val nameString = pt_type_call(name, PT_LC("tostring"), 0, NULL); + if (UNEXPECTED(nameString.isUndef())) return false; + bool isCompilerHaltOffset = Z_TYPE_P(nameString.raw()) == IS_STRING && zend_string_equals_literal(Z_STR_P(nameString.raw()), "__COMPILER_HALT_OFFSET__"); + if (isCompilerHaltOffset) return fileHasCompilerHaltStatementCalls(out); + + zv::Val globalConstantType = getGlobalConstantType(name); + if (UNEXPECTED(globalConstantType.isUndef())) return false; + if (!globalConstantType.isNull()) { + out = true; + return true; + } + + zv::Ref reflectionProvider = slot(PT_MS_PROP_REFLECTION_PROVIDER); + if (UNEXPECTED(!reflectionProvider.isObject())) { + (void) uninitializedProperty("reflectionProvider"); + return false; + } + zv::Args args{name, self}; + zv::Val result = pt_type_call(reflectionProvider.asObject(), PT_LC("hasconstant"), 2, args); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* private; false = pending exception */ + [[nodiscard]] bool fileHasCompilerHaltStatementCalls(bool &out) + { + zv::Ref parser = slot(PT_MS_PROP_PARSER); + if (UNEXPECTED(!parser.isObject())) { + (void) uninitializedProperty("parser"); + return false; + } + zv::Val file = thisGetFile(); + if (UNEXPECTED(file.isUndef())) return false; + zv::Val nodes = pt_type_call(parser.asObject(), PT_LC("parsefile"), 1, file.raw()); + if (UNEXPECTED(nodes.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(nodes.raw()) != IS_ARRAY)) { + zend_type_error("foreach() argument must be of type array|object, %s given", zend_zval_value_name(nodes.raw())); + return false; + } + for (auto entry : zv::ArrRef(nodes.raw())) { + bool isHaltCompiler; + if (UNEXPECTED(!isInstance(entry.value().deref(), PT_CLASS_HALT_COMPILER, isHaltCompiler))) return false; + if (isHaltCompiler) { + out = true; + return true; + } + } + + out = false; + return true; + } + + bool isInAnonymousFunction() const { return !slot(PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION).isNull(); } + zv::Val getAnonymousFunctionReflection() const { return copyOfSlot(PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION); } + + zv::Val getAnonymousFunctionReturnType() + { + zv::Ref reflection = slot(PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION); + if (reflection.isNull()) return zv::Val::null(); + if (UNEXPECTED(!reflection.isObject())) { + zend_throw_error(NULL, "Call to a member function getReturnType() on %s", zend_zval_value_name(reflection.raw())); + return zv::Val(); + } + return pt_type_call(reflection.asObject(), PT_LC("getreturntype"), 0, NULL); + } + + zv::Val withAnonymousFunctionReflection(zval *anonymousFunctionReflection) + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + a.set(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, zv::Ref(anonymousFunctionReflection)); + return scopeFactoryCreate(a); + } + + /* {{{ the type resolution core (twin 1054–1808) */ + + /* {{{ collaborator reads shared by the type resolution core */ + + /* $this->expressionResultStorageStack->getCurrent(); UNDEF = pending + * exception, else null or the storage */ + zv::Val currentStorage() + { + zv::Ref stack = slot(PT_MS_PROP_EXPRESSION_RESULT_STORAGE_STACK); + if (UNEXPECTED(!stack.isObject())) return uninitializedProperty("expressionResultStorageStack"); + return pt_expression_result_storage_stack_current(stack.raw()); + } + + /* $storage->findExpressionResult($node) for the (non-null) result of + * currentStorage(); UNDEF = pending exception, else null or the result */ + static zv::Val storageFind(zv::Val &storage, zend_object *node) + { + if (UNEXPECTED(Z_TYPE_P(storage.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function findExpressionResult() on %s", zend_zval_value_name(storage.raw())); + return zv::Val(); + } + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + return pt_expression_result_storage_find(storage.raw(), &nodeZv); + } + + /* $storage !== null ? $storage->duplicate() : new ExpressionResultStorage() */ + static zv::Val onDemandStorage(zv::Val &storage) + { + if (storage.isNull()) return pt_expression_result_storage_new(); + if (UNEXPECTED(Z_TYPE_P(storage.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function duplicate() on %s", zend_zval_value_name(storage.raw())); + return zv::Val(); + } + return pt_expression_result_storage_duplicate(storage.raw()); + } + + /* $this->container->getByType($className); UNDEF = pending exception */ + zv::Val containerGetByType(const char *className, size_t len) + { + zv::Ref container = slot(PT_MS_PROP_CONTAINER); + if (UNEXPECTED(!container.isObject())) return uninitializedProperty("container"); + zval name; + ZVAL_STRINGL(&name, className, len); + zv::Val service = pt_type_call(container.asObject(), PT_LC("getbytype"), 1, &name); + zval_ptr_dtor(&name); + return service; + } + + /* the object of a getByType() / toWalkScope() result, an Error on + * anything else (a subclass returned a non-object) */ + static zend_object *requireObject(zv::Val &value, const char *methodName) + { + if (UNEXPECTED(Z_TYPE_P(value.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function %s() on %s", methodName, zend_zval_value_name(value.raw())); + return NULL; + } + return Z_OBJ_P(value.raw()); + } + + /* $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand($node, $scope, $storage) */ + zv::Val processExprOnDemand(zend_object *node, zval *scope, zv::Val storage) + { + zv::Val resolver = containerGetByType(PT_LC("PHPStan\\Analyser\\NodeScopeResolver")); + if (UNEXPECTED(resolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(resolver, "processExprOnDemand"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Args args{node, scope, storage.raw()}; + return pt_type_call(resolverObject, PT_LC("processexprondemand"), 3, args); + } + + /* $scope->nativeTypesPromoted of any scope object (the walk scope a + * subclass's toWalkScope() returns need not be this class): the slot + * of a native scope, the property table of anything else; false = + * pending exception */ + static bool scopeNativeTypesPromoted(zend_object *scope, bool &out) + { + if (pt_ce_mutating_scope != NULL && instanceof_function(scope->ce, pt_ce_mutating_scope)) { + zv::Ref value = zv::ObjRef(scope).propAt(PT_MS_PROP_NATIVE_TYPES_PROMOTED); + if (UNEXPECTED(value.isUndef())) { + (void) uninitializedProperty("nativeTypesPromoted"); + return false; + } + out = value.isTrue(); + return true; + } + zv::Ref value = zv::ObjRef(scope).prop(PT_LC("nativeTypesPromoted")); + if (UNEXPECTED(value.raw() == NULL)) { + zend_error(E_WARNING, "Undefined property: %s::$nativeTypesPromoted", ZSTR_VAL(scope->ce->name)); + out = false; + return EG(exception) == NULL; + } + if (UNEXPECTED(value.isUndef())) { + zend_throw_error(NULL, "Typed property %s::$nativeTypesPromoted must not be accessed before initialization", ZSTR_VAL(scope->ce->name)); + return false; + } + out = zend_is_true(value.deref().raw()); + return true; + } + + /* $result->getTypeOnScope($scope, $scope->nativeTypesPromoted) — the + * property read again at the call, as the twin spells it */ + static zv::Val typeOnScope(zend_object *result, zval *scope) + { + bool promoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(Z_OBJ_P(scope), promoted))) return zv::Val(); + zv::Args args{scope, promoted}; + return pt_type_call(result, PT_LC("gettypeonscope"), 2, args); + } + + /* a promoted slot the constructor never wrote: the twin's Error */ + bool requireSlot(uint32_t index, const char *name) + { + if (UNEXPECTED(slot(index).isUndef())) { + (void) uninitializedProperty(name); + return false; + } + return true; + } + + /* $holder->getExpr() instanceof PossiblyImpureCallExpr; false = pending exception */ + [[nodiscard]] static bool holderExprIsPossiblyImpureCall(zv::Ref holder, bool &out) + { + zv::Val expr = holderExpr(holder); + if (UNEXPECTED(expr.isUndef())) return false; + return isInstance(expr.ref(), PT_CLASS_POSSIBLY_IMPURE_CALL_EXPR, out); + } + + /* $a->equals($b): the native holders' body when both are native, the + * method of anything else; false = pending exception */ + [[nodiscard]] static bool holderEquals(zv::Ref a, zv::Ref b, bool &out) + { + a = a.deref(); + b = b.deref(); + if (UNEXPECTED(!a.isObject())) { + zend_throw_error(NULL, "Call to a member function equals() on %s", zend_zval_value_name(a.raw())); + return false; + } + if (EXPECTED(a.asObject()->ce == pt_ce_expr_type_holder && b.isObject() && b.asObject()->ce == pt_ce_expr_type_holder)) { + return pt_holder_equals(a.raw(), b.raw(), &out); + } + zv::Val result = pt_type_call(a.asObject(), PT_LC("equals"), 1, b.raw()); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* $a === $b for two objects (false for anything else, as === is) */ + static bool sameObject(zv::Ref a, zv::Ref b) + { + a = a.deref(); + b = b.deref(); + return a.isObject() && b.isObject() && a.asObject() == b.asObject(); + } + + /* $table[$key] for the key of a foreach entry over another table (a + * string key is never numeric in a PHP array — plain hash lookup) */ + static zval *findByEntryKey(HashTable *table, const zv::ArrayEntry &entry) + { + zend_string *key = entry.stringKeyOrNull(); + return key != NULL ? zend_hash_find(table, key) : zend_hash_index_find(table, entry.indexKey()); + } + + /* isset($table[$key]) for such a key */ + static bool issetByEntryKey(HashTable *table, const zv::ArrayEntry &entry) + { + zval *found = findByEntryKey(table, entry); + if (found == NULL) return false; + ZVAL_DEREF(found); + return Z_TYPE_P(found) != IS_NULL; + } + + /* $table[$key] = $value / unset($table[$key]) for such a key */ + static void updateByEntryKey(HashTable *table, const zv::ArrayEntry &entry, zval *value) + { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + zend_hash_update(table, key, value); + } else { + zend_hash_index_update(table, entry.indexKey(), value); + } + } + + static void deleteByEntryKey(HashTable *table, const zv::ArrayEntry &entry) + { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + zend_hash_del(table, key); + } else { + zend_hash_index_del(table, entry.indexKey()); + } + } + + /* the string of a foreach key handed to a string parameter of a + * private static helper: an integer key is the twin's strict_types + * TypeError */ + static zend_string *entryKeyString(const zv::ArrayEntry &entry, const char *method, const char *parameter) + { + zend_string *key = entry.stringKeyOrNull(); + if (UNEXPECTED(key == NULL)) { + zend_type_error("PHPStan\\Analyser\\MutatingScope::%s(): Argument #1 ($%s) must be of type string, int given", method, parameter); + return NULL; + } + return key; + } + + /* $array === ['static'] / $array === [$className]: one entry at key 0 + * holding that string */ + static bool isSingleStringList(HashTable *array, const char *value, size_t len) + { + if (zend_hash_num_elements(array) != 1) return false; + zval *first = zend_hash_index_find(array, 0); + return first != NULL && Z_TYPE_P(first) == IS_STRING && zend_string_equals_cstr(Z_STR_P(first), value, len); + } + + /* $this->inClosureBindScopeClasses[0] as a string return value: the + * twin's undefined-offset warning then the return-type TypeError when + * the entry is missing; NULL = pending exception */ + [[nodiscard]] static zend_string *firstBindScopeClass(HashTable *array, const char *method) + { + zval *first = zend_hash_index_find(array, 0); + if (UNEXPECTED(first == NULL)) { + zend_error(E_WARNING, "Undefined array key 0"); + if (EG(exception)) return NULL; + zend_type_error("PHPStan\\Analyser\\MutatingScope::%s(): Return value must be of type string, null returned", method); + return NULL; + } + ZVAL_DEREF(first); + if (UNEXPECTED(Z_TYPE_P(first) != IS_STRING)) { + zend_type_error("PHPStan\\Analyser\\MutatingScope::%s(): Return value must be of type string, %s returned", method, zend_zval_value_name(first)); + return NULL; + } + return Z_STR_P(first); + } + + /* $expr instanceof Variable && is_string($expr->name); false = pending exception */ + [[nodiscard]] static bool isVariableWithStringName(zend_object *expr, bool &out) + { + zend_class_entry *variableCe = pt_class_loaded(PT_CLASS_VARIABLE); + if (variableCe == NULL) { + out = false; + return EG(exception) == NULL; + } + if (!instanceof_function(expr->ce, variableCe)) { + out = false; + return true; + } + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + out = name.deref().isString(); + return true; + } + + /* $expr->name instanceof (Identifier / VarLikeIdentifier) */ + static bool nodeNameIs(zend_object *expr, int classIdx, bool &out) + { + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + return isInstance(name.deref(), classIdx, out); + } + + /* !$call->isFirstClassCallable() && $call->getArgs() === []; false = pending exception */ + [[nodiscard]] static bool isArgumentLessPlainCall(zend_object *call, bool &out) + { + zv::Val fcc = pt_type_call(call, PT_LC("isfirstclasscallable"), 0, NULL); + if (UNEXPECTED(fcc.isUndef())) return false; + if (zend_is_true(fcc.raw())) { + out = false; + return true; + } + zv::Val args = pt_type_call(call, PT_LC("getargs"), 0, NULL); + if (UNEXPECTED(args.isUndef())) return false; + out = Z_TYPE_P(args.raw()) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(args.raw())) == 0; + return true; + } + + /* throw new ShouldNotHappenException(sprintf('...%s on line %d...', get_class($node), $node->getStartLine())) */ + static void throwUnprocessedNode(zend_object *node, const char *format) + { + zv::Val line = pt_type_call(node, PT_LC("getstartline"), 0, NULL); + if (UNEXPECTED(line.isUndef())) return; + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (UNEXPECTED(ce == NULL)) return; + zend_throw_exception_ex(ce, 0, format, ZSTR_VAL(node->ce->name), (int) zval_get_long(line.raw())); + } + + /* }}} */ + + /* {{{ the NodeScopeResolver::$guard* diagnostics getType() and + * obtainResultForNode() read */ + + /* NodeScopeResolver::$; NULL = pending exception */ + [[nodiscard]] static zval *guardStatic(const char *name, size_t len) + { + zend_class_entry *ce = pt_class(PT_CLASS_NODE_SCOPE_RESOLVER); + if (UNEXPECTED(ce == NULL)) return NULL; + zval *value = zend_read_static_property(ce, name, len, 0); + if (UNEXPECTED(value == NULL)) return NULL; + ZVAL_DEREF(value); + return value; + } + + /* isset(NodeScopeResolver::$[$id]); false = pending exception */ + [[nodiscard]] static bool guardIdIsSet(const char *name, size_t len, zend_ulong id, bool &out) + { + zval *table = guardStatic(name, len); + if (UNEXPECTED(table == NULL)) return false; + out = false; + if (Z_TYPE_P(table) == IS_ARRAY) { + zval *found = zend_hash_index_find(Z_ARRVAL_P(table), id); + if (found != NULL) { + ZVAL_DEREF(found); + out = Z_TYPE_P(found) != IS_NULL; + } + } + return true; + } + + /* NodeScopeResolver::$guardNewWorld && isset($guardRealExprIds[id]) && + * !isset($guardProcessedExprIds[id]); false = pending exception */ + [[nodiscard]] static bool guardFires(zend_object *node, bool &fires) + { + fires = false; + zval *flag = guardStatic(PT_LC("guardNewWorld")); + if (UNEXPECTED(flag == NULL)) return false; + if (!zend_is_true(flag)) return true; + bool real; + if (UNEXPECTED(!guardIdIsSet(PT_LC("guardRealExprIds"), node->handle, real))) return false; + if (!real) return true; + bool processed; + if (UNEXPECTED(!guardIdIsSet(PT_LC("guardProcessedExprIds"), node->handle, processed))) return false; + fires = !processed; + return true; + } + + /* the nodes getType()'s guard exempts: a variable read is scope state + * and a literal is a constant */ + static bool isVariableOrLiteral(zend_object *node, bool &out) + { + if (UNEXPECTED(!isVariableWithStringName(node, out))) return false; + if (out) return true; + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + static const int literalClasses[] = { PT_CLASS_SCALAR_STRING, PT_CLASS_SCALAR_INT, PT_CLASS_SCALAR_FLOAT }; + for (int classIdx : literalClasses) { + if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), classIdx, out))) return false; + if (out) return true; + } + return true; + } + + /* }}} */ + + /** @api */ + zv::Val getType(zend_object *node) + { + bool fires; + if (UNEXPECTED(!guardFires(node, fires))) return zv::Val(); + if (UNEXPECTED(fires)) { + bool exempt; + if (UNEXPECTED(!isVariableOrLiteral(node, exempt))) return zv::Val(); + if (!exempt) { + throwUnprocessedNode(node, "getType() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node's ExpressionResult instead."); + return zv::Val(); + } + } + + zend_string *keyRaw = NULL; + zv::Val cached = pt_scope_ops_get_type_from_cache(thisZval(), node, &keyRaw); + if (UNEXPECTED(cached.isUndef())) return zv::Val(); + zv::Str key = zv::Str::adopt(keyRaw); + if (!cached.isNull()) return cached; + + zv::Val resolved = resolveType(key.get(), node); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + zv::Val type = pt_type_utils_resolve_late_resolvable_types(resolved.raw()); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + /* $this->resolvedTypes[$key] = $type */ + zval *table = OBJ_PROP_NUM(self, PT_MS_PROP_RESOLVED_TYPES); + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) { + zend_throw_error(NULL, "Cannot use a scalar value as an array"); + return zv::Val(); + } + SEPARATE_ARRAY(table); + zval copy; + ZVAL_COPY(©, type.raw()); + zend_symtable_update(Z_ARRVAL_P(table), key.get(), ©); + return type; + } + + zv::Val getScopeType(zval *expr) { return thisGetType(expr); } + + zv::Val getScopeNativeType(zval *expr) { return thisGetNativeType(expr); } + + /* getNodeKey() 1093 and getExprPrinter() 1099: below, out of the twin's file order */ + + /** @internal called by ScopeOps */ + zv::Val duplicateWith(zval *expressionTypes, zval *nativeExpressionTypes, zval *conditionalExpressions, zval *currentlyAssignedExpressions, zval *currentlyAllowedUndefinedExpressions, zval *inFunctionCallsStack, bool inFirstLevelStatement, bool afterExtractCall) + { + CreateArgs a; + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONTEXT, "context"))) return zv::Val(); + a.set(CreateArgs::CONTEXT, slot(PT_MS_PROP_CONTEXT)); + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + a.set(CreateArgs::EXPRESSION_TYPES, zv::Ref(expressionTypes)); + a.set(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Ref(nativeExpressionTypes)); + a.set(CreateArgs::CONDITIONAL_EXPRESSIONS, zv::Ref(conditionalExpressions)); + static const struct { uint32_t slot; uint32_t arg; const char *name; } fromSlots[] = { + { PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses" }, + { PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION, CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, "anonymousFunctionReflection" }, + }; + for (const auto &entry : fromSlots) { + if (UNEXPECTED(!requireSlot(entry.slot, entry.name))) return zv::Val(); + a.set(entry.arg, slot(entry.slot)); + } + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, inFirstLevelStatement); + a.set(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS, zv::Ref(currentlyAssignedExpressions)); + a.set(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, zv::Ref(currentlyAllowedUndefinedExpressions)); + a.set(CreateArgs::IN_FUNCTION_CALLS_STACK, zv::Ref(inFunctionCallsStack)); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, afterExtractCall); + static const struct { uint32_t slot; uint32_t arg; const char *name; } tailFromSlots[] = { + { PT_MS_PROP_PARENT_SCOPE, CreateArgs::PARENT_SCOPE, "parentScope" }, + { PT_MS_PROP_NATIVE_TYPES_PROMOTED, CreateArgs::NATIVE_TYPES_PROMOTED, "nativeTypesPromoted" }, + { PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME, CreateArgs::TEMPLATE_ARGUMENT_FRAME, "templateArgumentFrame" }, + { PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints" }, + }; + for (const auto &entry : tailFromSlots) { + if (UNEXPECTED(!requireSlot(entry.slot, entry.name))) return zv::Val(); + a.set(entry.arg, slot(entry.slot)); + } + return scopeFactoryCreate(a); + } + + /* $relevantRoots: NULL for null */ + zv::Val getClosureScopeCacheKey(zval *relevantRoots) + { + zval *cacheLevel = pt_verbosity_level_singleton(PT_VERBOSITY_LEVEL_CACHE); + if (UNEXPECTED(cacheLevel == NULL)) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + /* $parts, joined by "\n" as they are collected (implode) */ + smart_str parts = {}; + bool first = true; + auto separate = [&]() { + if (!first) { + smart_str_appendc(&parts, '\n'); + } + first = false; + }; + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())) { + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) { + smart_str_free(&parts); + return zv::Val(); + } + bool isVirtual; + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_VIRTUAL_NODE, isVirtual))) { + smart_str_free(&parts); + return zv::Val(); + } + if (isVirtual) continue; + zend_string *exprString = entry.stringKeyOrNull(); + if (relevantRoots != NULL) { + if (UNEXPECTED(exprString == NULL)) { + entryKeyString(entry, "exprStringIsRootedIn", "exprString"); + smart_str_free(&parts); + return zv::Val(); + } + bool rooted; + if (UNEXPECTED(!exprStringIsRootedIn(exprString, Z_ARRVAL_P(relevantRoots), rooted))) { + smart_str_free(&parts); + return zv::Val(); + } + if (!rooted) continue; + } + zv::Val type = holderType(entry.value()); + if (UNEXPECTED(type.isUndef())) { + smart_str_free(&parts); + return zv::Val(); + } + zv::Val description = describeAt(type, cacheLevel); + if (UNEXPECTED(description.isUndef())) { + smart_str_free(&parts); + return zv::Val(); + } + separate(); + if (exprString != NULL) { + smart_str_append(&parts, exprString); + } else { + smart_str_append_long(&parts, (zend_long) entry.indexKey()); + } + smart_str_appendl(&parts, "::", 2); + smart_str_append(&parts, Z_STR_P(description.raw())); + } + separate(); + smart_str_appendl(&parts, "---", 3); + + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) { + smart_str_free(&parts); + return zv::Val(); + } + zv::ArrRef stack(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw()); + separate(); + smart_str_appendc(&parts, ':'); + smart_str_append_long(&parts, (zend_long) stack.size()); + for (auto entry : stack) { + /* [, $parameter] */ + zv::Ref pair = entry.value().deref(); + zval *parameter = NULL; + if (pair.isArray()) { + parameter = zend_hash_index_find(Z_ARRVAL_P(pair.raw()), 1); + if (parameter == NULL) { + zend_error(E_WARNING, "Undefined array key 1"); + if (UNEXPECTED(EG(exception))) { + smart_str_free(&parts); + return zv::Val(); + } + } + } + separate(); + if (parameter == NULL || Z_TYPE_P(parameter) == IS_NULL) { + smart_str_appendl(&parts, ",null", 5); + continue; + } + ZVAL_DEREF(parameter); + if (UNEXPECTED(Z_TYPE_P(parameter) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getType() on %s", zend_zval_value_name(parameter)); + smart_str_free(&parts); + return zv::Val(); + } + zv::Val type = pt_type_call(Z_OBJ_P(parameter), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) { + smart_str_free(&parts); + return zv::Val(); + } + zv::Val description = describeAt(type, cacheLevel); + if (UNEXPECTED(description.isUndef())) { + smart_str_free(&parts); + return zv::Val(); + } + smart_str_appendc(&parts, ','); + smart_str_append(&parts, Z_STR_P(description.raw())); + } + smart_str_0(&parts); + + /* md5(implode("\n", $parts)) */ + PHP_MD5_CTX context; + unsigned char digest[16]; + char hex[33]; + PHP_MD5Init(&context); + if (parts.s != NULL) { + PHP_MD5Update(&context, (const unsigned char *) ZSTR_VAL(parts.s), ZSTR_LEN(parts.s)); + } + PHP_MD5Final(digest, &context); + make_digest_ex(hex, digest, sizeof(digest)); + smart_str_free(&parts); + return zv::Val::string(hex, 32); + } + + /* $type->describe($level) as a string; UNDEF = pending exception */ + static zv::Val describeAt(zv::Val &type, zval *level) + { + if (UNEXPECTED(Z_TYPE_P(type.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function describe() on %s", zend_zval_value_name(type.raw())); + return zv::Val(); + } + zv::Val description = pt_type_op(Z_OBJ_P(type.raw()), PT_OP_DESCRIBE, 1, level); + if (UNEXPECTED(description.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(description.raw()) != IS_STRING)) { + zend_string *converted = zval_get_string(description.raw()); + if (UNEXPECTED(EG(exception))) { + zend_string_release(converted); + return zv::Val(); + } + return zv::Val::adoptString(converted); + } + return description; + } + + /* private static; false = pending exception */ + [[nodiscard]] static bool exprStringIsRootedIn(zend_string *exprString, HashTable *roots, bool &out) + { + for (auto entry : zv::TableRef(roots)) { + zv::Ref root = entry.value().deref(); + if (root.isString() && zend_string_equals(exprString, root.asString())) { + out = true; + return true; + } + if (UNEXPECTED(!root.isString())) { + zend_type_error("str_starts_with(): Argument #2 ($needle) must be of type string, %s given", zend_zval_value_name(root.raw())); + return false; + } + zend_string *rootString = root.asString(); + if (ZSTR_LEN(exprString) < ZSTR_LEN(rootString) || memcmp(ZSTR_VAL(exprString), ZSTR_VAL(rootString), ZSTR_LEN(rootString)) != 0) continue; + /* $exprString[strlen($root)] — the strings differ, so the + * expression is strictly longer */ + unsigned char next = (unsigned char) ZSTR_VAL(exprString)[ZSTR_LEN(rootString)]; + if (next != '_' && !isalnum(next)) { + out = true; + return true; + } + } + out = false; + return true; + } + + /* private */ + zv::Val resolveType(zend_string *exprString, zend_object *node) + { + zv::Ref extensions = slot(PT_MS_PROP_EXPRESSION_TYPE_RESOLVER_EXTENSIONS); + if (UNEXPECTED(!extensions.isObject())) return uninitializedProperty("expressionTypeResolverExtensions"); + zv::Val all = pt_type_call(extensions.asObject(), PT_LC("getall"), 0, NULL); + if (UNEXPECTED(all.isUndef())) return zv::Val(); + if (EXPECTED(Z_TYPE_P(all.raw()) == IS_ARRAY)) { + zv::Args args{node, self}; + for (auto entry : zv::ArrRef(all.raw())) { + zv::Ref extension = entry.value().deref(); + if (UNEXPECTED(!extension.isObject())) { + zend_throw_error(NULL, "Call to a member function getType() on %s", zend_zval_value_name(extension.raw())); + return zv::Val(); + } + zv::Val type = pt_type_call(extension.asObject(), PT_LC("gettype"), 2, args); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (!type.isNull()) return type; + } + } else { + zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(all.raw())); + if (UNEXPECTED(EG(exception))) return zv::Val(); + } + + zv::Val expressionType = pt_scope_ops_expression_type_by_key(thisZval(), node, exprString); + if (UNEXPECTED(expressionType.isUndef())) return zv::Val(); + if (!expressionType.isNull()) return expressionType; + + /* NodeScopeResolver intercepts a first-class callable CallLike before + * the ExprHandler dispatch - no handler supports the original node, + * its closure type lives on the stored result's typeCallback */ + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + bool isCallLike; + if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), PT_CLASS_CALL_LIKE, isCallLike))) return zv::Val(); + if (isCallLike) { + zv::Val fcc = pt_type_call(node, PT_LC("isfirstclasscallable"), 0, NULL); + if (UNEXPECTED(fcc.isUndef())) return zv::Val(); + if (zend_is_true(fcc.raw())) return resolveTypeOfNewWorldHandlerNode(node); + } + + zv::Ref container = slot(PT_MS_PROP_CONTAINER); + if (UNEXPECTED(!container.isObject())) return uninitializedProperty("container"); + zv::Args resolveArgs{node, container.raw()}; + zv::Val exprHandler = pt_type_call_static(PT_CLASS_EXPR_HANDLER_REGISTRY, PT_LC("resolve"), 2, resolveArgs); + if (UNEXPECTED(exprHandler.isUndef())) return zv::Val(); + if (!exprHandler.isNull()) return resolveTypeOfNewWorldHandlerNode(node); + + return pt_type_new_mixed_type(); + } + + /* private */ + zv::Val resolveTypeOfNewWorldHandlerNode(zend_object *node) + { + /* the hooks are the boundary between the rule-facing world and the + * engine - a rule's NodeCallbackScope must not flow into result + * callbacks or on-demand processing */ + zv::Val scope = thisToWalkScope(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + if (UNEXPECTED(requireObject(scope, "toWalkScope") == NULL)) return zv::Val(); + zv::Val storage = currentStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + bool counterfactualAsk = false; + if (!storage.isNull()) { + zv::Val result = storageFind(storage, node); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (!result.isNull()) { + zend_object *resultObject = requireObject(result, "canResolveOwnType"); + if (UNEXPECTED(resultObject == NULL)) return zv::Val(); + zv::Val canResolve = pt_type_call(resultObject, PT_LC("canresolveowntype"), 0, NULL); + if (UNEXPECTED(canResolve.isUndef())) return zv::Val(); + if (zend_is_true(canResolve.raw())) { + /* a counterfactual ask must re-price the node on that + * scope - the memoized walk-position type answers a + * different question */ + bool promoted; + if (UNEXPECTED(!scopeNativeTypesPromoted(Z_OBJ_P(scope.raw()), promoted))) return zv::Val(); + zv::Args args{scope.raw(), promoted}; + zv::Val matches = pt_type_call(resultObject, PT_LC("askscopevariablestatematches"), 2, args); + if (UNEXPECTED(matches.isUndef())) return zv::Val(); + counterfactualAsk = !zend_is_true(matches.raw()); + if (!counterfactualAsk) return typeOnScope(resultObject, scope.raw()); + } + } + } + + /* A closure/arrow function type is computed directly - never by + * processing it on demand, which would re-enter + * ClosureHandler::processExpr() endlessly */ + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + bool isClosure; + if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), PT_CLASS_CLOSURE_EXPR, isClosure))) return zv::Val(); + if (!isClosure) { + if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), PT_CLASS_ARROW_FUNCTION, isClosure))) return zv::Val(); + } + if (isClosure) { + zv::Val closureTypeResolver = containerGetByType(PT_LC("PHPStan\\Analyser\\ExprHandler\\Helper\\ClosureTypeResolver")); + if (UNEXPECTED(closureTypeResolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(closureTypeResolver, "getClosureType"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Args args{scope.raw(), node, false, storage.raw()}; + return pt_type_call(resolverObject, PT_LC("getclosuretype"), 4, args); + } + + if (!counterfactualAsk && !storage.isNull()) { + zv::Val stored = storageFind(storage, node); + if (UNEXPECTED(stored.isUndef())) return zv::Val(); + if (!stored.isNull()) { + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (ce != NULL) { + zend_throw_exception_ex(ce, 0, "ExpressionResult of %s cannot resolve its own type (no eager type, no typeCallback).", ZSTR_VAL(node->ce->name)); + } + return zv::Val(); + } + } + + /* a synthetic node, or no analysis in progress */ + zv::Val resolver = containerGetByType(PT_LC("PHPStan\\Analyser\\NodeScopeResolver")); + if (UNEXPECTED(resolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(resolver, "processExprOnDemand"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Val onDemand = onDemandStorage(storage); + if (UNEXPECTED(onDemand.isUndef())) return zv::Val(); + zv::Args args{node, scope.raw(), onDemand.raw()}; + zv::Val onDemandResult = pt_type_call(resolverObject, PT_LC("processexprondemand"), 3, args); + if (UNEXPECTED(onDemandResult.isUndef())) return zv::Val(); + zend_object *onDemandObject = requireObject(onDemandResult, "getTypeOnScope"); + if (UNEXPECTED(onDemandObject == NULL)) return zv::Val(); + return typeOnScope(onDemandObject, scope.raw()); + } + + /* private; null (no analysis in progress) or [Type, Type] */ + zv::Val getCurrentTypesOfSpecifiedExpr(zend_object *expr) + { + zv::Val storage = currentStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + if (storage.isNull()) return zv::Val::null(); + + zv::Val exprResult = storageFind(storage, expr); + if (UNEXPECTED(exprResult.isUndef())) return zv::Val(); + bool narrowable; + if (UNEXPECTED(!isNarrowableSpecifiedExpr(expr, narrowable))) return zv::Val(); + if (narrowable) { + bool containsNullsafe = false; + if (!exprResult.isNull()) { + zend_object *resultObject = requireObject(exprResult, "containsNullsafe"); + if (UNEXPECTED(resultObject == NULL)) return zv::Val(); + zv::Val contains = pt_type_call(resultObject, PT_LC("containsnullsafe"), 0, NULL); + if (UNEXPECTED(contains.isUndef())) return zv::Val(); + containsNullsafe = zend_is_true(contains.raw()); + } + if (!containsNullsafe) { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_NATIVE_TYPES_PROMOTED, "nativeTypesPromoted"))) return zv::Val(); + zv::Val phpDoc = resolveScopeStateType(expr, slotBool(PT_MS_PROP_NATIVE_TYPES_PROMOTED)); + if (UNEXPECTED(phpDoc.isUndef())) return zv::Val(); + zv::Val native = resolveScopeStateType(expr, true); + if (UNEXPECTED(native.isUndef())) return zv::Val(); + return typePair(std::move(phpDoc), std::move(native)); + } + } + + if (exprResult.isNull()) { + /* a call subject (or a synthetic plain-chain variant) is priced + * on demand: one walk answers both flavours */ + zv::Val scope = thisToWalkScope(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + if (UNEXPECTED(requireObject(scope, "toWalkScope") == NULL)) return zv::Val(); + zv::Val duplicated = onDemandStorage(storage); + if (UNEXPECTED(duplicated.isUndef())) return zv::Val(); + zv::Val result = processExprOnDemand(expr, scope.raw(), std::move(duplicated)); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + zend_object *resultObject = requireObject(result, "getTypeOnScope"); + if (UNEXPECTED(resultObject == NULL)) return zv::Val(); + zv::Val phpDoc = typeOnScope(resultObject, scope.raw()); + if (UNEXPECTED(phpDoc.isUndef())) return zv::Val(); + zv::Args args{scope.raw(), true}; + zv::Val native = pt_type_call(resultObject, PT_LC("gettypeonscope"), 2, args); + if (UNEXPECTED(native.isUndef())) return zv::Val(); + return typePair(std::move(phpDoc), std::move(native)); + } + + /* a type tracked for the whole expression on the asking scope wins + * over the stored result's own type */ + zend_object *resultObject = requireObject(exprResult, "getTypeOnScope"); + if (UNEXPECTED(resultObject == NULL)) return zv::Val(); + zv::Val phpDoc = typeOnScope(resultObject, thisZval()); + if (UNEXPECTED(phpDoc.isUndef())) return zv::Val(); + zv::Args args{self, true}; + zv::Val native = pt_type_call(resultObject, PT_LC("gettypeonscope"), 2, args); + if (UNEXPECTED(native.isUndef())) return zv::Val(); + return typePair(std::move(phpDoc), std::move(native)); + } + + static zv::Val typePair(zv::Val phpDoc, zv::Val native) + { + zv::Arr pair = zv::Arr::create(2); + pair.push(std::move(phpDoc)); + pair.push(std::move(native)); + return zv::Val(std::move(pair)); + } + + /* a variable read, property/offset fetch, or an argument-less instance + * call — the shapes whose scope-view type derives from tracked state */ + static bool isNarrowableSpecifiedExpr(zend_object *expr, bool &out) + { + if (UNEXPECTED(!isVariableWithStringName(expr, out))) return false; + if (out) return true; + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + static const int fetchClasses[] = { PT_CLASS_PROPERTY_FETCH, PT_CLASS_ARRAY_DIM_FETCH, PT_CLASS_STATIC_PROPERTY_FETCH }; + for (int classIdx : fetchClasses) { + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), classIdx, out))) return false; + if (out) return true; + } + bool isMethodCall; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_METHOD_CALL, isMethodCall))) return false; + if (!isMethodCall) { + out = false; + return true; + } + bool nameIsIdentifier; + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_IDENTIFIER, nameIsIdentifier))) return false; + if (!nameIsIdentifier) { + out = false; + return true; + } + return isArgumentLessPlainCall(expr, out); + } + + /** @internal */ + zv::Val specifyTypesOfNewWorldHandlerNode(zend_object *node, zval *context) + { + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + zv::Val result = thisObtainResultForNode(&nodeZv); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + zend_object *resultObject = requireObject(result, "getSpecifiedTypesForScope"); + if (UNEXPECTED(resultObject == NULL)) return zv::Val(); + zv::Val scope = thisToWalkScope(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zv::Args args{scope.raw(), context}; + return pt_type_call(resultObject, PT_LC("getspecifiedtypesforscope"), 2, args); + } + + zv::Val obtainResultForNode(zend_object *node) + { + zv::Val scope = thisToWalkScope(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zv::Val storage = currentStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + if (!storage.isNull()) { + zv::Val result = storageFind(storage, node); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (!result.isNull()) return result; + } + + bool fires; + if (UNEXPECTED(!guardFires(node, fires))) return zv::Val(); + if (UNEXPECTED(fires)) { + throwUnprocessedNode(node, "obtainResultForNode() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node's ExpressionResult instead."); + return zv::Val(); + } + + /* a synthetic node, or no analysis in progress */ + zv::Val resolver = containerGetByType(PT_LC("PHPStan\\Analyser\\NodeScopeResolver")); + if (UNEXPECTED(resolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(resolver, "processExprOnDemand"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Val onDemand = onDemandStorage(storage); + if (UNEXPECTED(onDemand.isUndef())) return zv::Val(); + zv::Args args{node, scope.raw(), onDemand.raw()}; + return pt_type_call(resolverObject, PT_LC("processexprondemand"), 3, args); + } + + /* false = pending exception */ + [[nodiscard]] bool pushExpressionResultStorage(zval *storage) + { + zv::Ref stack = slot(PT_MS_PROP_EXPRESSION_RESULT_STORAGE_STACK); + if (UNEXPECTED(!stack.isObject())) { + (void) uninitializedProperty("expressionResultStorageStack"); + return false; + } + return !pt_type_call(stack.asObject(), PT_LC("push"), 1, storage).isUndef(); + } + + bool popExpressionResultStorage() + { + zv::Ref stack = slot(PT_MS_PROP_EXPRESSION_RESULT_STORAGE_STACK); + if (UNEXPECTED(!stack.isObject())) { + (void) uninitializedProperty("expressionResultStorageStack"); + return false; + } + return !pt_type_call(stack.asObject(), PT_LC("pop"), 0, NULL).isUndef(); + } + + /* protected: the settled stored result of the current storage - + * NodeCallbackScope's no-switch fast path */ + zv::Val findSettledStoredResult(zend_object *node) + { + zv::Val storage = currentStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + if (storage.isNull()) return zv::Val::null(); + return storageFind(storage, node); + } + + zv::Val getCurrentExpressionResultStorage() { return currentStorage(); } + + /* $frame: IS_NULL or the frame */ + zv::Val withTemplateArgumentFrame(zval *frame) + { + zv::Val scope = thisWithoutMemoizedTypes(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(scope.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Attempt to assign property \"templateArgumentFrame\" on %s", zend_zval_value_name(scope.raw())); + return zv::Val(); + } + /* $scope->templateArgumentFrame = $frame — the engine write path + * from the scope's own class (the property is protected) */ + zend_object *scopeObject = Z_OBJ_P(scope.raw()); + zend_update_property(scopeObject->ce, scopeObject, PT_LC("templateArgumentFrame"), frame); + if (UNEXPECTED(EG(exception))) return zv::Val(); + return scope; + } + + zv::Val getCurrentTemplateArgumentFrame() const { return copyOfSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME); } + + zv::Val getTemplateArgumentConstraints() const { return copyOfSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS); } + + /* $constraints: IS_NULL or the constraints */ + zv::Val withTemplateArgumentConstraints(zval *constraints) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) return zv::Val(); + zv::Ref current = slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS); + bool same = Z_TYPE_P(constraints) == IS_NULL + ? current.isNull() + : (current.isObject() && current.asObject() == Z_OBJ_P(constraints)); + if (same) return self_(); + /* $scope = clone $this */ + zend_object *clone = self->handlers->clone_obj(self); + if (UNEXPECTED(EG(exception))) { + if (clone != NULL) { + OBJ_RELEASE(clone); + } + return zv::Val(); + } + MutatingScope scope(clone); + scope.writeSlot(PT_MS_PROP_NODE_CALLBACK_SCOPE, zv::Val::null()); + scope.writeSlot(PT_MS_PROP_SCOPE_OUT_OF_FIRST_LEVEL_STATEMENT, zv::Val::null()); + scope.writeSlot(PT_MS_PROP_SCOPE_WITH_PROMOTED_NATIVE_TYPES, zv::Val::null()); + scope.writeSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, zv::Val::copyOf(zv::Ref(constraints))); + zval cloneZv; + ZVAL_OBJ(&cloneZv, clone); + return zv::Val::adopt(cloneZv); + } + + /* Inference facts join independently of variable-state convergence and branch termination. */ + zv::Val addTemplateArgumentConstraints(zval *constraints) + { + if (Z_TYPE_P(constraints) == IS_NULL) return self_(); + zv::Val isEmpty = pt_type_call(Z_OBJ_P(constraints), PT_LC("isempty"), 0, NULL); + if (UNEXPECTED(isEmpty.isUndef())) return zv::Val(); + if (zend_is_true(isEmpty.raw())) return self_(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) return zv::Val(); + zv::Ref current = slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS); + zv::Val merged; + if (current.isNull()) { + merged = zv::Val::copyOf(zv::Ref(constraints)); + } else { + if (UNEXPECTED(!current.isObject())) { + zend_throw_error(NULL, "Call to a member function merge() on %s", zend_zval_value_name(current.raw())); + return zv::Val(); + } + merged = pt_type_call(current.asObject(), PT_LC("merge"), 1, constraints); + if (UNEXPECTED(merged.isUndef())) return zv::Val(); + } + return thisWithTemplateArgumentConstraints(merged.raw()); + } + + /* A copy of this scope without its memoized type answers. */ + zv::Val withoutMemoizedTypes() + { + static const struct { uint32_t slot; const char *name; } tables[] = { + { PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes" }, + { PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes" }, + { PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions" }, + { PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions" }, + { PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions" }, + { PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack" }, + { PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, "inFirstLevelStatement" }, + { PT_MS_PROP_AFTER_EXTRACT_CALL, "afterExtractCall" }, + }; + zval args[8]; + for (uint32_t i = 0; i < 8; i++) { + if (UNEXPECTED(!requireSlot(tables[i].slot, tables[i].name))) return zv::Val(); + ZVAL_COPY_VALUE(&args[i], slot(tables[i].slot).raw()); + } + return thisDuplicateWith(args); + } + + /* The variables rooting the tracked expressions whose state differs + * between this scope and $other; null when a differing entry has no + * variable root. $other is an instance of this class (a subclass + * included) — its slots are read directly. */ + zv::Val getDifferingVariableRoots(zend_object *otherObject) + { + MutatingScope other(otherObject); + zv::Arr roots = zv::Arr::create(0); + static const struct { uint32_t slot; const char *name; } tables[] = { + { PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes" }, + { PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes" }, + }; + for (const auto &table : tables) { + if (UNEXPECTED(!requireSlot(table.slot, table.name) || !other.requireSlot(table.slot, table.name))) return zv::Val(); + HashTable *ours = Z_ARRVAL_P(slot(table.slot).raw()); + HashTable *theirs = Z_ARRVAL_P(other.slot(table.slot).raw()); + for (auto entry : zv::TableRef(ours)) { + bool impure; + if (UNEXPECTED(!holderExprIsPossiblyImpureCall(entry.value(), impure))) return zv::Val(); + if (impure) continue; + zval *theirHolder = findByEntryKey(theirs, entry); + if (theirHolder != NULL && Z_TYPE_P(theirHolder) != IS_NULL) { + bool equal = sameObject(entry.value(), zv::Ref(theirHolder)); + if (!equal && UNEXPECTED(!holderEquals(entry.value(), zv::Ref(theirHolder), equal))) return zv::Val(); + if (equal) continue; + } + bool noRoot; + if (UNEXPECTED(!addVariableRoot(roots, entry, noRoot))) return zv::Val(); + if (noRoot) return zv::Val::null(); + } + for (auto entry : zv::TableRef(theirs)) { + if (issetByEntryKey(ours, entry)) continue; + bool impure; + if (UNEXPECTED(!holderExprIsPossiblyImpureCall(entry.value(), impure))) return zv::Val(); + if (impure) continue; + bool noRoot; + if (UNEXPECTED(!addVariableRoot(roots, entry, noRoot))) return zv::Val(); + if (noRoot) return zv::Val::null(); + } + } + + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") || !other.requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) { + return zv::Val(); + } + HashTable *ourConditionals = Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()); + HashTable *theirConditionals = Z_ARRVAL_P(other.slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()); + const struct { HashTable *ours; HashTable *theirs; } conditionalTables[] = { + { ourConditionals, theirConditionals }, + { theirConditionals, ourConditionals }, + }; + for (const auto &table : conditionalTables) { + for (auto entry : zv::TableRef(table.ours)) { + zval *theirHolders = findByEntryKey(table.theirs, entry); + if (theirHolders != NULL && Z_TYPE_P(theirHolders) != IS_NULL && zend_is_identical(theirHolders, entry.value().raw())) continue; + zend_string *key = entryKeyString(entry, "getVariableRootOfExpressionKey", "key"); + if (UNEXPECTED(key == NULL)) return zv::Val(); + zv::Str root = getVariableRootOfExpressionKey(key); + if (root.isNull()) { + zv::Ref holders = entry.value().deref(); + if (UNEXPECTED(!holders.isArray())) { + zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(holders.raw())); + if (UNEXPECTED(EG(exception))) return zv::Val(); + continue; + } + for (auto holderEntry : zv::ArrRef(holders.raw())) { + zv::Ref holder = holderEntry.value().deref(); + if (UNEXPECTED(!holder.isObject())) { + zend_throw_error(NULL, "Call to a member function getTypeHolder() on %s", zend_zval_value_name(holder.raw())); + return zv::Val(); + } + zv::Val typeHolder = pt_type_call(holder.asObject(), PT_LC("gettypeholder"), 0, NULL); + if (UNEXPECTED(typeHolder.isUndef())) return zv::Val(); + bool impure; + if (UNEXPECTED(!holderExprIsPossiblyImpureCall(typeHolder.ref(), impure))) return zv::Val(); + if (!impure) return zv::Val::null(); + } + continue; + } + roots.set(root.get(), zv::Val::boolean(true)); + } + } + + /* array_keys($roots) */ + zv::Arr keys = zv::Arr::create(roots.arrRef().size()); + for (auto entry : roots.arrRef()) { + keys.push(zv::Val::string(entry.stringKey())); + } + return zv::Val(std::move(keys)); + } + + /* $root = self::getVariableRootOfExpressionKey($key); `noRoot` when + * it is null (the caller returns null), else $roots[$root] = true; + * false = pending exception */ + [[nodiscard]] static bool addVariableRoot(zv::Arr &roots, const zv::ArrayEntry &entry, bool &noRoot) + { + zend_string *key = entryKeyString(entry, "getVariableRootOfExpressionKey", "key"); + if (UNEXPECTED(key == NULL)) return false; + zv::Str root = getVariableRootOfExpressionKey(key); + noRoot = root.isNull(); + if (!noRoot) { + roots.set(root.get(), zv::Val::boolean(true)); + } + return true; + } + + /* private static: preg_match('/^\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)/'); NULL for no match */ + static zv::Str getVariableRootOfExpressionKey(zend_string *key) + { + const unsigned char *s = (const unsigned char *) ZSTR_VAL(key); + size_t n = ZSTR_LEN(key); + if (n < 2 || s[0] != '$') return zv::Str(); + unsigned char c = s[1]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c >= 0x80)) return zv::Str(); + size_t end = 2; + while (end < n) { + c = s[end]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c >= 0x80) { + end++; + } else { + break; + } + } + return zv::Str::adopt(zend_string_init((const char *) s + 1, end - 1, 0)); + } + + /* This scope after a statement whose recorded walk stands. */ + zv::Val withRecordedStatementDelta(zend_object *recordedEntryObject, zend_object *recordedExitObject) + { + MutatingScope recordedEntry(recordedEntryObject); + MutatingScope recordedExit(recordedExitObject); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") + || !recordedExit.requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") + || !recordedEntry.requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) { + return zv::Val(); + } + zv::Arr conditionalExpressions = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw())); + HashTable *entryConditionals = Z_ARRVAL_P(recordedEntry.slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()); + HashTable *exitConditionals = Z_ARRVAL_P(recordedExit.slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()); + for (auto entry : zv::TableRef(exitConditionals)) { + zval *entryHolders = findByEntryKey(entryConditionals, entry); + if (entryHolders != NULL && Z_TYPE_P(entryHolders) != IS_NULL && zend_is_identical(entryHolders, entry.value().raw())) continue; + conditionalExpressions.separate(); + zval copy; + ZVAL_COPY(©, entry.value().raw()); + updateByEntryKey(conditionalExpressions.table(), entry, ©); + } + for (auto entry : zv::TableRef(entryConditionals)) { + if (issetByEntryKey(exitConditionals, entry)) continue; + conditionalExpressions.separate(); + deleteByEntryKey(conditionalExpressions.table(), entry); + } + + static const struct { uint32_t slot; const char *name; } holderTables[] = { + { PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes" }, + { PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes" }, + }; + zv::Val deltas[2]; + for (uint32_t i = 0; i < 2; i++) { + if (UNEXPECTED(!requireSlot(holderTables[i].slot, holderTables[i].name) + || !recordedEntry.requireSlot(holderTables[i].slot, holderTables[i].name) + || !recordedExit.requireSlot(holderTables[i].slot, holderTables[i].name))) { + return zv::Val(); + } + deltas[i] = applyRecordedHolderDelta(slot(holderTables[i].slot), recordedEntry.slot(holderTables[i].slot), recordedExit.slot(holderTables[i].slot)); + if (UNEXPECTED(deltas[i].isUndef())) return zv::Val(); + } + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, "inFirstLevelStatement") || !recordedExit.requireSlot(PT_MS_PROP_AFTER_EXTRACT_CALL, "afterExtractCall"))) { + return zv::Val(); + } + + zval args[8]; + ZVAL_COPY_VALUE(&args[0], deltas[0].raw()); + ZVAL_COPY_VALUE(&args[1], deltas[1].raw()); + ZVAL_COPY_VALUE(&args[2], conditionalExpressions.raw()); + ZVAL_EMPTY_ARRAY(&args[3]); + ZVAL_EMPTY_ARRAY(&args[4]); + ZVAL_EMPTY_ARRAY(&args[5]); + ZVAL_BOOL(&args[6], slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + ZVAL_BOOL(&args[7], recordedExit.slotBool(PT_MS_PROP_AFTER_EXTRACT_CALL)); + return thisDuplicateWith(args); + } + + /* private static; the twin's by-value $current parameter: a copy of + * the table, written on first change */ + static zv::Val applyRecordedHolderDelta(zv::Ref current, zv::Ref recordedEntry, zv::Ref recordedExit) + { + zv::Arr result = zv::Arr::copyOfTable(Z_ARRVAL_P(current.raw())); + HashTable *entryTable = Z_ARRVAL_P(recordedEntry.raw()); + HashTable *exitTable = Z_ARRVAL_P(recordedExit.raw()); + for (auto entry : zv::TableRef(exitTable)) { + zval *entryHolder = findByEntryKey(entryTable, entry); + if (entryHolder != NULL && Z_TYPE_P(entryHolder) != IS_NULL) { + bool equal = sameObject(zv::Ref(entryHolder), entry.value()); + if (!equal && UNEXPECTED(!holderEquals(zv::Ref(entryHolder), entry.value(), equal))) return zv::Val(); + if (equal) continue; + } + result.separate(); + zval copy; + ZVAL_COPY(©, entry.value().raw()); + updateByEntryKey(result.table(), entry, ©); + } + for (auto entry : zv::TableRef(entryTable)) { + if (issetByEntryKey(exitTable, entry)) continue; + result.separate(); + deleteByEntryKey(result.table(), entry); + } + return zv::Val(std::move(result)); + } + + /** @api */ + zv::Val getNativeType(zval *expr) + { + zv::Val promoted = promoteNativeTypes(); + if (UNEXPECTED(promoted.isUndef())) return zv::Val(); + zend_object *promotedObject = requireObject(promoted, "getType"); + if (UNEXPECTED(promotedObject == NULL)) return zv::Val(); + return MutatingScope(promotedObject).thisGetType(expr); + } + + zv::Val getKeepVoidType(zend_object *node) + { + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + /* !Match_ && !Yield_ && !YieldFrom && ((!FuncCall && !MethodCall && + * !NullsafeMethodCall && !StaticCall) || isFirstClassCallable()) */ + bool plain = true; + static const int valueClasses[] = { PT_CLASS_MATCH, PT_CLASS_YIELD, PT_CLASS_YIELD_FROM }; + for (int classIdx : valueClasses) { + bool is; + if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), classIdx, is))) return zv::Val(); + if (is) { + plain = false; + break; + } + } + if (plain) { + static const int callClasses[] = { PT_CLASS_FUNC_CALL, PT_CLASS_METHOD_CALL, PT_CLASS_NULLSAFE_METHOD_CALL, PT_CLASS_STATIC_CALL }; + bool isCall = false; + for (int classIdx : callClasses) { + if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), classIdx, isCall))) return zv::Val(); + if (isCall) break; + } + if (isCall) { + zv::Val fcc = pt_type_call(node, PT_LC("isfirstclasscallable"), 0, NULL); + if (UNEXPECTED(fcc.isUndef())) return zv::Val(); + plain = zend_is_true(fcc.raw()); + } + } + if (plain) return getScopeStateType(node); + + zv::Val originalType = getScopeStateType(node); + if (UNEXPECTED(originalType.isUndef())) return zv::Val(); + bool containsNull; + if (UNEXPECTED(!pt_type_combinator_contains_null(originalType.raw(), containsNull))) return zv::Val(); + if (!containsNull) return originalType; + + /* the null may be a projected void: read the call's/match's raw + * (void-kept) own type */ + zv::Val storage = currentStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + zv::Val result = zv::Val::null(); + if (!storage.isNull()) { + result = storageFind(storage, node); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + } + if (result.isNull()) { + zv::Val resolver = containerGetByType(PT_LC("PHPStan\\Analyser\\NodeScopeResolver")); + if (UNEXPECTED(resolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(resolver, "processExprOnDemand"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Val scope = thisToWalkScope(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zv::Val onDemand = onDemandStorage(storage); + if (UNEXPECTED(onDemand.isUndef())) return zv::Val(); + zv::Args args{node, scope.raw(), onDemand.raw()}; + result = pt_type_call(resolverObject, PT_LC("processexprondemand"), 3, args); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + } + zend_object *resultObject = requireObject(result, "getKeepVoidType"); + if (UNEXPECTED(resultObject == NULL)) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_NATIVE_TYPES_PROMOTED, "nativeTypesPromoted"))) return zv::Val(); + zval promoted; + ZVAL_BOOL(&promoted, slotBool(PT_MS_PROP_NATIVE_TYPES_PROMOTED)); + return pt_type_call(resultObject, PT_LC("getkeepvoidtype"), 1, &promoted); + } + + zv::Val doNotTreatPhpDocTypesAsCertain() { return promoteNativeTypes(); } + + /* private */ + zv::Val promoteNativeTypes() + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_NATIVE_TYPES_PROMOTED, "nativeTypesPromoted"))) return zv::Val(); + if (slotBool(PT_MS_PROP_NATIVE_TYPES_PROMOTED)) return self_(); + + zv::Ref memo = slot(PT_MS_PROP_SCOPE_WITH_PROMOTED_NATIVE_TYPES); + if (!memo.isNull()) return zv::Val::copyOf(memo); + + /* create($this->context, $this->declareStrictTypes, $this->function, + * $this->namespace, $this->nativeExpressionTypes, [], [], ..., + * nativeTypesPromoted: true) — the slots, not the dispatched getters */ + CreateArgs a; + static const struct { uint32_t slot; uint32_t arg; const char *name; } fromSlots[] = { + { PT_MS_PROP_CONTEXT, CreateArgs::CONTEXT, "context" }, + { PT_MS_PROP_DECLARE_STRICT_TYPES, CreateArgs::DECLARE_STRICT_TYPES, "declareStrictTypes" }, + { PT_MS_PROP_FUNCTION, CreateArgs::FUNCTION, "function" }, + { PT_MS_PROP_NAMESPACE, CreateArgs::NAMESPACE_, "namespace" }, + { PT_MS_PROP_NATIVE_EXPRESSION_TYPES, CreateArgs::EXPRESSION_TYPES, "nativeExpressionTypes" }, + { PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses" }, + { PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION, CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, "anonymousFunctionReflection" }, + { PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, CreateArgs::IN_FIRST_LEVEL_STATEMENT, "inFirstLevelStatement" }, + { PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions" }, + { PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions" }, + { PT_MS_PROP_IN_FUNCTION_CALLS_STACK, CreateArgs::IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack" }, + { PT_MS_PROP_AFTER_EXTRACT_CALL, CreateArgs::AFTER_EXTRACT_CALL, "afterExtractCall" }, + { PT_MS_PROP_PARENT_SCOPE, CreateArgs::PARENT_SCOPE, "parentScope" }, + { PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME, CreateArgs::TEMPLATE_ARGUMENT_FRAME, "templateArgumentFrame" }, + { PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints" }, + }; + for (const auto &entry : fromSlots) { + if (UNEXPECTED(!requireSlot(entry.slot, entry.name))) return zv::Val(); + a.set(entry.arg, slot(entry.slot)); + } + a.setEmptyArray(CreateArgs::NATIVE_EXPRESSION_TYPES); + a.setEmptyArray(CreateArgs::CONDITIONAL_EXPRESSIONS); + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, true); + zv::Val created = scopeFactoryCreate(a); + if (UNEXPECTED(created.isUndef())) return zv::Val(); + writeSlot(PT_MS_PROP_SCOPE_WITH_PROMOTED_NATIVE_TYPES, zv::Val::copyOf(created.ref())); + return created; + } + + /** @api */ + zv::Val resolveName(zend_object *name) + { + /* (string) $name */ + zval nameZv; + ZVAL_OBJ(&nameZv, name); + zv::Str originalClass = zv::Str::adopt(zval_get_string(&nameZv)); + if (UNEXPECTED(EG(exception))) return zv::Val(); + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (inClass) { + zv::Str lowerClass = zv::Str::adopt(zend_string_tolower(originalClass.get())); + if (zend_string_equals_literal(lowerClass.get(), "self") || zend_string_equals_literal(lowerClass.get(), "static")) { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) return zv::Val(); + HashTable *bindScopeClasses = Z_ARRVAL_P(slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES).raw()); + if (zend_hash_num_elements(bindScopeClasses) != 0 && !isSingleStringList(bindScopeClasses, PT_LC("static"))) { + zend_string *first = firstBindScopeClass(bindScopeClasses, "resolveName"); + if (UNEXPECTED(first == NULL)) return zv::Val(); + return zv::Val::string(first); + } + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + zend_object *reflection = requireObject(classReflection, "getName"); + if (UNEXPECTED(reflection == NULL)) return zv::Val(); + return pt_class_reflection_get_name(reflection); + } else if (zend_string_equals_literal(lowerClass.get(), "parent")) { + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + zend_object *reflection = requireObject(classReflection, "getParentClass"); + if (UNEXPECTED(reflection == NULL)) return zv::Val(); + zv::Val parentClass = pt_type_call(reflection, PT_LC("getparentclass"), 0, NULL); + if (UNEXPECTED(parentClass.isUndef())) return zv::Val(); + if (!parentClass.isNull()) { + /* $currentClassReflection->getParentClass()->getName() — read again, as the twin does */ + parentClass = pt_type_call(reflection, PT_LC("getparentclass"), 0, NULL); + if (UNEXPECTED(parentClass.isUndef())) return zv::Val(); + zend_object *parent = requireObject(parentClass, "getName"); + if (UNEXPECTED(parent == NULL)) return zv::Val(); + return pt_class_reflection_get_name(parent); + } + } + } + + return zv::Val::string(originalClass.get()); + } + + /** @api */ + zv::Val resolveTypeByName(zend_object *name) + { + zv::Val lower = pt_type_call(name, PT_LC("tolowerstring"), 0, NULL); + if (UNEXPECTED(lower.isUndef())) return zv::Val(); + if (Z_TYPE_P(lower.raw()) == IS_STRING && zend_string_equals_literal(Z_STR_P(lower.raw()), "static")) { + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (inClass) { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) return zv::Val(); + HashTable *bindScopeClasses = Z_ARRVAL_P(slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES).raw()); + if (zend_hash_num_elements(bindScopeClasses) != 0 && !isSingleStringList(bindScopeClasses, PT_LC("static"))) { + zval *first = zend_hash_index_find(bindScopeClasses, 0); + zval nullZv; + ZVAL_NULL(&nullZv); + if (first == NULL) { + zend_error(E_WARNING, "Undefined array key 0"); + if (UNEXPECTED(EG(exception))) return zv::Val(); + first = &nullZv; + } + zv::Ref provider = slot(PT_MS_PROP_REFLECTION_PROVIDER); + if (UNEXPECTED(!provider.isObject())) return uninitializedProperty("reflectionProvider"); + bool hasClass; + if (UNEXPECTED(!pt_reflection_provider_has_class(provider.asObject(), first, hasClass))) return zv::Val(); + if (hasClass) { + zv::Val classReflection = pt_reflection_provider_get_class(provider.asObject(), first); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + return newStaticType(classReflection, "StaticType"); + } + } + + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + return newStaticType(classReflection, "StaticType"); + } + } + + zval nameZv; + ZVAL_OBJ(&nameZv, name); + zv::Val originalClass = thisResolveName(&nameZv); + if (UNEXPECTED(originalClass.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(originalClass.raw()) != IS_STRING)) { + zend_type_error("PHPStan\\Analyser\\MutatingScope::resolveName(): Return value must be of type string, %s returned", zend_zval_value_name(originalClass.raw())); + return zv::Val(); + } + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (inClass) { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) return zv::Val(); + HashTable *bindScopeClasses = Z_ARRVAL_P(slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES).raw()); + if (isSingleStringList(bindScopeClasses, ZSTR_VAL(Z_STR_P(originalClass.raw())), ZSTR_LEN(Z_STR_P(originalClass.raw())))) { + zv::Ref provider = slot(PT_MS_PROP_REFLECTION_PROVIDER); + if (UNEXPECTED(!provider.isObject())) return uninitializedProperty("reflectionProvider"); + bool hasClass; + if (UNEXPECTED(!pt_reflection_provider_has_class(provider.asObject(), originalClass.raw(), hasClass))) return zv::Val(); + if (hasClass) { + zv::Val classReflection = pt_reflection_provider_get_class(provider.asObject(), originalClass.raw()); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + return newThisType(classReflection); + } + return pt_type_new_object_type(originalClass.raw()); + } + + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + zv::Val thisType = newThisType(classReflection); + if (UNEXPECTED(thisType.isUndef())) return zv::Val(); + zv::Val ancestor = pt_type_op(Z_OBJ_P(thisType.raw()), PT_OP_GET_ANCESTOR_WITH_CLASS_NAME, 1, originalClass.raw()); + if (UNEXPECTED(ancestor.isUndef())) return zv::Val(); + if (!ancestor.isNull()) return ancestor; + } + + return pt_type_new_object_type(originalClass.raw()); + } + + /* new StaticType($classReflection) / new ThisType($classReflection): + * the constructors' TypeError on anything but a ClassReflection */ + static zv::Val newStaticType(zv::Val &classReflection, const char *className) + { + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_type_error("PHPStan\\Type\\%s::__construct(): Argument #1 ($classReflection) must be of type PHPStan\\Reflection\\ClassReflection, %s given", className, zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + zval out; + if (UNEXPECTED(!pt_static_type_new(&out, classReflection.raw()))) return zv::Val(); + return zv::Val::adopt(out); + } + + static zv::Val newThisType(zv::Val &classReflection) + { + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_type_error("PHPStan\\Type\\ThisType::__construct(): Argument #1 ($classReflection) must be of type PHPStan\\Reflection\\ClassReflection, %s given", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + zval out; + if (UNEXPECTED(!pt_this_type_new(&out, classReflection.raw()))) return zv::Val(); + return zv::Val::adopt(out); + } + + /** @api */ + static zv::Val getTypeFromValue(zval *value) { return pt_constant_type_helper_get_type_from_value(value); } + + /* }}} */ + + /* {{{ twin 1835-2011: the in-function-call stack, enterClass(), + * enterTrait() */ + + /* array_pop($array): drops the last entry and gives back the auto-index + * it took (the twin's `$stack = $this->inFunctionCallsStack; array_pop($stack)`) */ + static void arrayPop(zv::Arr &array) + { + if (zend_hash_num_elements(array.table()) == 0) return; + array.separate(); + HashTable *table = array.table(); + zend_string *lastKey = NULL; + zend_ulong lastIndex = 0; + for (auto entry : zv::TableRef(table)) { + lastKey = entry.stringKeyOrNull(); + lastIndex = entry.indexKey(); + } + if (lastKey != NULL) { + zend_hash_del(table, lastKey); + return; + } + if ((zend_long) lastIndex == table->nNextFreeElement - 1) { + table->nNextFreeElement = (zend_long) lastIndex; + } + zend_hash_index_del(table, lastIndex); + } + + /* $scope->resolvedTypes = $this->resolvedTypes — the public memo, written + * through the engine path from the result's own class (the factory + * answers with any MutatingScope); false = pending exception */ + [[nodiscard]] bool assignResolvedTypes(zv::Val &scope) + { + if (UNEXPECTED(Z_TYPE_P(scope.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Attempt to assign property \"resolvedTypes\" on %s", zend_zval_value_name(scope.raw())); + return false; + } + if (UNEXPECTED(!requireSlot(PT_MS_PROP_RESOLVED_TYPES, "resolvedTypes"))) return false; + zend_object *object = Z_OBJ_P(scope.raw()); + zend_update_property(object->ce, object, PT_LC("resolvedTypes"), slot(PT_MS_PROP_RESOLVED_TYPES).raw()); + return EXPECTED(EG(exception) == NULL); + } + + /* create(...) with everything from $this and the given call stack */ + zv::Val createWithFunctionCallStack(zv::Arr stack) + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + a.setOwned(CreateArgs::IN_FUNCTION_CALLS_STACK, zv::Val(std::move(stack))); + return scopeFactoryCreate(a); + } + + /* $reflection: MethodReflection|FunctionReflection|null (untyped in the + * twin), $parameter: ParameterReflection|null */ + zv::Val pushInFunctionCall(zval *reflection, zval *parameter, bool rememberTypes) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) return zv::Val(); + zv::Arr stack = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw())); + zv::Arr pair = zv::Arr::create(2); + pair.push(zv::Ref(reflection)); + pair.push(zv::Ref(parameter)); + stack.push(zv::Val(std::move(pair))); + + zv::Val functionScope = createWithFunctionCallStack(std::move(stack)); + if (UNEXPECTED(functionScope.isUndef())) return zv::Val(); + if (rememberTypes) { + if (UNEXPECTED(!assignResolvedTypes(functionScope))) return zv::Val(); + } + return functionScope; + } + + zv::Val popInFunctionCall() + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) return zv::Val(); + zv::Arr stack = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw())); + arrayPop(stack); + + zv::Val parentScope = createWithFunctionCallStack(std::move(stack)); + if (UNEXPECTED(parentScope.isUndef())) return zv::Val(); + if (UNEXPECTED(!assignResolvedTypes(parentScope))) return zv::Val(); + return parentScope; + } + + /* $this->inFunctionCallsStack entry's [0] — the reflection of the call, + * NULL when the entry carries none (the twin's list destructuring) */ + static zval *inFunctionCallReflection(zv::Ref entryValue) + { + zv::Ref item = entryValue.deref(); + if (!item.isArray()) return NULL; + return zend_hash_index_find(item.asArrayTable(), 0); + } + + /** @api; false = pending exception */ + [[nodiscard]] bool isInClassExists(zend_string *className, bool &out) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) return false; + static const char *const classExistsFunctions[] = { "class_exists", "interface_exists", "trait_exists", "enum_exists" }; + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw())) { + zval *inFunctionCall = inFunctionCallReflection(entry.value()); + if (inFunctionCall == NULL) continue; + zv::Ref reflection = zv::Ref(inFunctionCall).deref(); + bool isFunctionReflection; + if (UNEXPECTED(!isInstance(reflection, PT_CLASS_FUNCTION_REFLECTION, isFunctionReflection))) return false; + if (!isFunctionReflection) continue; + zv::Val name = pt_type_call(reflection.asObject(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return false; + for (const char *functionName : classExistsFunctions) { + if (name.ref().isString() && zend_string_equals_cstr(name.ref().asString(), functionName, strlen(functionName))) { + out = true; + return true; + } + } + } + + /* interface_exists() etc. imply class_exists() therefore not listed here */ + return existenceCheckIsTrue(PT_LC("class_exists"), className, out); + } + + /** @api; false = pending exception */ + [[nodiscard]] bool isInFunctionExists(zend_string *functionName, bool &out) + { + return existenceCheckIsTrue(PT_LC("function_exists"), functionName, out); + } + + /* $this->getType(new FuncCall(new FullyQualified($check), [new Arg(new + * String_(ltrim($name, '\\')))]))->isTrue()->yes() */ + bool existenceCheckIsTrue(const char *check, size_t checkLen, zend_string *name, bool &out) + { + const char *value = ZSTR_VAL(name); + size_t valueLen = ZSTR_LEN(name); + while (valueLen > 0 && *value == '\\') { + value++; + valueLen--; + } + zv::Val literal = zv::Val::string(value, valueLen); + zv::Val string_ = pt_type_new(PT_CLASS_SCALAR_STRING, 1, literal.raw()); + if (UNEXPECTED(string_.isUndef())) return false; + zv::Val arg = pt_type_new(PT_CLASS_ARG, 1, string_.raw()); + if (UNEXPECTED(arg.isUndef())) return false; + zv::Arr args = zv::Arr::create(1); + args.push(std::move(arg)); + zv::Val checkName = zv::Val::string(check, checkLen); + zv::Val fullyQualified = pt_type_new(PT_CLASS_FULLY_QUALIFIED, 1, checkName.raw()); + if (UNEXPECTED(fullyQualified.isUndef())) return false; + zv::Args argv{fullyQualified.raw(), args.raw()}; + zv::Val expr = pt_type_new(PT_CLASS_FUNC_CALL, 2, argv); + if (UNEXPECTED(expr.isUndef())) return false; + zv::Val type = thisGetType(expr.raw()); + if (UNEXPECTED(type.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(type.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isTrue() on %s", zend_zval_value_name(type.raw())); + return false; + } + zend_long isTrue = pt_type_call_trinary(Z_OBJ_P(type.raw()), PT_LC("istrue"), 0, NULL); + if (UNEXPECTED(isTrue < 0)) return false; + out = isTrue == PT_TRI_YES; + return true; + } + + /* the two call-stack readers: the entries' reflections (withParameters: + * the entries themselves) of the entries that carry one, reindexed */ + zv::Val functionCallStack(bool withParameters) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) return zv::Val(); + zv::Arr stack = zv::Arr::create(0); + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw())) { + zval *inFunctionCall = inFunctionCallReflection(entry.value()); + if (inFunctionCall == NULL || zv::Ref(inFunctionCall).deref().isNull()) continue; + stack.push(withParameters ? entry.value().deref() : zv::Ref(inFunctionCall).deref()); + } + return zv::Val(std::move(stack)); + } + + zv::Val getFunctionCallStack() { return functionCallStack(false); } + zv::Val getFunctionCallStackWithParameters() { return functionCallStack(true); } + + /* getConstantTypes() (twin 5763) / getNativeConstantTypes() (5798) over + * the given table slot */ + zv::Val constantTypesOf(uint32_t tableSlot, const char *name) + { + if (UNEXPECTED(!requireSlot(tableSlot, name))) return zv::Val(); + zv::Arr constantTypes = zv::Arr::create(0); + for (auto entry : zv::ArrRef(slot(tableSlot).raw())) { + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + bool isConstFetch; + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_CONST_FETCH, isConstFetch))) return zv::Val(); + if (!isConstFetch) continue; + zval copy; + ZVAL_COPY(©, entry.value().raw()); + pt_ht_update(constantTypes.table(), entry.stringKeyOrNull(), entry.indexKey(), ©); + } + return zv::Val(std::move(constantTypes)); + } + + zv::Val getConstantTypes() { return constantTypesOf(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"); } + zv::Val getNativeConstantTypes() { return constantTypesOf(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes"); } + + /* new Variable($name) */ + static zv::Val newVariable(const char *name, size_t len) + { + zv::Val nameVal = zv::Val::string(name, len); + return pt_type_new(PT_CLASS_VARIABLE, 1, nameVal.raw()); + } + + /* $this->context->(...$argv) — the ScopeContext is native, but + * its state-changing methods are called by name (the differential + * harness hands the native scope a PHP context) */ + zv::Val contextCall(const char *lcname, size_t len, uint32_t argc, zval *argv) + { + zv::Ref context = slot(PT_MS_PROP_CONTEXT); + if (UNEXPECTED(!context.isObject())) return uninitializedProperty("context"); + return pt_type_call(context.asObject(), lcname, len, argc, argv); + } + + /** @api */ + zv::Val enterClass(zval *classReflection) + { + /* $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection)) */ + zv::Val thisVariable = newVariable(PT_LC("this")); + if (UNEXPECTED(thisVariable.isUndef())) return zv::Val(); + zval thisTypeZv; + if (UNEXPECTED(!pt_this_type_new(&thisTypeZv, classReflection))) return zv::Val(); + zv::Val thisType = zv::Val::adopt(thisTypeZv); + zval thisHolderZv; + pt_holder_create(&thisHolderZv, thisVariable.raw(), thisType.raw(), PT_TRI_YES); + zv::Val thisHolder = zv::Val::adopt(thisHolderZv); + + zv::Val constantTypesVal = getConstantTypes(); + if (UNEXPECTED(constantTypesVal.isUndef())) return zv::Val(); + zv::Arr constantTypes = zv::Arr::adoptVal(std::move(constantTypesVal)); + constantTypes.set("$this", zv::Val::copyOf(thisHolder.ref())); + + zv::Val nativeConstantTypesVal = getNativeConstantTypes(); + if (UNEXPECTED(nativeConstantTypesVal.isUndef())) return zv::Val(); + zv::Arr nativeConstantTypes = zv::Arr::adoptVal(std::move(nativeConstantTypesVal)); + nativeConstantTypes.set("$this", zv::Val::copyOf(thisHolder.ref())); + + zv::Val context = contextCall(PT_LC("enterclass"), 1, classReflection); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + + CreateArgs a; + a.setOwned(CreateArgs::CONTEXT, std::move(context)); + if (UNEXPECTED(!fillDispatched(a, false, false))) return zv::Val(); + a.setNull(CreateArgs::FUNCTION); + a.setOwned(CreateArgs::EXPRESSION_TYPES, zv::Val(std::move(constantTypes))); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Val(std::move(nativeConstantTypes))); + a.setEmptyArray(CreateArgs::CONDITIONAL_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES); + a.setNull(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + /* $classReflection->isAnonymous() ? $this : null */ + zv::Val isAnonymous = pt_type_call(Z_OBJ_P(classReflection), PT_LC("isanonymous"), 0, NULL); + if (UNEXPECTED(isAnonymous.isUndef())) return zv::Val(); + if (zend_is_true(isAnonymous.raw())) { + a.set(CreateArgs::PARENT_SCOPE, zv::Ref(thisZval())); + } else { + a.setNull(CreateArgs::PARENT_SCOPE); + } + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, false); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) return zv::Val(); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + zv::Val enterTrait(zval *traitReflection) + { + /* $namespace = the trait name without its last segment, null when it has one segment */ + zv::Val traitName = pt_class_reflection_get_name(Z_OBJ_P(traitReflection)); + if (UNEXPECTED(traitName.isUndef())) return zv::Val(); + zend_string *name = zval_get_string(traitName.raw()); + if (UNEXPECTED(name == NULL)) return zv::Val(); + zv::Str ownedName = zv::Str::adopt(name); + zv::Val ns = zv::Val::null(); + for (size_t i = ZSTR_LEN(name); i > 0; i--) { + if (ZSTR_VAL(name)[i - 1] == '\\') { + ns = zv::Val::string(ZSTR_VAL(name), i - 1); + break; + } + } + + zv::Val context = contextCall(PT_LC("entertrait"), 1, traitReflection); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + a.setOwned(CreateArgs::CONTEXT, std::move(context)); + /* the dispatched getters this site passes, $namespace among them not */ + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + zv::Val function = thisGetFunction(); + if (UNEXPECTED(function.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::FUNCTION, std::move(function)); + a.setOwned(CreateArgs::NAMESPACE_, std::move(ns)); + a.setEmptyArray(CreateArgs::CONDITIONAL_EXPRESSIONS); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.setNull(CreateArgs::PARENT_SCOPE); + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, false); + return scopeFactoryCreate(a); + } + + /* {{{ out of the twin's file order, for the function-like family: + * getPhpVersion() (twin 5835), isParameterValueNullable() (2868), + * getFunctionType() (2881) */ + + zv::Val getPhpVersion() + { + zv::Val constantName = zv::Val::string(PT_LC("PHP_VERSION_ID")); + zv::Val nameNode = pt_type_new(PT_CLASS_NAME, 1, constantName.raw()); + if (UNEXPECTED(nameNode.isUndef())) return zv::Val(); + zv::Val constType = getGlobalConstantType(Z_OBJ_P(nameNode.raw())); + if (UNEXPECTED(constType.isUndef())) return zv::Val(); + + bool isOverallPhpVersionRange = false; + if (constType.ref().isObject() && constType.ref().instanceOf(pt_ce_integer_range_type)) { + zv::Val min = pt_type_call(constType.ref().asObject(), PT_LC("getmin"), 0, NULL); + if (UNEXPECTED(min.isUndef())) return zv::Val(); + if (min.ref().isLong() && min.ref().asLong() == PT_MS_PHP_MIN_ANALYZABLE_VERSION_ID) { + zv::Val max = pt_type_call(constType.ref().asObject(), PT_LC("getmax"), 0, NULL); + if (UNEXPECTED(max.isUndef())) return zv::Val(); + if (max.isNull() || (max.ref().isLong() && max.ref().asLong() == PT_MS_MAX_PHP_VERSION)) { + isOverallPhpVersionRange = true; + } + } + } + + if (!constType.isNull() && !isOverallPhpVersionRange) return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, constType.raw()); + + // The analysed PHP version range comes either from the NEON phpVersion min/max + // config or from the composer.json "require.php" constraint - the very same + // source ConstantResolver narrows PHP_VERSION_ID with, so that + // Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant. + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONFIGURED_PHP_VERSION_RANGE_HELPER, "configuredPhpVersionRangeHelper"))) return zv::Val(); + zv::Ref rangeHelper = slot(PT_MS_PROP_CONFIGURED_PHP_VERSION_RANGE_HELPER); + zv::Val range = pt_type_call(rangeHelper.asObject(), PT_LC("getversionrange"), 0, NULL); + if (UNEXPECTED(range.isUndef())) return zv::Val(); + /* [$minPhpVersion, $maxPhpVersion] = ... */ + zval *bounds[2] = { NULL, NULL }; + if (range.ref().isArray()) { + for (zend_ulong i = 0; i < 2; i++) { + zval *bound = zend_hash_index_find(range.ref().asArrayTable(), i); + if (UNEXPECTED(bound == NULL)) { + zend_error(E_WARNING, "Undefined array key " ZEND_ULONG_FMT, i); + if (UNEXPECTED(EG(exception))) return zv::Val(); + continue; + } + ZVAL_DEREF(bound); + if (Z_TYPE_P(bound) != IS_NULL) bounds[i] = bound; + } + } + zval *minPhpVersion = bounds[0]; + zval *maxPhpVersion = bounds[1]; + bool narrowed = minPhpVersion != NULL; + if (!narrowed && maxPhpVersion != NULL) { + zv::Val maxVersionId = pt_type_call(Z_OBJ_P(maxPhpVersion), PT_LC("getversionid"), 0, NULL); + if (UNEXPECTED(maxVersionId.isUndef())) return zv::Val(); + narrowed = !(maxVersionId.ref().isLong() && maxVersionId.ref().asLong() == PT_MS_MAX_PHP_VERSION); + } + if (narrowed) { + zval interval[2]; + zv::Val minVersionId, maxVersionId; + if (minPhpVersion != NULL) { + minVersionId = pt_type_call(Z_OBJ_P(minPhpVersion), PT_LC("getversionid"), 0, NULL); + if (UNEXPECTED(minVersionId.isUndef())) return zv::Val(); + ZVAL_COPY_VALUE(&interval[0], minVersionId.raw()); + } else { + ZVAL_LONG(&interval[0], PT_MS_PHP_MIN_ANALYZABLE_VERSION_ID); + } + if (maxPhpVersion != NULL) { + maxVersionId = pt_type_call(Z_OBJ_P(maxPhpVersion), PT_LC("getversionid"), 0, NULL); + if (UNEXPECTED(maxVersionId.isUndef())) return zv::Val(); + ZVAL_COPY_VALUE(&interval[1], maxVersionId.raw()); + } else { + ZVAL_NULL(&interval[1]); + } + zv::Val versionRange = pt_type_call_static_ce(pt_ce_integer_range_type, PT_LC("frominterval"), 2, interval); + if (UNEXPECTED(versionRange.isUndef())) return zv::Val(); + return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, versionRange.raw()); + } + + zv::Ref phpVersion = slot(PT_MS_PROP_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) return uninitializedProperty("phpVersion"); + zv::Val versionId = pt_type_call(phpVersion.asObject(), PT_LC("getversionid"), 0, NULL); + if (UNEXPECTED(versionId.isUndef())) return zv::Val(); + zval constantInteger; + if (UNEXPECTED(!pt_constant_integer_type_new(&constantInteger, zval_get_long(versionId.raw())))) return zv::Val(); + zv::Val constantIntegerType = zv::Val::adopt(constantInteger); + return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, constantIntegerType.raw()); + } + + /* $this->getPhpVersion()->supportsNamedArguments()->no() negated — the + * test the variadic parameter types are built on; false = pending exception */ + [[nodiscard]] bool phpVersionSupportsNamedArguments(bool &out) + { + zv::Val phpVersions = thisGetPhpVersion(); + if (UNEXPECTED(phpVersions.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(phpVersions.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function supportsNamedArguments() on %s", zend_zval_value_name(phpVersions.raw())); + return false; + } + zend_long supports = pt_type_call_trinary(Z_OBJ_P(phpVersions.raw()), PT_LC("supportsnamedarguments"), 0, NULL); + if (UNEXPECTED(supports < 0)) return false; + out = supports != PT_TRI_NO; + return true; + } + + /* IntegerRangeType::createAllGreaterThanOrEqualTo(0) */ + static zv::Val allGreaterThanOrEqualToZero() + { + zval zero; + ZVAL_LONG(&zero, 0); + return pt_type_call_static_ce(pt_ce_integer_range_type, PT_LC("createallgreaterthanorequalto"), 1, &zero); + } + + /* the variadic parameter's array type: keyed by int|string under named + * arguments, a list otherwise */ + static zv::Val variadicArrayType(zval *itemType, bool supportsNamedArguments) + { + zv::Val keyType = allGreaterThanOrEqualToZero(); + if (UNEXPECTED(keyType.isUndef())) return zv::Val(); + if (supportsNamedArguments) { + zval stringTypeZv; + if (UNEXPECTED(!pt_string_type_new(&stringTypeZv))) return zv::Val(); + zv::Val stringType = zv::Val::adopt(stringTypeZv); + zv::Arr keyTypes = zv::Arr::create(2); + keyTypes.push(keyType.ref()); + keyTypes.push(stringType.ref()); + zval unionZv; + if (UNEXPECTED(!pt_union_type_new(&unionZv, keyTypes.raw()))) return zv::Val(); + zv::Val unionType = zv::Val::adopt(unionZv); + zval arrayZv; + if (UNEXPECTED(!pt_array_type_new(&arrayZv, unionType.raw(), itemType))) return zv::Val(); + return zv::Val::adopt(arrayZv); + } + + zval arrayZv; + if (UNEXPECTED(!pt_array_type_new(&arrayZv, keyType.raw(), itemType))) return zv::Val(); + zv::Val arrayType = zv::Val::adopt(arrayZv); + zval listZv; + if (UNEXPECTED(!pt_accessory_array_list_type_new(&listZv))) return zv::Val(); + zv::Val listType = zv::Val::adopt(listZv); + zv::Arr types = zv::Arr::create(2); + types.push(arrayType.ref()); + types.push(listType.ref()); + zval intersectionZv; + if (UNEXPECTED(!pt_intersection_type_new(&intersectionZv, types.raw()))) return zv::Val(); + return zv::Val::adopt(intersectionZv); + } + + /** @api */ + bool isParameterValueNullable(zend_object *parameter, bool &out) + { + zv::Ref defaultValue = nodeProp(parameter, PT_LC("default")); + if (UNEXPECTED(defaultValue.raw() == NULL)) return false; + bool isConstFetch; + if (UNEXPECTED(!isInstance(defaultValue.deref(), PT_CLASS_CONST_FETCH, isConstFetch))) return false; + if (!isConstFetch) { + out = false; + return true; + } + /* strtolower((string) $parameter->default->name) === 'null' */ + zv::Ref name = nodeProp(defaultValue.deref().asObject(), PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + zend_string *nameString = zval_get_string(name.deref().raw()); + if (UNEXPECTED(nameString == NULL)) return false; + zend_string *lower = zend_string_tolower(nameString); + zend_string_release(nameString); + out = zend_string_equals_literal(lower, "null"); + zend_string_release(lower); + return EXPECTED(EG(exception) == NULL); + } + + /** @api; $type: Name|Identifier|ComplexType|null */ + zv::Val getFunctionType(zval *type, bool isNullable, bool isVariadic) + { + if (isVariadic) { + bool supportsNamedArguments; + if (UNEXPECTED(!phpVersionSupportsNamedArguments(supportsNamedArguments))) return zv::Val(); + zv::Val itemType = thisGetFunctionType(type, isNullable, false); + if (UNEXPECTED(itemType.isUndef())) return zv::Val(); + return variadicArrayType(itemType.raw(), supportsNamedArguments); + } + + bool isName; + if (UNEXPECTED(!isInstance(zv::Ref(type), PT_CLASS_NAME, isName))) return zv::Val(); + if (isName) { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) return zv::Val(); + HashTable *bindScopeClasses = Z_ARRVAL_P(slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES).raw()); + if (zend_hash_num_elements(bindScopeClasses) != 0 && !isSingleStringList(bindScopeClasses, PT_LC("static"))) { + zv::Val lower = pt_type_call(Z_OBJ_P(type), PT_LC("tolowerstring"), 0, NULL); + if (UNEXPECTED(lower.isUndef())) return zv::Val(); + bool isRelativeName = lower.ref().isString() + && (zend_string_equals_literal(lower.ref().asString(), "static") + || zend_string_equals_literal(lower.ref().asString(), "self") + || zend_string_equals_literal(lower.ref().asString(), "parent")); + if (isRelativeName) { + zval nullZv; + ZVAL_NULL(&nullZv); + zval *first = zend_hash_index_find(bindScopeClasses, 0); + if (UNEXPECTED(first == NULL)) { + zend_error(E_WARNING, "Undefined array key 0"); + if (UNEXPECTED(EG(exception))) return zv::Val(); + first = &nullZv; + } + zv::Ref provider = slot(PT_MS_PROP_REFLECTION_PROVIDER); + if (UNEXPECTED(!provider.isObject())) return uninitializedProperty("reflectionProvider"); + bool hasClass; + if (UNEXPECTED(!pt_reflection_provider_has_class(provider.asObject(), first, hasClass))) return zv::Val(); + if (hasClass) { + zv::Val classReflection = pt_reflection_provider_get_class(provider.asObject(), first); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclassreflection"), 1, classReflection.raw()); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + return initializerExprTypeResolverFunctionType(type, isNullable, context.raw()); + } + } + } + } + + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromscope"), 1, thisZval()); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + return initializerExprTypeResolverFunctionType(type, isNullable, context.raw()); + } + + /* $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, $context) */ + zv::Val initializerExprTypeResolverFunctionType(zval *type, bool isNullable, zval *context) + { + zv::Ref resolver = slot(PT_MS_PROP_INITIALIZER_EXPR_TYPE_RESOLVER); + if (UNEXPECTED(!resolver.isObject())) return uninitializedProperty("initializerExprTypeResolver"); + zv::Args argv{type, isNullable, bool(false), context}; + return pt_type_call(resolver.asObject(), PT_LC("getfunctiontype"), 4, argv); + } + + /* }}} */ + + /* {{{ twin 2012-2369: the function-like family */ + + /* array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $types), + * with $this->transformStaticType() around it where the twin has it + * (array_map over one array keeps the keys) */ + zv::Val mapToArgument(zval *types, bool transform) + { + zv::Arr result = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(types))); + for (auto entry : zv::ArrRef(types)) { + zv::Val mapped = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("toargument"), 1, entry.value().deref().raw()); + if (UNEXPECTED(mapped.isUndef())) return zv::Val(); + if (transform) { + zv::Val transformed = transformStaticType(mapped.raw()); + if (UNEXPECTED(transformed.isUndef())) return zv::Val(); + mapped = std::move(transformed); + } + zval value = mapped.take(); + pt_ht_update(result.table(), entry.stringKeyOrNull(), entry.indexKey(), &value); + } + return zv::Val(std::move(result)); + } + + /* array_merge($first, $second) */ + static zv::Val arrayMerge(HashTable *first, HashTable *second) + { + zv::Arr merged = zv::Arr::create(zend_hash_num_elements(first) + zend_hash_num_elements(second)); + for (HashTable *table : { first, second }) { + for (auto entry : zv::TableRef(table)) { + zval copy; + ZVAL_COPY(©, entry.value().deref().raw()); + if (entry.stringKeyOrNull() != NULL) { + zend_hash_update(merged.table(), entry.stringKeyOrNull(), ©); + } else { + zend_hash_next_index_insert(merged.table(), ©); + } + } + } + return zv::Val(std::move(merged)); + } + + /* $parameter->var->name of a Node\Param, the twin's + * ShouldNotHappenException when it is not a plain Variable; NULL = + * pending exception */ + static zend_string *parameterVariableName(zend_object *parameter) + { + zv::Ref var = nodeProp(parameter, PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return NULL; + bool isVariable; + if (UNEXPECTED(!isInstance(var.deref(), PT_CLASS_VARIABLE, isVariable))) return NULL; + if (isVariable) { + zv::Ref name = nodeProp(var.deref().asObject(), PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return NULL; + if (name.deref().isString()) return name.deref().asString(); + } + pt_throw_should_not_happen(); + return NULL; + } + + /* $functionLike->getParams() */ + static zv::Val functionLikeParams(zend_object *functionLike) + { + return pt_type_call(functionLike, PT_LC("getparams"), 0, NULL); + } + + /* private (twin 2164) */ + zv::Val transformStaticType(zval *type) + { + zv::Val traverser = pt_type_new(PT_CLASS_TRANSFORM_STATIC_TYPE_TRAVERSER, 1, thisZval()); + if (UNEXPECTED(traverser.isUndef())) return zv::Val(); + zval mapped; + if (UNEXPECTED(!pt_type_traverser_map(&mapped, type, traverser.raw()))) return zv::Val(); + return zv::Val::adopt(mapped); + } + + /* private (twin 2172) */ + zv::Val getRealParameterTypes(zend_object *functionLike) + { + zv::Val params = functionLikeParams(functionLike); + if (UNEXPECTED(params.isUndef())) return zv::Val(); + if (UNEXPECTED(!params.ref().isArray())) { + zend_type_error("phpstan_turbo: getParams() must return array, %s returned", zend_zval_value_name(params.raw())); + return zv::Val(); + } + zv::Arr realParameterTypes = zv::Arr::create(zend_hash_num_elements(params.ref().asArrayTable())); + for (auto entry : zv::ArrRef(params.raw())) { + zv::Ref parameter = entry.value().deref(); + if (UNEXPECTED(!parameter.isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_string *name = parameterVariableName(parameter.asObject()); + if (UNEXPECTED(name == NULL)) return zv::Val(); + zv::Str ownedName = zv::Str::copyOf(name); + bool nullable; + if (UNEXPECTED(!thisIsParameterValueNullable(parameter.raw(), nullable))) return zv::Val(); + zv::Ref flags = nodeProp(parameter.asObject(), PT_LC("flags")); + if (UNEXPECTED(flags.raw() == NULL)) return zv::Val(); + zv::Ref type = nodeProp(parameter.asObject(), PT_LC("type")); + if (UNEXPECTED(type.raw() == NULL)) return zv::Val(); + bool isNullable = nullable && flags.deref().isLong() && flags.deref().asLong() == 0; + zv::Val parameterType = thisGetFunctionType(type.deref().raw(), isNullable, false); + if (UNEXPECTED(parameterType.isUndef())) return zv::Val(); + realParameterTypes.set(ownedName.get(), std::move(parameterType)); + } + return zv::Val(std::move(realParameterTypes)); + } + + /* private (twin 2192) */ + zv::Val getRealParameterDefaultValues(zend_object *functionLike) + { + zv::Val params = functionLikeParams(functionLike); + if (UNEXPECTED(params.isUndef())) return zv::Val(); + if (UNEXPECTED(!params.ref().isArray())) { + zend_type_error("phpstan_turbo: getParams() must return array, %s returned", zend_zval_value_name(params.raw())); + return zv::Val(); + } + zv::Arr defaultValues = zv::Arr::create(0); + for (auto entry : zv::ArrRef(params.raw())) { + zv::Ref parameter = entry.value().deref(); + if (UNEXPECTED(!parameter.isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Ref defaultValue = nodeProp(parameter.asObject(), PT_LC("default")); + if (UNEXPECTED(defaultValue.raw() == NULL)) return zv::Val(); + if (defaultValue.deref().isNull()) continue; + zend_string *name = parameterVariableName(parameter.asObject()); + if (UNEXPECTED(name == NULL)) return zv::Val(); + zv::Str ownedName = zv::Str::copyOf(name); + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromscope"), 1, thisZval()); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Ref resolver = slot(PT_MS_PROP_INITIALIZER_EXPR_TYPE_RESOLVER); + if (UNEXPECTED(!resolver.isObject())) return uninitializedProperty("initializerExprTypeResolver"); + zv::Args argv{defaultValue.deref().raw(), context.raw()}; + zv::Val type = pt_type_call(resolver.asObject(), PT_LC("gettype"), 2, argv); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + defaultValues.set(ownedName.get(), std::move(type)); + } + return zv::Val(std::move(defaultValues)); + } + + /* private (twin 2211) */ + zv::Val getParameterAttributes(zend_object *functionLike) + { + zval classNameZv = {}; + ZVAL_NULL(&classNameZv); + zv::Val className; + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (inClass) { + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + className = pt_class_reflection_get_name(Z_OBJ_P(classReflection.raw())); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + ZVAL_COPY_VALUE(&classNameZv, className.raw()); + } + + zv::Val params = functionLikeParams(functionLike); + if (UNEXPECTED(params.isUndef())) return zv::Val(); + if (UNEXPECTED(!params.ref().isArray())) { + zend_type_error("phpstan_turbo: getParams() must return array, %s returned", zend_zval_value_name(params.raw())); + return zv::Val(); + } + zv::Arr parameterAttributes = zv::Arr::create(zend_hash_num_elements(params.ref().asArrayTable())); + for (auto entry : zv::ArrRef(params.raw())) { + zv::Ref parameter = entry.value().deref(); + if (UNEXPECTED(!parameter.isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_string *name = parameterVariableName(parameter.asObject()); + if (UNEXPECTED(name == NULL)) return zv::Val(); + zv::Str ownedName = zv::Str::copyOf(name); + zv::Ref attrGroups = nodeProp(parameter.asObject(), PT_LC("attrGroups")); + if (UNEXPECTED(attrGroups.raw() == NULL)) return zv::Val(); + zv::Val attributes = attributesFromAttrGroups(attrGroups.deref().raw(), &classNameZv, functionLike); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + parameterAttributes.set(ownedName.get(), std::move(attributes)); + } + return zv::Val(std::move(parameterAttributes)); + } + + /* $this->attributeReflectionFactory->fromAttrGroups($attrGroups, + * InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike)) */ + zv::Val attributesFromAttrGroups(zval *attrGroups, zval *className, zend_object *functionLike) + { + zv::Val file = thisGetFile(); + if (UNEXPECTED(file.isUndef())) return zv::Val(); + zv::Args contextArgs{className, file.raw(), functionLike}; + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromstubparameter"), 3, contextArgs); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Ref factory = slot(PT_MS_PROP_ATTRIBUTE_REFLECTION_FACTORY); + if (UNEXPECTED(!factory.isObject())) return uninitializedProperty("attributeReflectionFactory"); + zv::Args argv{attrGroups, context.raw()}; + return pt_type_call(factory.asObject(), PT_LC("fromattrgroups"), 2, argv); + } + + /* Assertions::createEmpty() */ + static zv::Val emptyAssertions() { return pt_type_call_static(PT_CLASS_ASSERTIONS, PT_LC("createempty"), 0, NULL); } + + /** @api (twin 2012) */ + zv::Val enterClassMethod(zval *classMethod, zval *templateTypeMap, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *throwType, zval *deprecatedDescription, bool isDeprecated, bool isInternal, bool isFinal, zval *isPure, bool acceptsNamedArguments, zval *asserts, zval *selfOutType, zval *phpDocComment, zval *parameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *phpDocClosureThisTypeParameters, bool isConstructor, zval *resolvedPhpDocBlock, zval *phpDocPureUnlessCallableIsImpureParameters) + { + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (!inClass) { + pt_throw_should_not_happen(); + return zv::Val(); + } + + zend_object *classMethodObject = Z_OBJ_P(classMethod); + Args<28> a; + PT_MS_ARG_OWNED(a, thisGetClassReflection()); + a.add(zv::Ref(classMethod)); + a.addNull(); + PT_MS_ARG_OWNED(a, thisGetFile()); + a.add(zv::Ref(templateTypeMap)); + PT_MS_ARG_OWNED(a, getRealParameterTypes(classMethodObject)); + PT_MS_ARG_OWNED(a, mapToArgument(phpDocParameterTypes, true)); + PT_MS_ARG_OWNED(a, getRealParameterDefaultValues(classMethodObject)); + PT_MS_ARG_OWNED(a, getParameterAttributes(classMethodObject)); + { + zv::Ref returnType = nodeProp(classMethodObject, PT_LC("returnType")); + if (UNEXPECTED(returnType.raw() == NULL)) return zv::Val(); + zv::Val functionType = thisGetFunctionType(returnType.deref().raw(), false, false); + if (UNEXPECTED(functionType.isUndef())) return zv::Val(); + PT_MS_ARG_OWNED(a, transformStaticType(functionType.raw())); + } + PT_MS_ARG_OWNED(a, transformedArgumentOrNull(phpDocReturnType)); + PT_MS_ARG_OWNED(a, transformedArgumentOrNull(throwType)); + a.add(zv::Ref(deprecatedDescription)); + a.addBool(isDeprecated); + a.addBool(isInternal); + a.addBool(isFinal); + a.add(zv::Ref(isPure)); + a.addBool(acceptsNamedArguments); + if (Z_TYPE_P(asserts) == IS_NULL) { + PT_MS_ARG_OWNED(a, emptyAssertions()); + } else { + a.add(zv::Ref(asserts)); + } + a.add(zv::Ref(selfOutType)); + a.add(zv::Ref(phpDocComment)); + a.add(zv::Ref(resolvedPhpDocBlock)); + PT_MS_ARG_OWNED(a, mapToArgument(parameterOutTypes, true)); + a.add(zv::Ref(immediatelyInvokedCallableParameters)); + PT_MS_ARG_OWNED(a, mapToArgument(phpDocClosureThisTypeParameters, true)); + a.addBool(isConstructor); + { + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(classReflection.raw())); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Ref attrGroups = nodeProp(classMethodObject, PT_LC("attrGroups")); + if (UNEXPECTED(attrGroups.raw() == NULL)) return zv::Val(); + PT_MS_ARG_OWNED(a, attributesFromAttrGroups(attrGroups.deref().raw(), className.raw(), classMethodObject)); + } + a.add(zv::Ref(phpDocPureUnlessCallableIsImpureParameters)); + + zv::Val reflection = pt_type_new(PT_CLASS_PHP_METHOD_FROM_PARSER_NODE_REFLECTION, a.count, a.argv); + if (UNEXPECTED(reflection.isUndef())) return zv::Val(); + zv::Val isStatic = pt_type_call(classMethodObject, PT_LC("isstatic"), 0, NULL); + if (UNEXPECTED(isStatic.isUndef())) return zv::Val(); + return enterFunctionLike(reflection.raw(), !zend_is_true(isStatic.raw())); + } + + /* $type !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($type)) : null */ + zv::Val transformedArgumentOrNull(zval *type) + { + if (Z_TYPE_P(type) == IS_NULL) return zv::Val::null(); + zv::Val argument = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("toargument"), 1, type); + if (UNEXPECTED(argument.isUndef())) return zv::Val(); + return transformStaticType(argument.raw()); + } + + /* (twin 2077) */ + zv::Val enterPropertyHook(zval *hook, zend_string *propertyName, zval *nativePropertyTypeNode, zval *phpDocPropertyType, zval *phpDocParameterTypes, zval *throwType, zval *deprecatedDescription, bool isDeprecated, zval *isPure, zval *phpDocComment, zval *resolvedPhpDocBlock) + { + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (!inClass) { + pt_throw_should_not_happen(); + return zv::Val(); + } + + zv::Val mappedParameterTypes = mapToArgument(phpDocParameterTypes, true); + if (UNEXPECTED(mappedParameterTypes.isUndef())) return zv::Val(); + zv::Arr parameterTypes = zv::Arr::adoptVal(std::move(mappedParameterTypes)); + + zend_object *hookObject = Z_OBJ_P(hook); + zv::Ref hookName = nodeProp(hookObject, PT_LC("name")); + if (UNEXPECTED(hookName.raw() == NULL)) return zv::Val(); + if (UNEXPECTED(!hookName.deref().isObject())) { + zend_throw_error(NULL, "Call to a member function toLowerString() on %s", zend_zval_value_name(hookName.deref().raw())); + return zv::Val(); + } + zv::Val lowerName = pt_type_call(hookName.deref().asObject(), PT_LC("tolowerstring"), 0, NULL); + if (UNEXPECTED(lowerName.isUndef())) return zv::Val(); + bool isSet = lowerName.ref().isString() && zend_string_equals_literal(lowerName.ref().asString(), "set"); + bool isGet = lowerName.ref().isString() && zend_string_equals_literal(lowerName.ref().asString(), "get"); + + zv::Val ownedHook; + zv::Val realReturnType; + zv::Val phpDocReturnType = zv::Val::null(); + if (isSet) { + zv::Ref params = nodeProp(hookObject, PT_LC("params")); + if (UNEXPECTED(params.raw() == NULL)) return zv::Val(); + if (params.deref().isArray() && zend_hash_num_elements(params.deref().asArrayTable()) == 0) { + /* $hook = clone $hook; $hook->params = [new Node\Param(new Variable('value'), type: $nativePropertyTypeNode)]; */ + zend_object *clone = hookObject->handlers->clone_obj(hookObject); + if (UNEXPECTED(EG(exception))) { + if (clone != NULL) { + OBJ_RELEASE(clone); + } + return zv::Val(); + } + zval cloneZv; + ZVAL_OBJ(&cloneZv, clone); + ownedHook = zv::Val::adopt(cloneZv); + hookObject = clone; + + zv::Val valueVariable = newVariable(PT_LC("value")); + if (UNEXPECTED(valueVariable.isUndef())) return zv::Val(); + zv::Args paramArgs{valueVariable.raw(), zv::null, nativePropertyTypeNode}; + zv::Val param = pt_type_new(PT_CLASS_PARAM, 3, paramArgs); + if (UNEXPECTED(param.isUndef())) return zv::Val(); + zv::Arr newParams = zv::Arr::create(1); + newParams.push(std::move(param)); + zend_update_property(hookObject->ce, hookObject, PT_LC("params"), newParams.raw()); + if (UNEXPECTED(EG(exception))) return zv::Val(); + } + + zv::Ref currentParams = nodeProp(hookObject, PT_LC("params")); + if (UNEXPECTED(currentParams.raw() == NULL)) return zv::Val(); + zval *firstParam = currentParams.deref().isArray() ? zend_hash_index_find(currentParams.deref().asArrayTable(), 0) : NULL; + if (firstParam != NULL && Z_TYPE_P(phpDocPropertyType) != IS_NULL) { + zv::Ref var = zv::Ref(firstParam).deref().isObject() ? nodeProp(Z_OBJ_P(zv::Ref(firstParam).deref().raw()), PT_LC("var")) : zv::Ref(NULL); + if (var.raw() != NULL) { + bool isVariable; + if (UNEXPECTED(!isInstance(var.deref(), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (isVariable) { + zv::Ref varName = nodeProp(var.deref().asObject(), PT_LC("name")); + if (UNEXPECTED(varName.raw() == NULL)) return zv::Val(); + if (varName.deref().isString()) { + zend_string *key = varName.deref().asString(); + if (zend_symtable_find(parameterTypes.table(), key) == NULL) { + zv::Val valueParamType = transformedArgumentOrNull(phpDocPropertyType); + if (UNEXPECTED(valueParamType.isUndef())) return zv::Val(); + parameterTypes.set(key, std::move(valueParamType)); + } + } + } + } else if (UNEXPECTED(EG(exception))) { + return zv::Val(); + } + } + + zval voidType; + if (UNEXPECTED(!pt_void_type_new(&voidType))) return zv::Val(); + realReturnType = zv::Val::adopt(voidType); + } else if (isGet) { + realReturnType = thisGetFunctionType(nativePropertyTypeNode, false, false); + if (UNEXPECTED(realReturnType.isUndef())) return zv::Val(); + phpDocReturnType = transformedArgumentOrNull(phpDocPropertyType); + if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); + } else { + pt_throw_should_not_happen(); + return zv::Val(); + } + + zv::Val realParameterTypes = getRealParameterTypes(hookObject); + if (UNEXPECTED(realParameterTypes.isUndef())) return zv::Val(); + + Args<28> a; + PT_MS_ARG_OWNED(a, thisGetClassReflection()); + { + zval hookZv; + ZVAL_OBJ(&hookZv, hookObject); + a.add(zv::Ref(&hookZv)); + } + a.addOwned(zv::Val::string(propertyName)); + PT_MS_ARG_OWNED(a, thisGetFile()); + { + zval emptyMap; + if (UNEXPECTED(!pt_template_type_map_empty(&emptyMap))) return zv::Val(); + a.addOwned(zv::Val::adopt(emptyMap)); + } + a.addOwned(std::move(realParameterTypes)); + a.addOwned(zv::Val(std::move(parameterTypes))); + a.addEmptyArray(); + PT_MS_ARG_OWNED(a, getParameterAttributes(hookObject)); + a.addOwned(std::move(realReturnType)); + a.addOwned(std::move(phpDocReturnType)); + PT_MS_ARG_OWNED(a, transformedArgumentOrNull(throwType)); + a.add(zv::Ref(deprecatedDescription)); + a.addBool(isDeprecated); + a.addBool(false); + a.addBool(false); + a.add(zv::Ref(isPure)); + a.addBool(true); + PT_MS_ARG_OWNED(a, emptyAssertions()); + a.addNull(); + a.add(zv::Ref(phpDocComment)); + a.add(zv::Ref(resolvedPhpDocBlock)); + a.addEmptyArray(); + a.addEmptyArray(); + a.addEmptyArray(); + a.addBool(false); + { + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(classReflection.raw())); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Ref attrGroups = nodeProp(hookObject, PT_LC("attrGroups")); + if (UNEXPECTED(attrGroups.raw() == NULL)) return zv::Val(); + PT_MS_ARG_OWNED(a, attributesFromAttrGroups(attrGroups.deref().raw(), className.raw(), hookObject)); + } + a.addEmptyArray(); + + zv::Val reflection = pt_type_new(PT_CLASS_PHP_METHOD_FROM_PARSER_NODE_REFLECTION, a.count, a.argv); + if (UNEXPECTED(reflection.isUndef())) return zv::Val(); + return enterFunctionLike(reflection.raw(), true); + } + + /** @api (twin 2237) */ + zv::Val enterFunction(zval *function, zval *templateTypeMap, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *throwType, zval *deprecatedDescription, bool isDeprecated, bool isInternal, zval *isPure, bool acceptsNamedArguments, zval *asserts, zval *phpDocComment, zval *parameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *phpDocClosureThisTypeParameters, zval *pureUnlessCallableIsImpureParameters) + { + zend_object *functionObject = Z_OBJ_P(function); + Args<28> a; + a.add(zv::Ref(function)); + PT_MS_ARG_OWNED(a, thisGetFile()); + a.add(zv::Ref(templateTypeMap)); + PT_MS_ARG_OWNED(a, getRealParameterTypes(functionObject)); + PT_MS_ARG_OWNED(a, mapToArgument(phpDocParameterTypes, false)); + PT_MS_ARG_OWNED(a, getRealParameterDefaultValues(functionObject)); + PT_MS_ARG_OWNED(a, getParameterAttributes(functionObject)); + { + zv::Ref returnType = nodeProp(functionObject, PT_LC("returnType")); + if (UNEXPECTED(returnType.raw() == NULL)) return zv::Val(); + PT_MS_ARG_OWNED(a, thisGetFunctionType(returnType.deref().raw(), returnType.deref().isNull(), false)); + } + if (Z_TYPE_P(phpDocReturnType) == IS_NULL) { + a.addNull(); + } else { + PT_MS_ARG_OWNED(a, pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("toargument"), 1, phpDocReturnType)); + } + a.add(zv::Ref(throwType)); + a.add(zv::Ref(deprecatedDescription)); + a.addBool(isDeprecated); + a.addBool(isInternal); + a.add(zv::Ref(isPure)); + a.addBool(acceptsNamedArguments); + if (Z_TYPE_P(asserts) == IS_NULL) { + PT_MS_ARG_OWNED(a, emptyAssertions()); + } else { + a.add(zv::Ref(asserts)); + } + a.add(zv::Ref(phpDocComment)); + PT_MS_ARG_OWNED(a, mapToArgument(parameterOutTypes, false)); + a.add(zv::Ref(immediatelyInvokedCallableParameters)); + a.add(zv::Ref(phpDocClosureThisTypeParameters)); + { + zval nullClassName; + ZVAL_NULL(&nullClassName); + zv::Ref attrGroups = nodeProp(functionObject, PT_LC("attrGroups")); + if (UNEXPECTED(attrGroups.raw() == NULL)) return zv::Val(); + PT_MS_ARG_OWNED(a, attributesFromAttrGroups(attrGroups.deref().raw(), &nullClassName, functionObject)); + } + a.add(zv::Ref(pureUnlessCallableIsImpureParameters)); + + zv::Val reflection = pt_type_new(PT_CLASS_PHP_FUNCTION_FROM_PARSER_NODE_REFLECTION, a.count, a.argv); + if (UNEXPECTED(reflection.isUndef())) return zv::Val(); + return enterFunctionLike(reflection.raw(), false); + } + + /* private (twin 2285) */ + zv::Val enterFunctionLike(zval *functionReflection, bool preserveConstructorScope) + { + zend_object *reflection = Z_OBJ_P(functionReflection); + zv::Val functionParameters = pt_type_call(reflection, PT_LC("getparameters"), 0, NULL); + if (UNEXPECTED(functionParameters.isUndef())) return zv::Val(); + if (UNEXPECTED(!functionParameters.ref().isArray())) { + zend_type_error("phpstan_turbo: getParameters() must return array, %s returned", zend_zval_value_name(functionParameters.raw())); + return zv::Val(); + } + + zv::Arr parametersByName = zv::Arr::create(zend_hash_num_elements(functionParameters.ref().asArrayTable())); + for (auto entry : zv::ArrRef(functionParameters.raw())) { + zv::Ref parameter = entry.value().deref(); + if (UNEXPECTED(!parameter.isObject())) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(parameter.raw())); + return zv::Val(); + } + zv::Val name = pt_type_call(parameter.asObject(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *key = zval_get_string(name.raw()); + if (UNEXPECTED(key == NULL)) return zv::Val(); + zval copy; + ZVAL_COPY(©, parameter.raw()); + zend_symtable_update(parametersByName.table(), key, ©); + zend_string_release(key); + } + + zv::Arr expressionTypes; + zv::Arr nativeExpressionTypes; + if (preserveConstructorScope) { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes"))) { + return zv::Val(); + } + expressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())); + nativeExpressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw())); + } else { + expressionTypes = zv::Arr::create(0); + nativeExpressionTypes = zv::Arr::create(0); + } + zv::Arr conditionalTypes = zv::Arr::create(0); + + for (auto entry : zv::ArrRef(functionParameters.raw())) { + zv::Ref parameter = entry.value().deref(); + zend_object *parameterObject = parameter.asObject(); + zv::Val parameterType = pt_type_call(parameterObject, PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(parameterType.isUndef())) return zv::Val(); + zv::Val name = pt_type_call(parameterObject, PT_LC("getname"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return zv::Val(); + zend_string *parameterNameRaw = zval_get_string(name.raw()); + if (UNEXPECTED(parameterNameRaw == NULL)) return zv::Val(); + zv::Str parameterName = zv::Str::adopt(parameterNameRaw); + + if (parameterType.ref().isObject() && parameterType.ref().instanceOf(pt_ce_conditional_type_for_parameter)) { + if (UNEXPECTED(!addConditionalParameterTypes(conditionalTypes, parametersByName, parameterType.ref(), parameterName.get()))) return zv::Val(); + } + + zv::Str paramExprString = zv::Str::adopt(zend_strpprintf(0, "$%s", ZSTR_VAL(parameterName.get()))); + zv::Val isVariadic = pt_type_call(parameterObject, PT_LC("isvariadic"), 0, NULL); + if (UNEXPECTED(isVariadic.isUndef())) return zv::Val(); + if (zend_is_true(isVariadic.raw())) { + bool named; + if (UNEXPECTED(!acceptsNamedArgumentsHere(reflection, named))) return zv::Val(); + zv::Val wrapped = variadicArrayType(parameterType.raw(), named); + if (UNEXPECTED(wrapped.isUndef())) return zv::Val(); + parameterType = std::move(wrapped); + } + + zv::Val parameterNode = newVariable(ZSTR_VAL(parameterName.get()), ZSTR_LEN(parameterName.get())); + if (UNEXPECTED(parameterNode.isUndef())) return zv::Val(); + zval holderZv; + pt_holder_create(&holderZv, parameterNode.raw(), parameterType.raw(), PT_TRI_YES); + expressionTypes.set(paramExprString.get(), zv::Val::adopt(holderZv)); + + zv::Val originalValueName = zv::Val::string(parameterName.get()); + zv::Val parameterOriginalValueExpr = pt_type_new(PT_CLASS_PARAMETER_VARIABLE_ORIGINAL_VALUE_EXPR, 1, originalValueName.raw()); + if (UNEXPECTED(parameterOriginalValueExpr.isUndef())) return zv::Val(); + zv::Val originalValueExprString = thisGetNodeKey(parameterOriginalValueExpr.raw()); + if (UNEXPECTED(originalValueExprString.isUndef())) return zv::Val(); + zend_string *originalValueKeyRaw = zval_get_string(originalValueExprString.raw()); + if (UNEXPECTED(originalValueKeyRaw == NULL)) return zv::Val(); + zv::Str originalValueKey = zv::Str::adopt(originalValueKeyRaw); + pt_holder_create(&holderZv, parameterOriginalValueExpr.raw(), parameterType.raw(), PT_TRI_YES); + expressionTypes.set(originalValueKey.get(), zv::Val::adopt(holderZv)); + + zv::Val nativeParameterType = pt_type_call(parameterObject, PT_LC("getnativetype"), 0, NULL); + if (UNEXPECTED(nativeParameterType.isUndef())) return zv::Val(); + if (zend_is_true(isVariadic.raw())) { + bool named; + if (UNEXPECTED(!acceptsNamedArgumentsHere(reflection, named))) return zv::Val(); + zv::Val wrapped = variadicArrayType(nativeParameterType.raw(), named); + if (UNEXPECTED(wrapped.isUndef())) return zv::Val(); + nativeParameterType = std::move(wrapped); + } + pt_holder_create(&holderZv, parameterNode.raw(), nativeParameterType.raw(), PT_TRI_YES); + nativeExpressionTypes.set(paramExprString.get(), zv::Val::adopt(holderZv)); + pt_holder_create(&holderZv, parameterOriginalValueExpr.raw(), nativeParameterType.raw(), PT_TRI_YES); + nativeExpressionTypes.set(originalValueKey.get(), zv::Val::adopt(holderZv)); + } + + CreateArgs a; + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONTEXT, "context"))) return zv::Val(); + a.set(CreateArgs::CONTEXT, slot(PT_MS_PROP_CONTEXT)); + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + a.set(CreateArgs::FUNCTION, zv::Ref(functionReflection)); + zv::Val ns = thisGetNamespace(); + if (UNEXPECTED(ns.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::NAMESPACE_, std::move(ns)); + zv::Val constantTypes = getConstantTypes(); + if (UNEXPECTED(constantTypes.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::EXPRESSION_TYPES, arrayMerge(Z_ARRVAL_P(constantTypes.raw()), expressionTypes.table())); + zv::Val nativeConstantTypes = getNativeConstantTypes(); + if (UNEXPECTED(nativeConstantTypes.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, arrayMerge(Z_ARRVAL_P(nativeConstantTypes.raw()), nativeExpressionTypes.table())); + a.setOwned(CreateArgs::CONDITIONAL_EXPRESSIONS, zv::Val(std::move(conditionalTypes))); + a.setEmptyArray(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES); + a.setNull(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.setNull(CreateArgs::PARENT_SCOPE); + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, false); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) return zv::Val(); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + /* !$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes() */ + bool acceptsNamedArgumentsHere(zend_object *functionReflection, bool &out) + { + bool supportsNamedArguments; + if (UNEXPECTED(!phpVersionSupportsNamedArguments(supportsNamedArguments))) return false; + if (!supportsNamedArguments) { + out = false; + return true; + } + zend_long accepts = pt_type_call_trinary(functionReflection, PT_LC("acceptsnamedarguments"), 0, NULL); + if (UNEXPECTED(accepts < 0)) return false; + out = accepts == PT_TRI_YES; + return true; + } + + /* the two ConditionalExpressionHolders a ConditionalTypeForParameter + * parameter adds to $conditionalTypes; false = pending exception */ + [[nodiscard]] bool addConditionalParameterTypes(zv::Arr &conditionalTypes, zv::Arr ¶metersByName, zv::Ref parameterType, zend_string *parameterName) + { + zend_object *conditional = parameterType.asObject(); + zv::Val targetParameterName = pt_type_call(conditional, PT_LC("getparametername"), 0, NULL); + if (UNEXPECTED(targetParameterName.isUndef())) return false; + zend_string *rawTargetName = zval_get_string(targetParameterName.raw()); + if (UNEXPECTED(rawTargetName == NULL)) return false; + zv::Str fullTargetName = zv::Str::adopt(rawTargetName); + /* substr($parameterType->getParameterName(), 1) */ + zv::Str targetName = zv::Str::adopt(ZSTR_LEN(fullTargetName.get()) == 0 + ? zend_string_init("", 0, 0) + : zend_string_init(ZSTR_VAL(fullTargetName.get()) + 1, ZSTR_LEN(fullTargetName.get()) - 1, 0)); + zval *targetParameter = zend_symtable_find(parametersByName.table(), targetName.get()); + if (targetParameter == NULL) return true; + if (UNEXPECTED(Z_TYPE_P(targetParameter) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getType() on %s", zend_zval_value_name(targetParameter)); + return false; + } + + zv::Val isNegated = pt_type_call(conditional, PT_LC("isnegated"), 0, NULL); + if (UNEXPECTED(isNegated.isUndef())) return false; + bool negated = zend_is_true(isNegated.raw()); + zv::Val ifType = negated + ? pt_type_call(conditional, PT_LC("getelse"), 0, NULL) + : pt_type_call(conditional, PT_LC("getif"), 0, NULL); + if (UNEXPECTED(ifType.isUndef())) return false; + zv::Val elseType = negated + ? pt_type_call(conditional, PT_LC("getif"), 0, NULL) + : pt_type_call(conditional, PT_LC("getelse"), 0, NULL); + if (UNEXPECTED(elseType.isUndef())) return false; + zv::Val targetParameterType = pt_type_call(Z_OBJ_P(targetParameter), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(targetParameterType.isUndef())) return false; + zv::Val target = pt_type_call(conditional, PT_LC("gettarget"), 0, NULL); + if (UNEXPECTED(target.isUndef())) return false; + + zv::Args intersectArgs{targetParameterType.raw(), target.raw()}; + zv::Val intersected = pt_type_combinator_intersect(2, intersectArgs); + if (UNEXPECTED(intersected.isUndef())) return false; + zv::Val removed = pt_type_combinator_remove(targetParameterType.raw(), target.raw()); + if (UNEXPECTED(removed.isUndef())) return false; + + zv::Val targetVariable = newVariable(ZSTR_VAL(targetName.get()), ZSTR_LEN(targetName.get())); + if (UNEXPECTED(targetVariable.isUndef())) return false; + zv::Val parameterVariable = newVariable(ZSTR_VAL(parameterName), ZSTR_LEN(parameterName)); + if (UNEXPECTED(parameterVariable.isUndef())) return false; + + zv::Str key = zv::Str::adopt(zend_strpprintf(0, "$%s", ZSTR_VAL(parameterName))); + zval *bucket = zend_symtable_find(conditionalTypes.table(), key.get()); + zv::Arr holders; + if (bucket != NULL && Z_TYPE_P(bucket) == IS_ARRAY) { + holders = zv::Arr::copyOfTable(Z_ARRVAL_P(bucket)); + holders.separate(); + } else { + holders = zv::Arr::create(2); + } + + for (uint32_t i = 0; i < 2; i++) { + zval conditionHolder; + pt_holder_create(&conditionHolder, targetVariable.raw(), i == 0 ? intersected.raw() : removed.raw(), PT_TRI_YES); + zv::Val condition = zv::Val::adopt(conditionHolder); + zv::Arr conditions = zv::Arr::create(1); + conditions.set(fullTargetName.get(), std::move(condition)); + zval typeHolder; + pt_holder_create(&typeHolder, parameterVariable.raw(), i == 0 ? ifType.raw() : elseType.raw(), PT_TRI_YES); + zv::Val holderType = zv::Val::adopt(typeHolder); + zv::Args holderArgs{conditions.raw(), holderType.raw()}; + zv::Val holder = pt_type_new_ce(pt_ce_cond_expr_holder, 2, holderArgs); + if (UNEXPECTED(holder.isUndef())) return false; + zv::Val holderKey = pt_type_call(Z_OBJ_P(holder.raw()), PT_LC("getkey"), 0, NULL); + if (UNEXPECTED(holderKey.isUndef())) return false; + zend_string *rawHolderKey = zval_get_string(holderKey.raw()); + if (UNEXPECTED(rawHolderKey == NULL)) return false; + holders.set(rawHolderKey, std::move(holder)); + zend_string_release(rawHolderKey); + } + + conditionalTypes.set(key.get(), zv::Val(std::move(holders))); + return true; + } + + /** @api (twin 2370) */ + zv::Val enterNamespace(zend_string *namespaceName) + { + zv::Val context = contextCall(PT_LC("beginfile"), 0, NULL); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + CreateArgs a; + a.setOwned(CreateArgs::CONTEXT, std::move(context)); + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + a.setNull(CreateArgs::FUNCTION); + a.setOwned(CreateArgs::NAMESPACE_, zv::Val::string(namespaceName)); + a.setEmptyArray(CreateArgs::EXPRESSION_TYPES); + a.setEmptyArray(CreateArgs::NATIVE_EXPRESSION_TYPES); + a.setEmptyArray(CreateArgs::CONDITIONAL_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES); + a.setNull(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.setNull(CreateArgs::PARENT_SCOPE); + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, false); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) return zv::Val(); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + /* }}} */ + + /* {{{ twin 2385-2558: the closure-bind family */ + + /* the two tables with $this set to the given types (IS_NULL drops the + * entry), as enterClosureBind() and enterClosureCall() build them */ + bool bindThisTables(TablePair &tables, zval *thisType, zval *nativeThisType) + { + zv::Str ownedKey = zv::Str::adopt(zend_string_init(PT_LC("$this"), 0)); + zend_string *key = ownedKey.get(); + for (uint32_t i = 0; i < 2; i++) { + zval *type = i == 0 ? thisType : nativeThisType; + zv::Arr &table = i == 0 ? tables.expressionTypes : tables.nativeExpressionTypes; + table.separate(); + if (Z_TYPE_P(type) == IS_NULL) { + zend_symtable_del(table.table(), key); + continue; + } + /* a Variable of its own per table, as the twin writes it */ + zv::Val thisVariable = newVariable(PT_LC("this")); + if (UNEXPECTED(thisVariable.isUndef())) return false; + zval holder; + pt_holder_create(&holder, thisVariable.raw(), type, PT_TRI_YES); + zend_symtable_update(table.table(), key, &holder); + } + return true; + } + + /* create(...) with the twin's closure-bind argument list: the tables, + * the given bind scope classes, everything else from $this */ + zv::Val createWithTablesAndBindScopeClasses(TablePair &tables, zv::Val scopeClasses) + { + CreateArgs a; + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONTEXT, "context") || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") || !requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) { + return zv::Val(); + } + a.set(CreateArgs::CONTEXT, slot(PT_MS_PROP_CONTEXT)); + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + zv::Val function = thisGetFunction(); + if (UNEXPECTED(function.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::FUNCTION, std::move(function)); + zv::Val ns = thisGetNamespace(); + if (UNEXPECTED(ns.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::NAMESPACE_, std::move(ns)); + a.setOwned(CreateArgs::EXPRESSION_TYPES, zv::Val(std::move(tables.expressionTypes))); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Val(std::move(tables.nativeExpressionTypes))); + a.set(CreateArgs::CONDITIONAL_EXPRESSIONS, slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS)); + a.setOwned(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, std::move(scopeClasses)); + a.set(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, slot(PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION)); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.setNull(CreateArgs::PARENT_SCOPE); + a.setBool(CreateArgs::NATIVE_TYPES_PROMOTED, false); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + zv::Val enterClosureBind(zval *thisType, zval *nativeThisType, zval *scopeClasses) + { + TablePair tables(*this); + if (UNEXPECTED(!bindThisTables(tables, thisType, nativeThisType))) return zv::Val(); + + /* if ($scopeClasses === ['static'] && $this->isInClass()) { $scopeClasses = [$this->getClassReflection()->getName()]; } */ + zv::Val ownScopeClasses = zv::Val::copyOf(zv::Ref(scopeClasses)); + if (isSingleStringList(Z_ARRVAL_P(scopeClasses), PT_LC("static"))) { + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (inClass) { + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(classReflection.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(classReflection.raw())); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Arr single = zv::Arr::create(1); + single.push(std::move(className)); + ownScopeClasses = zv::Val(std::move(single)); + } + } + + return createWithTablesAndBindScopeClasses(tables, std::move(ownScopeClasses)); + } + + zv::Val restoreOriginalScopeAfterClosureBind(zend_object *originalScopeObject) + { + MutatingScope originalScope(originalScopeObject); + if (UNEXPECTED(!originalScope.requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !originalScope.requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !originalScope.requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) { + return zv::Val(); + } + TablePair tables(*this); + zend_string *key = zend_string_init(PT_LC("$this"), 0); + zv::Str ownedKey = zv::Str::adopt(key); + for (uint32_t i = 0; i < 2; i++) { + uint32_t sourceSlot = i == 0 ? PT_MS_PROP_EXPRESSION_TYPES : PT_MS_PROP_NATIVE_EXPRESSION_TYPES; + zv::Arr &table = i == 0 ? tables.expressionTypes : tables.nativeExpressionTypes; + zval *holder = zend_symtable_find(Z_ARRVAL_P(originalScope.slot(sourceSlot).raw()), key); + table.separate(); + if (holder == NULL || Z_TYPE_P(holder) == IS_NULL) { + zend_symtable_del(table.table(), key); + continue; + } + zval copy; + ZVAL_COPY(©, holder); + zend_symtable_update(table.table(), key, ©); + } + + return createWithTablesAndBindScopeClasses(tables, zv::Val::copyOf(originalScope.slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES))); + } + + zv::Val restoreThis(zend_object *restoreThisScopeObject) + { + MutatingScope restoreThisScope(restoreThisScopeObject); + if (UNEXPECTED(!restoreThisScope.requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !restoreThisScope.requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !restoreThisScope.requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) { + return zv::Val(); + } + TablePair tables(*this); + zv::Val inClassResult = pt_type_call(restoreThisScopeObject, PT_LC("isinclass"), 0, NULL); + if (UNEXPECTED(inClassResult.isUndef())) return zv::Val(); + if (zend_is_true(inClassResult.raw())) { + for (uint32_t i = 0; i < 2; i++) { + uint32_t sourceSlot = i == 0 ? PT_MS_PROP_EXPRESSION_TYPES : PT_MS_PROP_NATIVE_EXPRESSION_TYPES; + zv::Arr &table = i == 0 ? tables.expressionTypes : tables.nativeExpressionTypes; + for (auto entry : zv::ArrRef(restoreThisScope.slot(sourceSlot).raw())) { + zend_string *exprString = entry.stringKeyOrNull(); + if (exprString == NULL || ZSTR_LEN(exprString) < 5 || memcmp(ZSTR_VAL(exprString), "$this", 5) != 0) continue; + table.separate(); + zval copy; + ZVAL_COPY(©, entry.value().raw()); + zend_symtable_update(table.table(), exprString, ©); + } + } + } else { + zv::Str thisKey = zv::Str::adopt(zend_string_init(PT_LC("$this"), 0)); + tables.unset(thisKey.get()); + } + + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + zv::Val function = thisGetFunction(); + if (UNEXPECTED(function.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::FUNCTION, std::move(function)); + zv::Val ns = thisGetNamespace(); + if (UNEXPECTED(ns.isUndef())) return zv::Val(); + a.setOwned(CreateArgs::NAMESPACE_, std::move(ns)); + a.setOwned(CreateArgs::EXPRESSION_TYPES, zv::Val(std::move(tables.expressionTypes))); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Val(std::move(tables.nativeExpressionTypes))); + a.set(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, restoreThisScope.slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES)); + /* the twin passes $this->inFirstLevelStatement, not the getter */ + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + return scopeFactoryCreate(a); + } + + zv::Val enterClosureCall(zval *thisType, zval *nativeThisType) + { + TablePair tables(*this); + if (UNEXPECTED(!bindThisTables(tables, thisType, nativeThisType))) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(thisType) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getObjectClassNames() on %s", zend_zval_value_name(thisType)); + return zv::Val(); + } + zv::Val classNames = pt_type_op(Z_OBJ_P(thisType), PT_OP_GET_OBJECT_CLASS_NAMES, 0, NULL); + if (UNEXPECTED(classNames.isUndef())) return zv::Val(); + return createWithTablesAndBindScopeClasses(tables, std::move(classNames)); + } + + /** @api */ + bool isInClosureBind(bool &out) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, "inClosureBindScopeClasses"))) return false; + out = zend_hash_num_elements(Z_ARRVAL_P(slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES).raw())) != 0; + return true; + } + + zv::Val withClosureBindScopeClasses(zval *scopeClasses) + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + a.set(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, zv::Ref(scopeClasses)); + return scopeFactoryCreate(a); + } + + /* }}} */ + + /* }}} */ + + /* {{{ the $this-dispatch helpers of the assignment family */ + + static void optionalArg(zval *out, zval *value) + { + if (value == NULL) { + ZVAL_NULL(out); + } else { + ZVAL_COPY_VALUE(out, value); + } + } + + /* $this->phpVersion->() as a bool; false = pending exception */ + [[nodiscard]] bool phpVersionBool(const char *lcname, size_t len, bool &out) + { + zv::Ref phpVersion = slot(PT_MS_PROP_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) { + (void) uninitializedProperty("phpVersion"); + return false; + } + return otherCallBool(phpVersion.asObject(), lcname, len, out); + } + + zv::Val thisGetCurrentExpressionResultStorage() + { + return thisCall(PT_LC("getcurrentexpressionresultstorage"), msGetCurrentExpressionResultStorage, 0, NULL, [&]() { return getCurrentExpressionResultStorage(); }); + } + + zv::Val thisEnterAnonymousFunctionWithoutReflection(zend_object *closure, zval *callableParameters, zval *nativeCallableParameters) + { + zval args[3]; + ZVAL_OBJ(&args[0], closure); + optionalArg(&args[1], callableParameters); + optionalArg(&args[2], nativeCallableParameters); + return thisCall(PT_LC("enteranonymousfunctionwithoutreflection"), msEnterAnonymousFunctionWithoutReflection, 3, args, [&]() { + return enterAnonymousFunctionWithoutReflection(closure, callableParameters, nativeCallableParameters); + }); + } + + zv::Val thisEnterArrowFunctionWithoutReflection(zend_object *arrowFunction, zval *callableParameters, zval *nativeCallableParameters) + { + zval args[3]; + ZVAL_OBJ(&args[0], arrowFunction); + optionalArg(&args[1], callableParameters); + optionalArg(&args[2], nativeCallableParameters); + return thisCall(PT_LC("enterarrowfunctionwithoutreflection"), msEnterArrowFunctionWithoutReflection, 3, args, [&]() { + return enterArrowFunctionWithoutReflection(arrowFunction, callableParameters, nativeCallableParameters); + }); + } + + /* $this->assignVariable($name, $type, $nativeType, $certainty, $intertwinedPropagatedFrom) */ + zv::Val thisAssignVariable(zval *args) + { + return thisCall(PT_LC("assignvariable"), msAssignVariable, 5, args, [&]() { + return assignVariable(Z_STR(args[0]), &args[1], &args[2], &args[3], &args[4]); + }); + } + + zv::Val thisAssignExpression(zval *expr, zval *type, zval *nativeType) + { + zv::Args args{expr, type, nativeType}; + return thisCall(PT_LC("assignexpression"), msAssignExpression, 3, args, [&]() { return assignExpression(Z_OBJ_P(expr), type, nativeType); }); + } + + zv::Val thisSpecifyExpressionType(zval *expr, zval *type, zval *nativeType, zval *certainty) + { + zv::Args args{expr, type, nativeType, certainty}; + return thisCall(PT_LC("specifyexpressiontype"), msSpecifyExpressionType, 4, args, [&]() { return specifyExpressionType(Z_OBJ_P(expr), type, nativeType, certainty); }); + } + + zv::Val thisApplySpecifiedTypes(zval *specifiedTypes) + { + return thisCall(PT_LC("applyspecifiedtypes"), msApplySpecifiedTypes, 1, specifiedTypes, [&]() { return applySpecifiedTypes(specifiedTypes); }); + } + + zv::Val thisInvalidateExpression(zval *expr, bool requireMoreCharacters, zval *invalidatingClass, bool keepPropertyFetches) + { + zval args[4]; + ZVAL_COPY_VALUE(&args[0], expr); + ZVAL_BOOL(&args[1], requireMoreCharacters); + optionalArg(&args[2], invalidatingClass); + ZVAL_BOOL(&args[3], keepPropertyFetches); + return thisCall(PT_LC("invalidateexpression"), msInvalidateExpression, 4, args, [&]() { + return invalidateExpression(expr, requireMoreCharacters, invalidatingClass, keepPropertyFetches); + }); + } + + /* }}} */ + + /* {{{ twin 2560-3990: the anonymous- and arrow-function + * entries, the assignment / invalidation family and the specification + * machinery */ + + /* {{{ foreign scope objects: the twin writes $scope = $this->a()->b(), + * so every call after the first runs on whatever the factory answered + * — any MutatingScope, a PHP twin under the differential prefix */ + + /* a table property of another scope object: its own slot when the + * object is (a subclass of) the native class, resolved by name + * otherwise; NULL with an Error pending when it has no such property */ + [[nodiscard]] static zval *otherProp(zend_object *object, uint32_t nativeSlot, const char *name, size_t len) + { + if (EXPECTED(pt_ce_mutating_scope != NULL && instanceof_function(object->ce, pt_ce_mutating_scope))) return OBJ_PROP_NUM(object, nativeSlot); + int32_t offset = pt_instance_prop_offset(object->ce, name, len); + if (UNEXPECTED(offset < 0)) { + zend_throw_error(NULL, "phpstan_turbo: %s has no property $%s", ZSTR_VAL(object->ce->name), name); + return NULL; + } + return OBJ_PROP(object, (uint32_t) offset); + } + + /* $other->() answering a bool; false = pending exception */ + [[nodiscard]] static bool otherCallBool(zend_object *object, const char *lcname, size_t len, bool &out) + { + zv::Val result = pt_type_call(object, lcname, len, 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* a private method of the twin on another scope object: the native body + * when the object is (a subclass of) the native class — private methods + * are never registered, so the engine would not find one there — the + * object's own method otherwise (the PHP twin under the prefix) */ + template + static zv::Val otherPrivate(zend_object *object, const char *lcname, size_t len, uint32_t argc, zval *argv, Direct direct) + { + if (EXPECTED(pt_ce_mutating_scope != NULL && instanceof_function(object->ce, pt_ce_mutating_scope))) { + MutatingScope other(object); + return direct(other); + } + return pt_type_call(object, lcname, len, argc, argv); + } + + /* $other->getType($expr) / ->getNativeType($expr) */ + static zv::Val otherGetType(zend_object *object, zval *expr, bool native) + { + if (native) return pt_type_call(object, PT_LC("getnativetype"), 1, expr); + return pt_type_call(object, PT_LC("gettype"), 1, expr); + } + + /* }}} */ + + /* {{{ the NodeFinder walks of these entries: findInstanceOf() collects + * every match in the same pre-order pt_find_first_recursive() visits, + * findFirst() stops at the first one */ + + struct InstanceOfCtx + { + pt_find_ctx base; + zend_class_entry *ce; + zv::Arr *found; + }; + + static bool instanceOfCollector(zend_object *node, void *vctx) + { + InstanceOfCtx *ctx = (InstanceOfCtx *) vctx; + if (instanceof_function(node->ce, ctx->ce)) { + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + ctx->found->push(zv::Ref(&nodeZv)); + } + return false; + } + + /* (new NodeFinder())->findInstanceOf([$expr], $className) */ + static bool findInstancesOf(zend_object *expr, int classIdx, zv::Arr &out) + { + zend_class_entry *ce = pt_class(classIdx); + if (UNEXPECTED(ce == NULL)) return false; + InstanceOfCtx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.ce = ce; + ctx.found = &out; + pt_find_first_recursive(expr, instanceOfCollector, &ctx); + return EXPECTED(!ctx.base.failed && EG(exception) == NULL); + } + + struct StaticExprCtx + { + pt_find_ctx base; + zend_class_entry *staticCall; + zend_class_entry *staticPropertyFetch; + }; + + static bool staticExprMatcher(zend_object *node, void *vctx) + { + StaticExprCtx *ctx = (StaticExprCtx *) vctx; + return instanceof_function(node->ce, ctx->staticCall) || instanceof_function(node->ce, ctx->staticPropertyFetch); + } + + /* }}} */ + + /** @api (twin 2560) */ + zv::Val enterAnonymousFunction(zend_object *closure, zval *callableParameters, zval *nativeCallableParameters) + { + zv::Val closureTypeResolver = containerGetByType(PT_LC("PHPStan\\Analyser\\ExprHandler\\Helper\\ClosureTypeResolver")); + if (UNEXPECTED(closureTypeResolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(closureTypeResolver, "getClosureType"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Val storage = thisGetCurrentExpressionResultStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + zv::Args resolverArgs{thisZval(), closure, true, storage.raw()}; + zv::Val anonymousFunctionReflection = pt_type_call(resolverObject, PT_LC("getclosuretype"), 4, resolverArgs); + if (UNEXPECTED(anonymousFunctionReflection.isUndef())) return zv::Val(); + + zv::Val scope = thisEnterAnonymousFunctionWithoutReflection(closure, callableParameters, nativeCallableParameters); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "isDeclareStrictTypes"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + return createForFunctionEntry(scopeObject, std::move(anonymousFunctionReflection), false); + } + + /* the create() argument list enterAnonymousFunction() (2560) and + * enterArrowFunction() (2791) build out of the scope their + * *WithoutReflection() sibling answered — the arrow function keeps that + * scope's afterExtractCall and parentScope, the closure resets them */ + zv::Val createForFunctionEntry(zend_object *scopeObject, zv::Val anonymousFunctionReflection, bool fromArrowFunction) + { + CreateArgs a; + zval *context = otherProp(scopeObject, PT_MS_PROP_CONTEXT, PT_LC("context")); + zval *expressionTypes = otherProp(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + zval *nativeExpressionTypes = otherProp(scopeObject, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + zval *conditionalExpressions = otherProp(scopeObject, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + zval *inClosureBindScopeClasses = otherProp(scopeObject, PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, PT_LC("inClosureBindScopeClasses")); + if (UNEXPECTED(context == NULL || expressionTypes == NULL || nativeExpressionTypes == NULL || conditionalExpressions == NULL || inClosureBindScopeClasses == NULL)) { + return zv::Val(); + } + a.set(CreateArgs::CONTEXT, zv::Ref(context)); + bool declareStrictTypes; + if (UNEXPECTED(!otherCallBool(scopeObject, PT_LC("isdeclarestricttypes"), declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + PT_MS_ARG_CREATE(a, CreateArgs::FUNCTION, pt_type_call(scopeObject, PT_LC("getfunction"), 0, NULL)); + PT_MS_ARG_CREATE(a, CreateArgs::NAMESPACE_, pt_type_call(scopeObject, PT_LC("getnamespace"), 0, NULL)); + a.set(CreateArgs::EXPRESSION_TYPES, zv::Ref(expressionTypes)); + a.set(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Ref(nativeExpressionTypes)); + a.set(CreateArgs::CONDITIONAL_EXPRESSIONS, zv::Ref(conditionalExpressions)); + a.set(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, zv::Ref(inClosureBindScopeClasses)); + a.setOwned(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, std::move(anonymousFunctionReflection)); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack") || !requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) { + return zv::Val(); + } + a.set(CreateArgs::IN_FUNCTION_CALLS_STACK, slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK)); + if (fromArrowFunction) { + zval *afterExtractCall = otherProp(scopeObject, PT_MS_PROP_AFTER_EXTRACT_CALL, PT_LC("afterExtractCall")); + zval *parentScope = otherProp(scopeObject, PT_MS_PROP_PARENT_SCOPE, PT_LC("parentScope")); + if (UNEXPECTED(afterExtractCall == NULL || parentScope == NULL)) return zv::Val(); + a.set(CreateArgs::AFTER_EXTRACT_CALL, zv::Ref(afterExtractCall)); + a.set(CreateArgs::PARENT_SCOPE, zv::Ref(parentScope)); + } else { + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.set(CreateArgs::PARENT_SCOPE, zv::Ref(thisZval())); + } + a.set(CreateArgs::NATIVE_TYPES_PROMOTED, slot(PT_MS_PROP_NATIVE_TYPES_PROMOTED)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + /* '$' . $name */ + static zend_string *dollarName(zend_string *name) + { + return zend_strpprintf(0, "$%s", ZSTR_VAL(name)); + } + + /* ExpressionTypeHolder::createYes($expr, $type) into $table[$key] */ + static void setHolder(zv::Arr &table, zend_string *key, zval *expr, zval *type, zend_long certainty) + { + zval holder; + pt_holder_create(&holder, expr, type, certainty); + table.set(key, zv::Val::adopt(holder)); + } + + /* the parameter tables of the anonymous- and arrow-function entries: + * $this->getFunctionType() narrowed by the callable parameter at $index + * where one is given; false = pending exception */ + [[nodiscard]] bool parameterTypes(zend_object *parameter, zval *callableParameters, zval *nativeCallableParameters, zend_long index, zv::Val ¶meterType, zv::Val &nativeParameterType) + { + zv::Ref typeNode = nodeProp(parameter, PT_LC("type")); + zv::Ref variadic = nodeProp(parameter, PT_LC("variadic")); + if (UNEXPECTED(typeNode.raw() == NULL || variadic.raw() == NULL)) return false; + zval parameterZv; + ZVAL_OBJ(¶meterZv, parameter); + bool isNullable; + if (UNEXPECTED(!thisIsParameterValueNullable(¶meterZv, isNullable))) return false; + parameterType = thisGetFunctionType(typeNode.deref().raw(), isNullable, zend_is_true(variadic.deref().raw())); + if (UNEXPECTED(parameterType.isUndef())) return false; + nativeParameterType = zv::Val::copyOf(parameterType.ref()); + if (callableParameters != NULL) { + zv::Val callableType = getCallableParameterType(parameter, callableParameters, index); + if (UNEXPECTED(callableType.isUndef())) return false; + parameterType = intersectButNotNever(parameterType.raw(), callableType.raw()); + if (UNEXPECTED(parameterType.isUndef())) return false; + } + if (nativeCallableParameters != NULL) { + zv::Val callableType = getCallableParameterType(parameter, nativeCallableParameters, index); + if (UNEXPECTED(callableType.isUndef())) return false; + nativeParameterType = intersectButNotNever(nativeParameterType.raw(), callableType.raw()); + if (UNEXPECTED(nativeParameterType.isUndef())) return false; + } + return true; + } + + /* (twin 2596) */ + zv::Val enterAnonymousFunctionWithoutReflection(zend_object *closure, zval *callableParameters, zval *nativeCallableParameters) + { + zv::Arr expressionTypes = zv::Arr::create(0); + zv::Arr nativeTypes = zv::Arr::create(0); + + zv::Ref params = nodeProp(closure, PT_LC("params")); + if (UNEXPECTED(params.raw() == NULL)) return zv::Val(); + for (auto entry : zv::ArrRef(params.deref().raw())) { + zv::Ref parameter = entry.value().deref(); + if (UNEXPECTED(!parameter.isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_string *name = parameterVariableName(parameter.asObject()); + if (UNEXPECTED(name == NULL)) return zv::Val(); + zv::Str key = zv::Str::adopt(dollarName(name)); + zv::Val parameterType, nativeParameterType; + if (UNEXPECTED(!parameterTypes(parameter.asObject(), callableParameters, nativeCallableParameters, (zend_long) entry.indexKey(), parameterType, nativeParameterType))) { + return zv::Val(); + } + zv::Ref var = nodeProp(parameter.asObject(), PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + setHolder(expressionTypes, key.get(), var.deref().raw(), parameterType.raw(), PT_TRI_YES); + setHolder(nativeTypes, key.get(), var.deref().raw(), nativeParameterType.raw(), PT_TRI_YES); + } + + zv::ScratchTable nonRefVariableNames(8); + zv::ScratchTable useVariableNames(8); + zval marker; + ZVAL_TRUE(&marker); + zv::Ref uses = nodeProp(closure, PT_LC("uses")); + if (UNEXPECTED(uses.raw() == NULL)) return zv::Val(); + for (auto entry : zv::ArrRef(uses.deref().raw())) { + zv::Ref use = entry.value().deref(); + if (UNEXPECTED(!use.isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Ref var = nodeProp(use.asObject(), PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + if (UNEXPECTED(!var.deref().isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Ref nameRef = nodeProp(var.deref().asObject(), PT_LC("name")); + if (UNEXPECTED(nameRef.raw() == NULL)) return zv::Val(); + if (UNEXPECTED(!nameRef.deref().isString())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_string *variableName = nameRef.deref().asString(); + zv::Str key = zv::Str::adopt(dollarName(variableName)); + zend_symtable_update(useVariableNames.table(), key.get(), &marker); + + zv::Ref byRef = nodeProp(use.asObject(), PT_LC("byRef")); + if (UNEXPECTED(byRef.raw() == NULL)) return zv::Val(); + if (zend_is_true(byRef.deref().raw())) { + zval mixedZv; + if (UNEXPECTED(!pt_mixed_type_new(&mixedZv))) return zv::Val(); + zv::Val mixedType = zv::Val::adopt(mixedZv); + zval holderZv; + pt_holder_create(&holderZv, var.deref().raw(), mixedType.raw(), PT_TRI_YES); + zv::Val holder = zv::Val::adopt(holderZv); + expressionTypes.set(key.get(), zv::Val::copyOf(holder.ref())); + nativeTypes.set(key.get(), std::move(holder)); + continue; + } + zend_symtable_update(nonRefVariableNames.table(), variableName, &marker); + + zval nameZv; + ZVAL_STR(&nameZv, variableName); + zv::Val variableType, variableNativeType; + zv::Val has = thisHasVariableType(&nameZv); + if (UNEXPECTED(has.isUndef())) return zv::Val(); + zend_long certainty = pt_type_trinary_value(has.raw()); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_NO) { + zval errorZv; + if (UNEXPECTED(!pt_error_type_new(&errorZv))) return zv::Val(); + variableType = zv::Val::adopt(errorZv); + zval nativeErrorZv; + if (UNEXPECTED(!pt_error_type_new(&nativeErrorZv))) return zv::Val(); + variableNativeType = zv::Val::adopt(nativeErrorZv); + } else { + variableType = thisGetVariableType(&nameZv); + if (UNEXPECTED(variableType.isUndef())) return zv::Val(); + /* a plain variable read is scope state — never priced via + * the node, which may not have been processed yet */ + zv::Val nativeScope = thisDoNotTreatPhpDocTypesAsCertain(); + if (UNEXPECTED(nativeScope.isUndef())) return zv::Val(); + zend_object *nativeScopeObject = requireObject(nativeScope, "hasVariableType"); + if (UNEXPECTED(nativeScopeObject == NULL)) return zv::Val(); + zv::Val nativeHas = pt_type_call(nativeScopeObject, PT_LC("hasvariabletype"), 1, &nameZv); + if (UNEXPECTED(nativeHas.isUndef())) return zv::Val(); + zend_long nativeCertainty = pt_type_trinary_value(nativeHas.raw()); + if (UNEXPECTED(nativeCertainty < 0)) return zv::Val(); + if (nativeCertainty == PT_TRI_NO) { + zval errorZv; + if (UNEXPECTED(!pt_error_type_new(&errorZv))) return zv::Val(); + variableNativeType = zv::Val::adopt(errorZv); + } else { + variableNativeType = pt_type_call(nativeScopeObject, PT_LC("getvariabletype"), 1, &nameZv); + if (UNEXPECTED(variableNativeType.isUndef())) return zv::Val(); + } + } + setHolder(expressionTypes, key.get(), var.deref().raw(), variableType.raw(), PT_TRI_YES); + setHolder(nativeTypes, key.get(), var.deref().raw(), variableNativeType.raw(), PT_TRI_YES); + } + + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + zv::Val nonStaticExpressions = invalidateStaticExpressions(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())); + if (UNEXPECTED(nonStaticExpressions.isUndef())) return zv::Val(); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + if (UNEXPECTED(variableCe == NULL)) return zv::Val(); + for (auto entry : zv::ArrRef(nonStaticExpressions.raw())) { + zend_string *exprString = entry.stringKeyOrNull(); + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + if (UNEXPECTED(!expr.ref().isObject())) { + zend_throw_error(NULL, "phpstan_turbo: ExpressionTypeHolder::getExpr() must return an object"); + return zv::Val(); + } + if (instanceof_function(expr.ref().asObject()->ce, variableCe)) continue; + zv::Arr variables = zv::Arr::create(0); + if (UNEXPECTED(!findInstancesOf(expr.ref().asObject(), PT_CLASS_VARIABLE, variables))) return zv::Val(); + if (zend_hash_num_elements(variables.table()) == 0) { + bool unchangeable; + if (UNEXPECTED(!expressionTypeIsUnchangeable(entry.value(), unchangeable))) return zv::Val(); + if (!unchangeable) continue; + } + bool skip = false; + for (auto variableEntry : variables.arrRef()) { + zv::Ref variableName = nodeProp(Z_OBJ_P(variableEntry.value().raw()), PT_LC("name")); + if (UNEXPECTED(variableName.raw() == NULL)) return zv::Val(); + if (!variableName.deref().isString() + || !zend_hash_exists(nonRefVariableNames.table(), variableName.deref().asString())) { + skip = true; + break; + } + } + if (skip || exprString == NULL) continue; + expressionTypes.set(exprString, zv::Val::copyOf(entry.value().deref())); + } + + zval thisNameZv; + ZVAL_STR(&thisNameZv, ZSTR_KNOWN(ZEND_STR_THIS)); + zv::Val hasThis = thisHasVariableType(&thisNameZv); + if (UNEXPECTED(hasThis.isUndef())) return zv::Val(); + zend_long thisCertainty = pt_type_trinary_value(hasThis.raw()); + if (UNEXPECTED(thisCertainty < 0)) return zv::Val(); + zv::Ref isStatic = nodeProp(closure, PT_LC("static")); + if (UNEXPECTED(isStatic.raw() == NULL)) return zv::Val(); + if (thisCertainty == PT_TRI_YES && !zend_is_true(isStatic.deref().raw())) { + zv::Val node = newVariable(PT_LC("this")); + if (UNEXPECTED(node.isUndef())) return zv::Val(); + zv::Val type = thisGetType(node.raw()); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + zv::Val nativeType = thisGetNativeType(node.raw()); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + zv::Str thisKey = zv::Str::adopt(zend_string_init(PT_LC("$this"), 0)); + setHolder(expressionTypes, thisKey.get(), node.raw(), type.raw(), PT_TRI_YES); + setHolder(nativeTypes, thisKey.get(), node.raw(), nativeType.raw(), PT_TRI_YES); + + bool supportsReadOnlyProperties; + if (UNEXPECTED(!phpVersionBool(PT_LC("supportsreadonlyproperties"), supportsReadOnlyProperties))) return zv::Val(); + if (supportsReadOnlyProperties) { + for (auto entry : zv::ArrRef(nonStaticExpressions.raw())) { + zend_string *exprString = entry.stringKeyOrNull(); + if (exprString == NULL) continue; + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + bool isPropertyFetch; + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_PROPERTY_FETCH, isPropertyFetch))) return zv::Val(); + if (!isPropertyFetch) continue; + bool readonly; + if (UNEXPECTED(!thisIsReadonlyPropertyFetch(expr.raw(), true, readonly))) return zv::Val(); + if (!readonly) continue; + expressionTypes.set(exprString, zv::Val::copyOf(entry.value().deref())); + } + } + } + + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) return zv::Val(); + zv::Arr filteredConditionalExpressions = zv::Arr::create(0); + for (auto entry : zv::ArrRef(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw())) { + zend_string *conditionalExprString = entry.stringKeyOrNull(); + if (conditionalExprString == NULL || !zend_hash_exists(useVariableNames.table(), conditionalExprString)) continue; + zv::Arr filteredHolders = zv::Arr::create(0); + for (auto holderEntry : zv::ArrRef(entry.value().deref().raw())) { + if (UNEXPECTED(!holderEntry.value().deref().isObject())) { + zend_throw_error(NULL, "Call to a member function getConditionExpressionTypeHolders() on %s", zend_zval_value_name(holderEntry.value().deref().raw())); + return zv::Val(); + } + zv::Val conditionHolders = pt_type_call(holderEntry.value().deref().asObject(), PT_LC("getconditionexpressiontypeholders"), 0, NULL); + if (UNEXPECTED(conditionHolders.isUndef())) return zv::Val(); + bool allUsed = true; + for (auto conditionEntry : zv::ArrRef(conditionHolders.raw())) { + zend_string *holderExprString = conditionEntry.stringKeyOrNull(); + if (holderExprString == NULL || !zend_hash_exists(useVariableNames.table(), holderExprString)) { + allUsed = false; + break; + } + } + if (!allUsed) continue; + filteredHolders.push(holderEntry.value().deref()); + } + if (zend_hash_num_elements(filteredHolders.table()) == 0) continue; + filteredConditionalExpressions.set(conditionalExprString, zv::Val(std::move(filteredHolders))); + } + + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + zv::Val constantTypes = getConstantTypes(); + if (UNEXPECTED(constantTypes.isUndef())) return zv::Val(); + PT_MS_ARG_CREATE(a, CreateArgs::EXPRESSION_TYPES, arrayMerge(Z_ARRVAL_P(constantTypes.raw()), expressionTypes.table())); + zv::Val nativeConstantTypes = getNativeConstantTypes(); + if (UNEXPECTED(nativeConstantTypes.isUndef())) return zv::Val(); + PT_MS_ARG_CREATE(a, CreateArgs::NATIVE_EXPRESSION_TYPES, arrayMerge(Z_ARRVAL_P(nativeConstantTypes.raw()), nativeTypes.table())); + a.setOwned(CreateArgs::CONDITIONAL_EXPRESSIONS, zv::Val(std::move(filteredConditionalExpressions))); + zval closureTypeZv; + if (UNEXPECTED(!pt_closure_type_new(&closureTypeZv))) return zv::Val(); + a.setOwned(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, zv::Val::adopt(closureTypeZv)); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.setBool(CreateArgs::AFTER_EXTRACT_CALL, false); + a.set(CreateArgs::PARENT_SCOPE, zv::Ref(thisZval())); + return scopeFactoryCreate(a); + } + + /* private (twin 2741); false = pending exception */ + [[nodiscard]] bool expressionTypeIsUnchangeable(zv::Ref typeHolder, bool &out) + { + out = false; + zv::Val expr = holderExpr(typeHolder); + if (UNEXPECTED(expr.isUndef())) return false; + bool isFuncCall; + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_FUNC_CALL, isFuncCall))) return false; + if (!isFuncCall) return true; + zend_object *call = expr.ref().asObject(); + zv::Val firstClassCallable = pt_type_call(call, PT_LC("isfirstclasscallable"), 0, NULL); + if (UNEXPECTED(firstClassCallable.isUndef())) return false; + if (zend_is_true(firstClassCallable.raw())) return true; + zv::Ref name = nodeProp(call, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + bool isFullyQualified; + if (UNEXPECTED(!isInstance(name.deref(), PT_CLASS_FULLY_QUALIFIED, isFullyQualified))) return false; + if (!isFullyQualified) return true; + zv::Val lower = pt_type_call(name.deref().asObject(), PT_LC("tolowerstring"), 0, NULL); + if (UNEXPECTED(lower.isUndef())) return false; + static const char *const existenceChecks[] = { + "class_exists", "interface_exists", "trait_exists", "enum_exists", "function_exists", + }; + bool isExistenceCheck = false; + if (lower.ref().isString()) { + for (const char *candidate : existenceChecks) { + if (zend_string_equals_cstr(lower.ref().asString(), candidate, strlen(candidate))) { + isExistenceCheck = true; + break; + } + } + } + if (!isExistenceCheck) return true; + zv::Val args = pt_type_call(call, PT_LC("getargs"), 0, NULL); + if (UNEXPECTED(args.isUndef())) return false; + zval *firstArg = zend_hash_index_find(Z_ARRVAL_P(args.raw()), 0); + if (firstArg == NULL || Z_TYPE_P(firstArg) == IS_NULL) return true; + zv::Ref value = nodeProp(Z_OBJ_P(firstArg), PT_LC("value")); + if (UNEXPECTED(value.raw() == NULL)) return false; + if (UNEXPECTED(!value.deref().isObject())) { + zend_type_error("PHPStan\\Analyser\\MutatingScope::getScopeStateType(): Argument #1 ($expr) must be of type PhpParser\\Node\\Expr, %s given", zend_zval_value_name(value.deref().raw())); + return false; + } + zv::Val argType = getScopeStateType(value.deref().asObject()); + if (UNEXPECTED(argType.isUndef())) return false; + if (UNEXPECTED(!argType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getConstantStrings() on %s", zend_zval_value_name(argType.raw())); + return false; + } + zv::Val constantStrings = pt_type_call(argType.ref().asObject(), PT_LC("getconstantstrings"), 0, NULL); + if (UNEXPECTED(constantStrings.isUndef())) return false; + if (Z_TYPE_P(constantStrings.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(constantStrings.raw())) != 1) return true; + zv::Val type = holderType(typeHolder); + if (UNEXPECTED(type.isUndef())) return false; + if (UNEXPECTED(!type.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function isTrue() on %s", zend_zval_value_name(type.raw())); + return false; + } + zv::Val isTrue = pt_type_call(type.ref().asObject(), PT_LC("istrue"), 0, NULL); + if (UNEXPECTED(isTrue.isUndef())) return false; + out = pt_type_trinary_value(isTrue.raw()) == PT_TRI_YES; + return EXPECTED(EG(exception) == NULL); + } + + /* private (twin 2769) */ + zv::Val invalidateStaticExpressions(HashTable *expressionTypes) + { + StaticExprCtx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.staticCall = pt_class(PT_CLASS_STATIC_CALL); + ctx.staticPropertyFetch = pt_class(PT_CLASS_STATIC_PROPERTY_FETCH); + if (UNEXPECTED(ctx.staticCall == NULL || ctx.staticPropertyFetch == NULL)) return zv::Val(); + zv::Arr filtered = zv::Arr::create(zend_hash_num_elements(expressionTypes)); + for (auto entry : zv::TableRef(expressionTypes)) { + zv::Val expr = holderExpr(entry.value()); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + if (UNEXPECTED(!expr.ref().isObject())) { + zend_throw_error(NULL, "phpstan_turbo: ExpressionTypeHolder::getExpr() must return an object"); + return zv::Val(); + } + ctx.base.failed = false; + zend_object *staticExpression = pt_find_first_recursive(expr.ref().asObject(), staticExprMatcher, &ctx); + if (UNEXPECTED(ctx.base.failed)) return zv::Val(); + if (staticExpression != NULL) continue; + zval copy; + ZVAL_COPY(©, entry.value().deref().raw()); + pt_ht_update(filtered.table(), entry.stringKeyOrNull(), entry.indexKey(), ©); + } + return zv::Val(std::move(filtered)); + } + + /** @api (twin 2791) */ + zv::Val enterArrowFunction(zend_object *arrowFunction, zval *callableParameters, zval *nativeCallableParameters) + { + zv::Val closureTypeResolver = containerGetByType(PT_LC("PHPStan\\Analyser\\ExprHandler\\Helper\\ClosureTypeResolver")); + if (UNEXPECTED(closureTypeResolver.isUndef())) return zv::Val(); + zend_object *resolverObject = requireObject(closureTypeResolver, "getClosureType"); + if (UNEXPECTED(resolverObject == NULL)) return zv::Val(); + zv::Val storage = thisGetCurrentExpressionResultStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + zv::Args resolverArgs{thisZval(), arrowFunction, true, storage.raw()}; + zv::Val anonymousFunctionReflection = pt_type_call(resolverObject, PT_LC("getclosuretype"), 4, resolverArgs); + if (UNEXPECTED(anonymousFunctionReflection.isUndef())) return zv::Val(); + + zv::Val scope = thisEnterArrowFunctionWithoutReflection(arrowFunction, callableParameters, nativeCallableParameters); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "isDeclareStrictTypes"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + return createForFunctionEntry(scopeObject, std::move(anonymousFunctionReflection), true); + } + + /* (twin 2823) */ + zv::Val enterArrowFunctionWithoutReflection(zend_object *arrowFunction, zval *callableParameters, zval *nativeCallableParameters) + { + zv::Val arrowFunctionScope = self_(); + zv::Ref params = nodeProp(arrowFunction, PT_LC("params")); + if (UNEXPECTED(params.raw() == NULL)) return zv::Val(); + for (auto entry : zv::ArrRef(params.deref().raw())) { + zv::Ref parameter = entry.value().deref(); + if (UNEXPECTED(!parameter.isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Val parameterType, nativeParameterType; + if (UNEXPECTED(!parameterTypes(parameter.asObject(), callableParameters, nativeCallableParameters, (zend_long) entry.indexKey(), parameterType, nativeParameterType))) { + return zv::Val(); + } + zend_string *name = parameterVariableName(parameter.asObject()); + if (UNEXPECTED(name == NULL)) return zv::Val(); + zval assignArgs[5]; + ZVAL_STR(&assignArgs[0], name); + ZVAL_COPY_VALUE(&assignArgs[1], parameterType.raw()); + ZVAL_COPY_VALUE(&assignArgs[2], nativeParameterType.raw()); + ZVAL_COPY_VALUE(&assignArgs[3], pt_trinary_singleton(PT_TRI_YES)); + ZVAL_EMPTY_ARRAY(&assignArgs[4]); + zend_object *scopeObject = requireObject(arrowFunctionScope, "assignVariable"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + arrowFunctionScope = pt_type_call(scopeObject, PT_LC("assignvariable"), 5, assignArgs); + if (UNEXPECTED(arrowFunctionScope.isUndef())) return zv::Val(); + } + + zv::Ref isStatic = nodeProp(arrowFunction, PT_LC("static")); + if (UNEXPECTED(isStatic.raw() == NULL)) return zv::Val(); + if (zend_is_true(isStatic.deref().raw())) { + zv::Val thisVariable = newVariable(PT_LC("this")); + if (UNEXPECTED(thisVariable.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(arrowFunctionScope, "invalidateExpression"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + arrowFunctionScope = pt_type_call(scopeObject, PT_LC("invalidateexpression"), 1, thisVariable.raw()); + if (UNEXPECTED(arrowFunctionScope.isUndef())) return zv::Val(); + } + + zend_object *scopeObject = requireObject(arrowFunctionScope, "getFunction"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zval *context = otherProp(scopeObject, PT_MS_PROP_CONTEXT, PT_LC("context")); + zval *scopeExpressionTypes = otherProp(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + zval *scopeNativeExpressionTypes = otherProp(scopeObject, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + zval *scopeConditionalExpressions = otherProp(scopeObject, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + zval *scopeInClosureBindScopeClasses = otherProp(scopeObject, PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, PT_LC("inClosureBindScopeClasses")); + zval *scopeAfterExtractCall = otherProp(scopeObject, PT_MS_PROP_AFTER_EXTRACT_CALL, PT_LC("afterExtractCall")); + zval *scopeParentScope = otherProp(scopeObject, PT_MS_PROP_PARENT_SCOPE, PT_LC("parentScope")); + if (UNEXPECTED(context == NULL || scopeExpressionTypes == NULL || scopeNativeExpressionTypes == NULL + || scopeConditionalExpressions == NULL || scopeInClosureBindScopeClasses == NULL + || scopeAfterExtractCall == NULL || scopeParentScope == NULL)) { + return zv::Val(); + } + + CreateArgs a; + a.set(CreateArgs::CONTEXT, zv::Ref(context)); + bool declareStrictTypes; + if (UNEXPECTED(!thisIsDeclareStrictTypes(declareStrictTypes))) return zv::Val(); + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, declareStrictTypes); + PT_MS_ARG_CREATE(a, CreateArgs::FUNCTION, pt_type_call(scopeObject, PT_LC("getfunction"), 0, NULL)); + PT_MS_ARG_CREATE(a, CreateArgs::NAMESPACE_, pt_type_call(scopeObject, PT_LC("getnamespace"), 0, NULL)); + PT_MS_ARG_CREATE(a, CreateArgs::EXPRESSION_TYPES, invalidateStaticExpressions(Z_ARRVAL_P(scopeExpressionTypes))); + a.set(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Ref(scopeNativeExpressionTypes)); + a.set(CreateArgs::CONDITIONAL_EXPRESSIONS, zv::Ref(scopeConditionalExpressions)); + a.set(CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, zv::Ref(scopeInClosureBindScopeClasses)); + zval closureTypeZv; + if (UNEXPECTED(!pt_closure_type_new(&closureTypeZv))) return zv::Val(); + a.setOwned(CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, zv::Val::adopt(closureTypeZv)); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, true); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + a.set(CreateArgs::AFTER_EXTRACT_CALL, zv::Ref(scopeAfterExtractCall)); + a.set(CreateArgs::PARENT_SCOPE, zv::Ref(scopeParentScope)); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, "templateArgumentConstraints"))) return zv::Val(); + a.set(CreateArgs::NATIVE_TYPES_PROMOTED, slot(PT_MS_PROP_NATIVE_TYPES_PROMOTED)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_FRAME, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME)); + a.set(CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, slot(PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS)); + return scopeFactoryCreate(a); + } + + /* the last value of a list, NULL for an empty one (array_last()) */ + static zval *arrayLast(HashTable *table) + { + zval *last = NULL; + for (auto entry : zv::TableRef(table)) { + last = entry.value().deref().raw(); + } + return last; + } + + /* $parameter->getType() of a ParameterReflection; UNDEF = pending exception */ + static zv::Val parameterReflectionType(zval *parameter) + { + if (UNEXPECTED(Z_TYPE_P(parameter) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getType() on %s", zend_zval_value_name(parameter)); + return zv::Val(); + } + return pt_type_call(Z_OBJ_P(parameter), PT_LC("gettype"), 0, NULL); + } + + /* $parameter->isVariadic(); false with an exception pending on failure */ + [[nodiscard]] static bool parameterReflectionIsVariadic(zval *parameter, bool &out) + { + if (UNEXPECTED(Z_TYPE_P(parameter) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isVariadic() on %s", zend_zval_value_name(parameter)); + return false; + } + return otherCallBool(Z_OBJ_P(parameter), PT_LC("isvariadic"), out); + } + + /* private (twin 2921) */ + zv::Val getCallableParameterType(zend_object *parameter, zval *callableParameters, zend_long index) + { + zv::Ref variadic = nodeProp(parameter, PT_LC("variadic")); + if (UNEXPECTED(variadic.raw() == NULL)) return zv::Val(); + if (zend_is_true(variadic.deref().raw())) return buildVariadicArrayTypeFromCallableParameters(callableParameters, index); + HashTable *parameters = Z_ARRVAL_P(callableParameters); + zval *atIndex = zend_hash_index_find(parameters, (zend_ulong) index); + if (atIndex != NULL && Z_TYPE_P(atIndex) != IS_NULL) return parameterReflectionType(atIndex); + if (zend_hash_num_elements(parameters) != 0) { + zval *lastParameter = arrayLast(parameters); + bool isVariadic; + if (UNEXPECTED(!parameterReflectionIsVariadic(lastParameter, isVariadic))) return zv::Val(); + if (isVariadic) return parameterReflectionType(lastParameter); + } + zval mixedZv; + if (UNEXPECTED(!pt_mixed_type_new(&mixedZv))) return zv::Val(); + return zv::Val::adopt(mixedZv); + } + + /* private (twin 2946) */ + zv::Val buildVariadicArrayTypeFromCallableParameters(zval *callableParameters, zend_long startIndex) + { + HashTable *parameters = Z_ARRVAL_P(callableParameters); + uint32_t count = zend_hash_num_elements(parameters); + zv::Arr elementTypes = zv::Arr::create(0); + for (zend_long j = startIndex; j < (zend_long) count; j++) { + zval *parameter = zend_hash_index_find(parameters, (zend_ulong) j); + if (parameter == NULL) { + zend_error(E_WARNING, "Undefined array key " ZEND_LONG_FMT, j); + if (UNEXPECTED(EG(exception))) return zv::Val(); + } + zval nullZv; + ZVAL_NULL(&nullZv); + zval *entry = parameter == NULL ? &nullZv : parameter; + zv::Val type = parameterReflectionType(entry); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + elementTypes.push(std::move(type)); + bool isVariadic; + if (UNEXPECTED(!parameterReflectionIsVariadic(entry, isVariadic))) return zv::Val(); + if (isVariadic) break; + } + + if (zend_hash_num_elements(elementTypes.table()) == 0 && count > 0) { + zval *lastParameter = arrayLast(parameters); + bool isVariadic; + if (UNEXPECTED(!parameterReflectionIsVariadic(lastParameter, isVariadic))) return zv::Val(); + if (isVariadic) { + zv::Val type = parameterReflectionType(lastParameter); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + elementTypes.push(std::move(type)); + } + } + + if (zend_hash_num_elements(elementTypes.table()) == 0) { + zval mixedZv; + if (UNEXPECTED(!pt_mixed_type_new(&mixedZv))) return zv::Val(); + return zv::Val::adopt(mixedZv); + } + + uint32_t elementCount = zend_hash_num_elements(elementTypes.table()); + ALLOCA_FLAG(useHeap) + zval *unionArgv = (zval *) do_alloca(sizeof(zval) * elementCount, useHeap); + uint32_t i = 0; + for (auto entry : elementTypes.arrRef()) { + ZVAL_COPY_VALUE(&unionArgv[i++], entry.value().deref().raw()); + } + zv::Val elementType = pt_type_combinator_union(elementCount, unionArgv); + free_alloca(unionArgv, useHeap); + if (UNEXPECTED(elementType.isUndef())) return zv::Val(); + bool supportsNamedArguments; + if (UNEXPECTED(!phpVersionSupportsNamedArguments(supportsNamedArguments))) return zv::Val(); + return variadicArrayType(elementType.raw(), supportsNamedArguments); + } + + /** @api (twin 2977, static) */ + static zv::Val intersectButNotNever(zval *nativeType, zval *inferredType) + { + if (UNEXPECTED(Z_TYPE_P(nativeType) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isSuperTypeOf() on %s", zend_zval_value_name(nativeType)); + return zv::Val(); + } + zv::Val isSuperType = pt_type_op(Z_OBJ_P(nativeType), PT_OP_IS_SUPER_TYPE_OF, 1, inferredType); + if (UNEXPECTED(isSuperType.isUndef())) return zv::Val(); + if (UNEXPECTED(!isSuperType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function no() on %s", zend_zval_value_name(isSuperType.raw())); + return zv::Val(); + } + zv::Val no = pt_type_call(isSuperType.ref().asObject(), PT_LC("no"), 0, NULL); + if (UNEXPECTED(no.isUndef())) return zv::Val(); + if (zend_is_true(no.raw())) return zv::Val::copyOf(zv::Ref(nativeType)); + + zv::Args args{nativeType, inferredType}; + zv::Val result = pt_type_combinator_intersect(2, args); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + // the inferred type says no value is ever produced - the native + // type's nullability must not resurrect one + if (zv::Ref(result.raw()).instanceOf(pt_ce_never_type)) return result; + bool containsNull; + if (UNEXPECTED(!pt_type_combinator_contains_null(nativeType, containsNull))) return zv::Val(); + if (containsNull) return pt_type_combinator_add_null(result.raw()); + return result; + } + + /* (twin 2991) */ + zv::Val enterMatch(zend_object *expr, zval *condType, zval *condNativeType) + { + zv::Ref cond = nodeProp(expr, PT_LC("cond")); + if (UNEXPECTED(cond.raw() == NULL)) return zv::Val(); + bool isVariable; + if (UNEXPECTED(!isInstance(cond.deref(), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (isVariable) return self_(); + zv::Val inner; + bool isAlwaysRemembered; + if (UNEXPECTED(!isInstance(cond.deref(), PT_CLASS_ALWAYS_REMEMBERED_EXPR, isAlwaysRemembered))) return zv::Val(); + if (isAlwaysRemembered) { + zv::Ref wrapped = nodeProp(cond.deref().asObject(), PT_LC("expr")); + if (UNEXPECTED(wrapped.raw() == NULL)) return zv::Val(); + inner = zv::Val::copyOf(wrapped.deref()); + } else { + inner = zv::Val::copyOf(cond.deref()); + } + bool isScalar; + if (UNEXPECTED(!isInstance(inner.ref(), PT_CLASS_SCALAR, isScalar))) return zv::Val(); + if (isScalar) return self_(); + + zv::Args condArgs{inner.raw(), condType, condNativeType}; + zv::Val condExpr = pt_type_new(PT_CLASS_ALWAYS_REMEMBERED_EXPR, 3, condArgs); + if (UNEXPECTED(condExpr.isUndef())) return zv::Val(); + zend_update_property(expr->ce, expr, PT_LC("cond"), condExpr.raw()); + if (UNEXPECTED(EG(exception))) return zv::Val(); + return thisAssignExpression(condExpr.raw(), condType, condNativeType); + } + + /* $originalScope->getIterableKeyType($type) / ->getIterableValueType($type) */ + static zv::Val iterableType(zend_object *originalScope, zval *iteratee, bool key) + { + if (key) return pt_type_call(originalScope, PT_LC("getiterablekeytype"), 1, iteratee); + return pt_type_call(originalScope, PT_LC("getiterablevaluetype"), 1, iteratee); + } + + /* $scope->assignExpression($expr, $type, $nativeType) on any scope */ + static zv::Val otherAssignExpression(zv::Val &scope, zval *expr, zval *type, zval *nativeType) + { + zend_object *object = requireObject(scope, "assignExpression"); + if (UNEXPECTED(object == NULL)) return zv::Val(); + zv::Args args{expr, type, nativeType}; + return pt_type_call(object, PT_LC("assignexpression"), 3, args); + } + + /* $scope->overwriteExpression($expr, $type, $nativeType) on any scope */ + static zv::Val otherOverwriteExpression(zv::Val &scope, zval *expr, zval *type, zval *nativeType) + { + zend_object *object = requireObject(scope, "overwriteExpression"); + if (UNEXPECTED(object == NULL)) return zv::Val(); + zv::Args args{expr, type, nativeType}; + return otherPrivate(object, PT_LC("overwriteexpression"), 3, args, [&](MutatingScope &other) { + return other.overwriteExpression(expr, type, nativeType); + }); + } + + /* (twin 3013) */ + zv::Val enterForeach(zend_object *originalScope, zval *iteratee, zval *iterateeType, zval *nativeIterateeType, zend_string *valueName, zend_string *keyName, bool valueByRef) + { + zv::Val valueType = iterableType(originalScope, iterateeType, false); + if (UNEXPECTED(valueType.isUndef())) return zv::Val(); + zv::Val nativeValueType = iterableType(originalScope, nativeIterateeType, false); + if (UNEXPECTED(nativeValueType.isUndef())) return zv::Val(); + zval assignArgs[5]; + ZVAL_STR(&assignArgs[0], valueName); + ZVAL_COPY_VALUE(&assignArgs[1], valueType.raw()); + ZVAL_COPY_VALUE(&assignArgs[2], nativeValueType.raw()); + ZVAL_COPY_VALUE(&assignArgs[3], pt_trinary_singleton(PT_TRI_YES)); + ZVAL_EMPTY_ARRAY(&assignArgs[4]); + zv::Val scope = thisAssignVariable(assignArgs); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + + /* Track the original foreach value so narrowings applied to the + * value variable can later be projected back onto the corresponding + * array dim fetch without being confused by a reassignment */ + zval valueNameZv; + ZVAL_STR(&valueNameZv, valueName); + zv::Val originalValueExpr = pt_type_new(PT_CLASS_ORIGINAL_FOREACH_VALUE_EXPR, 1, &valueNameZv); + if (UNEXPECTED(originalValueExpr.isUndef())) return zv::Val(); + scope = otherAssignExpression(scope, originalValueExpr.raw(), valueType.raw(), nativeValueType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + + bool writeThrough; + if (UNEXPECTED(!iterateeIsNonConstantArray(iterateeType, writeThrough))) return zv::Val(); + writeThrough = writeThrough && valueByRef; + if (writeThrough) { + /* the write-through rebuilds the iteratee AT FOREACH ENTRY with + * the value variable's latest type - captured here, not read live */ + zv::Val keyTypeExpr = nativeTypeExprOf(iterableType(originalScope, iterateeType, true), iterableType(originalScope, nativeIterateeType, true)); + if (UNEXPECTED(keyTypeExpr.isUndef())) return zv::Val(); + zv::Args iterateeTypeArgs{iterateeType, nativeIterateeType}; + zv::Val iterateeTypeExpr = pt_type_new(PT_CLASS_NATIVE_TYPE_EXPR, 2, iterateeTypeArgs); + if (UNEXPECTED(iterateeTypeExpr.isUndef())) return zv::Val(); + zv::Val valueVariable = newVariable(ZSTR_VAL(valueName), ZSTR_LEN(valueName)); + if (UNEXPECTED(valueVariable.isUndef())) return zv::Val(); + zv::Args setArgs{iterateeTypeExpr.raw(), keyTypeExpr.raw(), valueVariable.raw()}; + zv::Val setExpr = pt_type_new(PT_CLASS_SET_EXISTING_OFFSET_VALUE_TYPE_EXPR, 3, setArgs); + if (UNEXPECTED(setExpr.isUndef())) return zv::Val(); + zv::Args intertwinedArgs{valueName, iteratee, setExpr.raw()}; + zv::Val intertwined = pt_type_new(PT_CLASS_INTERTWINED_VAR, 3, intertwinedArgs); + if (UNEXPECTED(intertwined.isUndef())) return zv::Val(); + scope = otherAssignExpression(scope, intertwined.raw(), valueType.raw(), nativeValueType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } + + if (keyName != NULL) { + zend_object *scopeObject = requireObject(scope, "enterForeachKey"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Args keyArgs{originalScope, iteratee, iterateeType, nativeIterateeType, keyName}; + scope = pt_type_call(scopeObject, PT_LC("enterforeachkey"), 5, keyArgs); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + + if (writeThrough) { + zv::Val keyVariable = newVariable(ZSTR_VAL(keyName), ZSTR_LEN(keyName)); + if (UNEXPECTED(keyVariable.isUndef())) return zv::Val(); + zv::Args dimArgs{iteratee, keyVariable.raw()}; + zv::Val dimFetch = pt_type_new(PT_CLASS_ARRAY_DIM_FETCH, 2, dimArgs); + if (UNEXPECTED(dimFetch.isUndef())) return zv::Val(); + zv::Val valueVariable = newVariable(ZSTR_VAL(valueName), ZSTR_LEN(valueName)); + if (UNEXPECTED(valueVariable.isUndef())) return zv::Val(); + zv::Args intertwinedArgs{valueName, dimFetch.raw(), valueVariable.raw()}; + zv::Val intertwined = pt_type_new(PT_CLASS_INTERTWINED_VAR, 3, intertwinedArgs); + if (UNEXPECTED(intertwined.isUndef())) return zv::Val(); + scope = otherAssignExpression(scope, intertwined.raw(), valueType.raw(), nativeValueType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } + } + + return scope; + } + + /* new NativeTypeExpr($phpdocType, $nativeType) over two producers that + * may have thrown */ + static zv::Val nativeTypeExprOf(zv::Val phpdocType, zv::Val nativeType) + { + if (UNEXPECTED(phpdocType.isUndef() || nativeType.isUndef())) return zv::Val(); + zv::Args args{phpdocType.raw(), nativeType.raw()}; + return pt_type_new(PT_CLASS_NATIVE_TYPE_EXPR, 2, args); + } + + /* $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no() */ + static bool iterateeIsNonConstantArray(zval *iterateeType, bool &out) + { + out = false; + if (UNEXPECTED(Z_TYPE_P(iterateeType) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isArray() on %s", zend_zval_value_name(iterateeType)); + return false; + } + zend_long isArray = pt_type_op_trinary(Z_OBJ_P(iterateeType), PT_OP_IS_ARRAY, 0, NULL); + if (UNEXPECTED(isArray < 0)) return false; + if (isArray != PT_TRI_YES) return true; + zend_long isConstantArray = pt_type_op_trinary(Z_OBJ_P(iterateeType), PT_OP_IS_CONSTANT_ARRAY, 0, NULL); + if (UNEXPECTED(isConstantArray < 0)) return false; + out = isConstantArray == PT_TRI_NO; + return true; + } + + /* (twin 3061) */ + zv::Val enterForeachKey(zend_object *originalScope, zval *iteratee, zval *iterateeType, zval *nativeIterateeType, zend_string *keyName) + { + zv::Val keyType = iterableType(originalScope, iterateeType, true); + if (UNEXPECTED(keyType.isUndef())) return zv::Val(); + zv::Val nativeKeyType = iterableType(originalScope, nativeIterateeType, true); + if (UNEXPECTED(nativeKeyType.isUndef())) return zv::Val(); + zval assignArgs[5]; + ZVAL_STR(&assignArgs[0], keyName); + ZVAL_COPY_VALUE(&assignArgs[1], keyType.raw()); + ZVAL_COPY_VALUE(&assignArgs[2], nativeKeyType.raw()); + ZVAL_COPY_VALUE(&assignArgs[3], pt_trinary_singleton(PT_TRI_YES)); + ZVAL_EMPTY_ARRAY(&assignArgs[4]); + zv::Val scope = thisAssignVariable(assignArgs); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + + zval keyNameZv; + ZVAL_STR(&keyNameZv, keyName); + zv::Val originalKeyExpr = pt_type_new(PT_CLASS_ORIGINAL_FOREACH_KEY_EXPR, 1, &keyNameZv); + if (UNEXPECTED(originalKeyExpr.isUndef())) return zv::Val(); + scope = otherAssignExpression(scope, originalKeyExpr.raw(), keyType.raw(), nativeKeyType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + + if (UNEXPECTED(Z_TYPE_P(iterateeType) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isArray() on %s", zend_zval_value_name(iterateeType)); + return zv::Val(); + } + zend_long isArray = pt_type_op_trinary(Z_OBJ_P(iterateeType), PT_OP_IS_ARRAY, 0, NULL); + if (UNEXPECTED(isArray < 0)) return zv::Val(); + if (isArray == PT_TRI_YES) { + zv::Val keyVariable = newVariable(ZSTR_VAL(keyName), ZSTR_LEN(keyName)); + if (UNEXPECTED(keyVariable.isUndef())) return zv::Val(); + zv::Args dimArgs{iteratee, keyVariable.raw()}; + zv::Val dimFetch = pt_type_new(PT_CLASS_ARRAY_DIM_FETCH, 2, dimArgs); + if (UNEXPECTED(dimFetch.isUndef())) return zv::Val(); + zv::Val valueType = iterableType(originalScope, iterateeType, false); + if (UNEXPECTED(valueType.isUndef())) return zv::Val(); + zv::Val nativeValueType = iterableType(originalScope, nativeIterateeType, false); + if (UNEXPECTED(nativeValueType.isUndef())) return zv::Val(); + scope = otherAssignExpression(scope, dimFetch.raw(), valueType.raw(), nativeValueType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } + + return scope; + } + + /* (twin 3086) */ + zv::Val enterCatchType(zval *catchType, zend_string *variableName) + { + if (variableName == NULL) return self_(); + zval throwableZv; + zv::Str throwableName = zv::Str::adopt(zend_string_init(PT_LC("Throwable"), 0)); + if (UNEXPECTED(!pt_object_type_new(&throwableZv, throwableName.get()))) return zv::Val(); + zv::Val throwableType = zv::Val::adopt(throwableZv); + zv::Args args{catchType, throwableType.raw()}; + zv::Val intersected = pt_type_combinator_intersect(2, args); + if (UNEXPECTED(intersected.isUndef())) return zv::Val(); + /* the twin builds the intersection twice, once per table */ + zval nativeThrowableZv; + if (UNEXPECTED(!pt_object_type_new(&nativeThrowableZv, throwableName.get()))) return zv::Val(); + zv::Val nativeThrowableType = zv::Val::adopt(nativeThrowableZv); + zv::Args nativeArgs{catchType, nativeThrowableType.raw()}; + zv::Val nativeIntersected = pt_type_combinator_intersect(2, nativeArgs); + if (UNEXPECTED(nativeIntersected.isUndef())) return zv::Val(); + zval assignArgs[5]; + ZVAL_STR(&assignArgs[0], variableName); + ZVAL_COPY_VALUE(&assignArgs[1], intersected.raw()); + ZVAL_COPY_VALUE(&assignArgs[2], nativeIntersected.raw()); + ZVAL_COPY_VALUE(&assignArgs[3], pt_trinary_singleton(PT_TRI_YES)); + ZVAL_EMPTY_ARRAY(&assignArgs[4]); + return thisAssignVariable(assignArgs); + } + + /* create(...) with the twin's expression-assign argument list: the two + * currently-* tables as given, an empty call stack, everything else + * from $this — and $this->resolvedTypes carried onto the result */ + zv::Val createWithCurrentlyTables(zv::Val currentlyAssignedExpressions, zv::Val currentlyAllowedUndefinedExpressions) + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, true))) return zv::Val(); + a.setOwned(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS, std::move(currentlyAssignedExpressions)); + a.setOwned(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, std::move(currentlyAllowedUndefinedExpressions)); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + zv::Val scope = scopeFactoryCreate(a); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + if (UNEXPECTED(!assignResolvedTypes(scope))) return zv::Val(); + return scope; + } + + /* (twin 3100) */ + zv::Val enterExpressionAssign(zend_object *expr, bool isPlainWrite) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions"))) { + return zv::Val(); + } + zv::Arr currentlyAssignedExpressions = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw())); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + currentlyAssignedExpressions.set(key.get(), zv::Val::boolean(isPlainWrite)); + return createWithCurrentlyTables(zv::Val(std::move(currentlyAssignedExpressions)), copyOfSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS)); + } + + /* (twin 3131) */ + zv::Val exitExpressionAssign(zend_object *expr) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions"))) { + return zv::Val(); + } + zv::Arr currentlyAssignedExpressions = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw())); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + currentlyAssignedExpressions.separate(); + zend_symtable_del(currentlyAssignedExpressions.table(), key.get()); + return createWithCurrentlyTables(zv::Val(std::move(currentlyAssignedExpressions)), copyOfSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS)); + } + + /** @api (twin 3163) */ + bool isInExpressionAssign(zend_object *expr, bool &out) + { + out = false; + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions"))) return false; + HashTable *assigned = Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw()); + if (zend_hash_num_elements(assigned) == 0) return true; + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return false; + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + out = zend_symtable_exists(assigned, key.get()); + return true; + } + + /* (twin 3178) */ + bool isInWriteExpressionAssign(zend_object *expr, bool &out) + { + out = false; + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions"))) return false; + HashTable *assigned = Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw()); + if (zend_hash_num_elements(assigned) == 0) return true; + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return false; + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + zval *value = zend_symtable_find(assigned, key.get()); + out = value != NULL && Z_TYPE_P(zv::Ref(value).deref().raw()) == IS_TRUE; + return true; + } + + /* (twin 3188) */ + zv::Val setAllowedUndefinedExpression(zend_object *expr) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + bool isStaticPropertyFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_STATIC_PROPERTY_FETCH, isStaticPropertyFetch))) return zv::Val(); + if (isStaticPropertyFetch) return self_(); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions"))) { + return zv::Val(); + } + zv::Arr allowed = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw())); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + allowed.set(key.get(), zv::Val::boolean(true)); + return createWithCurrentlyTables(copyOfSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS), zv::Val(std::move(allowed))); + } + + /* (twin 3223) */ + zv::Val unsetAllowedUndefinedExpression(zend_object *expr) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions"))) { + return zv::Val(); + } + zv::Arr allowed = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw())); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + allowed.separate(); + zend_symtable_del(allowed.table(), key.get()); + return createWithCurrentlyTables(copyOfSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS), zv::Val(std::move(allowed))); + } + + /** @api (twin 3255) */ + bool isUndefinedExpressionAllowed(zend_object *expr, bool &out) + { + out = false; + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions"))) return false; + HashTable *allowed = Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw()); + if (zend_hash_num_elements(allowed) == 0) return true; + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return false; + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + out = zend_symtable_exists(allowed, key.get()); + return true; + } + + /* $scope->[$key] = $holder, or unset(...) for a NULL holder — on + * any scope object (the twin writes into the scope the factory + * answered); false = pending exception */ + [[nodiscard]] static bool writeScopeTable(zend_object *object, uint32_t nativeSlot, const char *name, size_t len, zend_string *key, zval *holder) + { + zval *table = otherProp(object, nativeSlot, name, len); + if (UNEXPECTED(table == NULL)) return false; + ZVAL_DEREF(table); + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) { + zend_throw_error(NULL, "Cannot use a scalar value as an array"); + return false; + } + SEPARATE_ARRAY(table); + if (holder == NULL) { + zend_symtable_del(Z_ARRVAL_P(table), key); + return true; + } + Z_TRY_ADDREF_P(holder); + zend_symtable_update(Z_ARRVAL_P(table), key, holder); + return true; + } + + /* in_array($needle, $haystack, true) over a list of strings */ + static bool listContainsString(zval *list, zend_string *needle) + { + for (auto entry : zv::ArrRef(list)) { + zv::Ref value = entry.value().deref(); + if (value.isString() && zend_string_equals(value.asString(), needle)) return true; + } + return false; + } + + /* private (twin 3378) */ + zv::Val resolveIntertwinedAssignedType(zend_object *scope, zval *rootType, zend_object *assignedExpr, zend_string *rootVariableName, bool native) + { + zval assignedZv; + ZVAL_OBJ(&assignedZv, assignedExpr); + bool isVariable; + if (UNEXPECTED(!isInstance(zv::Ref(&assignedZv), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (isVariable) { + zv::Ref name = nodeProp(assignedExpr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return zv::Val(); + if (name.deref().isString() && zend_string_equals(name.deref().asString(), rootVariableName)) return zv::Val::copyOf(zv::Ref(rootType)); + } + + bool isArrayDimFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&assignedZv), PT_CLASS_ARRAY_DIM_FETCH, isArrayDimFetch))) return zv::Val(); + if (isArrayDimFetch) { + zv::Ref dim = nodeProp(assignedExpr, PT_LC("dim")); + zv::Ref var = nodeProp(assignedExpr, PT_LC("var")); + if (UNEXPECTED(dim.raw() == NULL || var.raw() == NULL)) return zv::Val(); + if (!dim.deref().isNull()) { + if (UNEXPECTED(!var.deref().isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Val varType = resolveIntertwinedAssignedType(scope, rootType, var.deref().asObject(), rootVariableName, native); + if (UNEXPECTED(varType.isUndef())) return zv::Val(); + zv::Val dimType = otherGetType(scope, dim.deref().raw(), native); + if (UNEXPECTED(dimType.isUndef())) return zv::Val(); + if (UNEXPECTED(!varType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getOffsetValueType() on %s", zend_zval_value_name(varType.raw())); + return zv::Val(); + } + return pt_type_op(varType.ref().asObject(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, dimType.raw()); + } + } + + bool isSetExisting; + if (UNEXPECTED(!isInstance(zv::Ref(&assignedZv), PT_CLASS_SET_EXISTING_OFFSET_VALUE_TYPE_EXPR, isSetExisting))) return zv::Val(); + if (isSetExisting) { + /* the foreach-byref slot: the iteratee with its key offset set to + * the value variable's new type */ + zv::Val var = pt_type_call(assignedExpr, PT_LC("getvar"), 0, NULL); + if (UNEXPECTED(var.isUndef())) return zv::Val(); + zv::Val iterateeType = otherGetType(scope, var.raw(), native); + if (UNEXPECTED(iterateeType.isUndef())) return zv::Val(); + zv::Val dim = pt_type_call(assignedExpr, PT_LC("getdim"), 0, NULL); + if (UNEXPECTED(dim.isUndef())) return zv::Val(); + zv::Val dimType = otherGetType(scope, dim.raw(), native); + if (UNEXPECTED(dimType.isUndef())) return zv::Val(); + if (UNEXPECTED(!iterateeType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function setExistingOffsetValueType() on %s", zend_zval_value_name(iterateeType.raw())); + return zv::Val(); + } + zv::Args args{dimType.raw(), rootType}; + return pt_type_call(iterateeType.ref().asObject(), PT_LC("setexistingoffsetvaluetype"), 2, args); + } + + pt_throw_should_not_happen(); + return zv::Val(); + } + + /* private (twin 3403); false = pending exception */ + [[nodiscard]] bool isDimFetchPathReachable(zend_object *scope, zend_object *dimFetch, bool &out) + { + out = false; + zv::Ref dim = nodeProp(dimFetch, PT_LC("dim")); + zv::Ref var = nodeProp(dimFetch, PT_LC("var")); + if (UNEXPECTED(dim.raw() == NULL || var.raw() == NULL)) return false; + if (dim.deref().isNull()) return true; + bool varIsDimFetch; + if (UNEXPECTED(!isInstance(var.deref(), PT_CLASS_ARRAY_DIM_FETCH, varIsDimFetch))) return false; + if (!varIsDimFetch) { + out = true; + return true; + } + zv::Val varType = otherGetType(scope, var.deref().raw(), false); + if (UNEXPECTED(varType.isUndef())) return false; + zv::Val dimType = otherGetType(scope, dim.deref().raw(), false); + if (UNEXPECTED(dimType.isUndef())) return false; + if (UNEXPECTED(!varType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function hasOffsetValueType() on %s", zend_zval_value_name(varType.raw())); + return false; + } + zend_long has = pt_type_op_trinary(varType.ref().asObject(), PT_OP_HAS_OFFSET_VALUE_TYPE, 1, dimType.raw()); + if (UNEXPECTED(has < 0)) return false; + if (has != PT_TRI_YES) return true; + return isDimFetchPathReachable(scope, var.deref().asObject(), out); + } + + /* (twin 3267) */ + zv::Val assignVariable(zend_string *variableName, zval *type, zval *nativeType, zval *certainty, zval *intertwinedPropagatedFrom) + { + zv::Val node = newVariable(ZSTR_VAL(variableName), ZSTR_LEN(variableName)); + if (UNEXPECTED(node.isUndef())) return zv::Val(); + zv::Val scope = thisAssignExpression(node.raw(), type, nativeType); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_long certaintyValue = pt_type_trinary_value(certainty); + if (UNEXPECTED(certaintyValue < 0)) return zv::Val(); + if (certaintyValue == PT_TRI_NO) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_object *scopeObject = requireObject(scope, "hasExpressionType"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Str variableKey = zv::Str::adopt(dollarName(variableName)); + if (certaintyValue != PT_TRI_YES) { + zval holder; + pt_holder_create(&holder, node.raw(), type, certaintyValue); + zv::Val ownedHolder = zv::Val::adopt(holder); + if (UNEXPECTED(!writeScopeTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), variableKey.get(), ownedHolder.raw()))) { + return zv::Val(); + } + zval nativeHolder; + pt_holder_create(&nativeHolder, node.raw(), nativeType, certaintyValue); + zv::Val ownedNativeHolder = zv::Val::adopt(nativeHolder); + if (UNEXPECTED(!writeScopeTable(scopeObject, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes"), variableKey.get(), ownedNativeHolder.raw()))) { + return zv::Val(); + } + } + + /* foreach ($scope->expressionTypes as ...) — over the table as it + * stands here; PHP's by-value iteration sees neither the unsets + * below nor the tables of the scopes the loop moves on to */ + zval *initialTable = otherProp(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(initialTable == NULL)) return zv::Val(); + zv::Val snapshot = zv::Val::copyOf(zv::Ref(initialTable).deref()); + for (auto entry : zv::ArrRef(snapshot.raw())) { + zend_string *exprString = entry.stringKeyOrNull(); + zv::Val holderExprVal = holderExpr(entry.value()); + if (UNEXPECTED(holderExprVal.isUndef())) return zv::Val(); + bool isIntertwined; + if (UNEXPECTED(!isInstance(holderExprVal.ref(), PT_CLASS_INTERTWINED_VAR, isIntertwined))) return zv::Val(); + if (!isIntertwined) continue; + zend_long holderCertaintyValue = holderCertainty(entry.value()); + if (UNEXPECTED(holderCertaintyValue < 0)) return zv::Val(); + if (holderCertaintyValue != PT_TRI_YES) continue; + zend_object *intertwined = holderExprVal.ref().asObject(); + zv::Val intertwinedVariableName = pt_type_call(intertwined, PT_LC("getvariablename"), 0, NULL); + if (UNEXPECTED(intertwinedVariableName.isUndef())) return zv::Val(); + if (!intertwinedVariableName.ref().isString() || !zend_string_equals(intertwinedVariableName.ref().asString(), variableName)) continue; + + zv::Val assignedExpr = pt_type_call(intertwined, PT_LC("getassignedexpr"), 0, NULL); + if (UNEXPECTED(assignedExpr.isUndef())) return zv::Val(); + if (UNEXPECTED(!assignedExpr.ref().isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_object *assignedExprObject = assignedExpr.ref().asObject(); + scopeObject = requireObject(scope, "hasExpressionType"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + bool assignedIsDimFetch; + if (UNEXPECTED(!isInstance(assignedExpr.ref(), PT_CLASS_ARRAY_DIM_FETCH, assignedIsDimFetch))) return zv::Val(); + if (assignedIsDimFetch) { + bool reachable; + if (UNEXPECTED(!isDimFetchPathReachable(scopeObject, assignedExprObject, reachable))) return zv::Val(); + if (!reachable) { + if (exprString == NULL) continue; + if (UNEXPECTED(!writeScopeTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), exprString, NULL) + || !writeScopeTable(scopeObject, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes"), exprString, NULL))) { + return zv::Val(); + } + continue; + } + } + + /* When the byref's dim is non-constant AND not enumerable as a + * finite set of scalars, the just-performed write to the array + * might or might not have hit the byref's slot */ + bool unionWithOld = false; + if (assignedIsDimFetch) { + zv::Ref dim = nodeProp(assignedExprObject, PT_LC("dim")); + if (UNEXPECTED(dim.raw() == NULL)) return zv::Val(); + if (!dim.deref().isNull()) { + zv::Val dimType = otherGetType(scopeObject, dim.deref().raw(), false); + if (UNEXPECTED(dimType.isUndef())) return zv::Val(); + if (UNEXPECTED(!dimType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getConstantScalarValues() on %s", zend_zval_value_name(dimType.raw())); + return zv::Val(); + } + zv::Val constantScalarValues = pt_type_op(dimType.ref().asObject(), PT_OP_GET_CONSTANT_SCALAR_VALUES, 0, NULL); + if (UNEXPECTED(constantScalarValues.isUndef())) return zv::Val(); + zv::Val finiteTypes = pt_type_call(dimType.ref().asObject(), PT_LC("getfinitetypes"), 0, NULL); + if (UNEXPECTED(finiteTypes.isUndef())) return zv::Val(); + unionWithOld = zend_hash_num_elements(Z_ARRVAL_P(constantScalarValues.raw())) != 1 + && zend_hash_num_elements(Z_ARRVAL_P(finiteTypes.raw())) == 0; + } + } + + /* Resolve the byref slot's new value directly from the + * just-assigned root variable's type */ + zv::Val assignedType = resolveIntertwinedAssignedType(scopeObject, type, assignedExprObject, variableName, false); + if (UNEXPECTED(assignedType.isUndef())) return zv::Val(); + zv::Val assignedNativeType = resolveIntertwinedAssignedType(scopeObject, nativeType, assignedExprObject, variableName, true); + if (UNEXPECTED(assignedNativeType.isUndef())) return zv::Val(); + + zv::Val target = pt_type_call(intertwined, PT_LC("getexpr"), 0, NULL); + if (UNEXPECTED(target.isUndef())) return zv::Val(); + if (UNEXPECTED(!target.ref().isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Val has = pt_type_call(scopeObject, PT_LC("hasexpressiontype"), 1, target.raw()); + if (UNEXPECTED(has.isUndef())) return zv::Val(); + zend_long hasValue = pt_type_trinary_value(has.raw()); + if (UNEXPECTED(hasValue < 0)) return zv::Val(); + bool targetIsVariable; + if (UNEXPECTED(!isInstance(target.ref(), PT_CLASS_VARIABLE, targetIsVariable))) return zv::Val(); + zend_string *targetVarName = NULL; + if (targetIsVariable) { + zv::Ref targetName = nodeProp(target.ref().asObject(), PT_LC("name")); + if (UNEXPECTED(targetName.raw() == NULL)) return zv::Val(); + if (targetName.deref().isString()) { + targetVarName = targetName.deref().asString(); + } + } + + if (targetVarName != NULL && hasValue != PT_TRI_NO) { + if (listContainsString(intertwinedPropagatedFrom, targetVarName)) continue; + if (unionWithOld) { + zv::Val targetVarNode = newVariable(ZSTR_VAL(targetVarName), ZSTR_LEN(targetVarName)); + zv::Val rootVarNode = newVariable(ZSTR_VAL(variableName), ZSTR_LEN(variableName)); + if (UNEXPECTED(targetVarNode.isUndef() || rootVarNode.isUndef())) return zv::Val(); + if (UNEXPECTED(!unionAssignedWithOld(assignedType, rootVarNode.raw(), targetVarNode.raw(), assignedExprObject, variableName, scopeObject, false))) { + return zv::Val(); + } + if (UNEXPECTED(!unionAssignedWithOld(assignedNativeType, rootVarNode.raw(), targetVarNode.raw(), assignedExprObject, variableName, scopeObject, true))) { + return zv::Val(); + } + } + zv::Arr propagated = zv::Arr::copyOfTable(Z_ARRVAL_P(intertwinedPropagatedFrom)); + propagated.push(zv::Val::string(variableName)); + zv::Args assignArgs{targetVarName, assignedType.raw(), assignedNativeType.raw(), has.raw(), propagated.raw()}; + scope = pt_type_call(scopeObject, PT_LC("assignvariable"), 5, assignArgs); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } else { + zv::Val targetRootVar = pt_scope_ops_intertwined_ref_root_variable_name(target.ref().asObject()); + if (UNEXPECTED(targetRootVar.isUndef())) return zv::Val(); + if (targetRootVar.ref().isString() && listContainsString(intertwinedPropagatedFrom, targetRootVar.ref().asString())) continue; + scope = otherOverwriteExpression(scope, target.raw(), assignedType.raw(), assignedNativeType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } + } + + return scope; + } + + /* TypeCombinator::union($assigned, $this->resolveIntertwinedAssignedType($this, , ...), $scope->($targetVarNode)); + * in place on $assigned; false = pending exception */ + [[nodiscard]] bool unionAssignedWithOld(zv::Val &assigned, zval *rootVarNode, zval *targetVarNode, zend_object *assignedExpr, zend_string *variableName, zend_object *scopeObject, bool native) + { + zv::Val rootType = native ? thisGetNativeType(rootVarNode) : thisGetType(rootVarNode); + if (UNEXPECTED(rootType.isUndef())) return false; + zv::Val fromRoot = resolveIntertwinedAssignedType(self, rootType.raw(), assignedExpr, variableName, native); + if (UNEXPECTED(fromRoot.isUndef())) return false; + zv::Val targetType = otherGetType(scopeObject, targetVarNode, native); + if (UNEXPECTED(targetType.isUndef())) return false; + zv::Args args{assigned.raw(), fromRoot.raw(), targetType.raw()}; + zv::Val united = pt_type_combinator_union(3, args); + if (UNEXPECTED(united.isUndef())) return false; + assigned = std::move(united); + return true; + } + + /* private (twin 3423); its only caller is applySpecifiedTypes() */ + zv::Val unsetExpression(zend_object *expr) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val scope = unsetOffsetOfDimFetch(expr); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "invalidateExpression"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + return pt_type_call(scopeObject, PT_LC("invalidateexpression"), 1, &exprZv); + } + + /* the ArrayDimFetch arm of unsetExpression(): the scope carrying the + * unset offset (and the invalidated count()/sizeof() calls), $this for + * anything else */ + zv::Val unsetOffsetOfDimFetch(zend_object *expr) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + bool isArrayDimFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_ARRAY_DIM_FETCH, isArrayDimFetch))) return zv::Val(); + if (!isArrayDimFetch) return self_(); + zv::Ref dim = nodeProp(expr, PT_LC("dim")); + zv::Ref var = nodeProp(expr, PT_LC("var")); + if (UNEXPECTED(dim.raw() == NULL || var.raw() == NULL)) return zv::Val(); + if (dim.deref().isNull()) return self_(); + if (UNEXPECTED(!var.deref().isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Val exprVarType = getScopeStateType(var.deref().asObject()); + if (UNEXPECTED(exprVarType.isUndef())) return zv::Val(); + zv::Val dimType = thisGetType(dim.deref().raw()); + if (UNEXPECTED(dimType.isUndef())) return zv::Val(); + zv::Val unsetType = unsetOffset(exprVarType, dimType.raw()); + if (UNEXPECTED(unsetType.isUndef())) return zv::Val(); + zv::Val exprVarNativeType = getScopeStateNativeType(var.deref().asObject()); + if (UNEXPECTED(exprVarNativeType.isUndef())) return zv::Val(); + zv::Val dimNativeType = thisGetNativeType(dim.deref().raw()); + if (UNEXPECTED(dimNativeType.isUndef())) return zv::Val(); + zv::Val unsetNativeType = unsetOffset(exprVarNativeType, dimNativeType.raw()); + if (UNEXPECTED(unsetNativeType.isUndef())) return zv::Val(); + zv::Val thisScope = self_(); + zv::Val scope = otherAssignExpression(thisScope, var.deref().raw(), unsetType.raw(), unsetNativeType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + static const struct { const char *name; bool fullyQualified; } countCalls[] = { + {"count", true}, {"sizeof", true}, {"count", false}, {"sizeof", false}, + }; + for (const auto &countCall : countCalls) { + zv::Val call = newCountCall(countCall.name, countCall.fullyQualified, var.deref().raw()); + if (UNEXPECTED(call.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "invalidateExpression"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + scope = pt_type_call(scopeObject, PT_LC("invalidateexpression"), 1, call.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } + + bool varIsArrayDimFetch; + if (UNEXPECTED(!isInstance(var.deref(), PT_CLASS_ARRAY_DIM_FETCH, varIsArrayDimFetch))) return zv::Val(); + if (!varIsArrayDimFetch) return scope; + zend_object *varObject = var.deref().asObject(); + zv::Ref varDim = nodeProp(varObject, PT_LC("dim")); + zv::Ref varVar = nodeProp(varObject, PT_LC("var")); + if (UNEXPECTED(varDim.raw() == NULL || varVar.raw() == NULL)) return zv::Val(); + if (varDim.deref().isNull()) return scope; + zend_object *scopeObject = requireObject(scope, "getType"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Val outer = setOffsetFromScopeState(varVar.deref().raw(), varDim.deref().raw(), varObject, scopeObject, false); + if (UNEXPECTED(outer.isUndef())) return zv::Val(); + zv::Val outerNative = setOffsetFromScopeState(varVar.deref().raw(), varDim.deref().raw(), varObject, scopeObject, true); + if (UNEXPECTED(outerNative.isUndef())) return zv::Val(); + return otherAssignExpression(scope, varVar.deref().raw(), outer.raw(), outerNative.raw()); + } + + /* $this->($outerVar)->setOffsetValueType($scope->($outerDim), $scope->getScopeStateType($inner)) */ + zv::Val setOffsetFromScopeState(zval *outerVar, zval *outerDim, zend_object *inner, zend_object *scopeObject, bool native) + { + zv::Val outerType = native ? thisGetNativeType(outerVar) : thisGetType(outerVar); + if (UNEXPECTED(outerType.isUndef())) return zv::Val(); + zv::Val dimType = otherGetType(scopeObject, outerDim, native); + if (UNEXPECTED(dimType.isUndef())) return zv::Val(); + zval innerZv; + ZVAL_OBJ(&innerZv, inner); + auto readState = [&](MutatingScope &other) { return native ? other.getScopeStateNativeType(inner) : other.getScopeStateType(inner); }; + zv::Val innerType = native + ? otherPrivate(scopeObject, PT_LC("getscopestatenativetype"), 1, &innerZv, readState) + : otherPrivate(scopeObject, PT_LC("getscopestatetype"), 1, &innerZv, readState); + if (UNEXPECTED(innerType.isUndef())) return zv::Val(); + if (UNEXPECTED(!outerType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function setOffsetValueType() on %s", zend_zval_value_name(outerType.raw())); + return zv::Val(); + } + zv::Args args{dimType.raw(), innerType.raw()}; + return pt_type_call(outerType.ref().asObject(), PT_LC("setoffsetvaluetype"), 2, args); + } + + /* $type->unsetOffset($dimType) */ + static zv::Val unsetOffset(zv::Val &type, zval *dimType) + { + if (UNEXPECTED(!type.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function unsetOffset() on %s", zend_zval_value_name(type.raw())); + return zv::Val(); + } + return pt_type_call(type.ref().asObject(), PT_LC("unsetoffset"), 1, dimType); + } + + /* new FuncCall(new FullyQualified|Name($name), [new Arg($var)]) */ + static zv::Val newCountCall(const char *name, bool fullyQualified, zval *var) + { + zv::Val nameVal = zv::Val::string(name, strlen(name)); + zv::Val nameNode = fullyQualified + ? pt_type_new(PT_CLASS_FULLY_QUALIFIED, 1, nameVal.raw()) + : pt_type_new(PT_CLASS_NAME, 1, nameVal.raw()); + if (UNEXPECTED(nameNode.isUndef())) return zv::Val(); + zv::Val arg = pt_type_new(PT_CLASS_ARG, 1, var); + if (UNEXPECTED(arg.isUndef())) return zv::Val(); + zv::Arr args = zv::Arr::create(1); + args.push(arg.ref()); + zv::Args callArgs{nameNode.raw(), args.raw()}; + return pt_type_new(PT_CLASS_FUNC_CALL, 2, callArgs); + } + + /* (twin 3468) */ + zv::Val getStateType(zend_object *expr) { return resolveScopeStateType(expr, slotBool(PT_MS_PROP_NATIVE_TYPES_PROMOTED)); } + + /* private (twin 3478) */ + zv::Val getScopeStateNativeType(zend_object *expr) { return resolveScopeStateType(expr, true); } + + /* (twin 3625) */ + zv::Val specifyExpressionType(zend_object *expr, zval *type, zval *nativeType, zval *certainty) + { + bool noop; + if (UNEXPECTED(!isSpecifyExpressionTypeNoop(expr, type, noop))) return zv::Val(); + if (noop) return self_(); + zv::Val scope = openSpecificationScope(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "specifyExpressionTypeInPlace"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + if (UNEXPECTED(!otherSpecifyInPlace(scopeObject, expr, type, nativeType, certainty))) return zv::Val(); + return scope; + } + + /* $scope->specifyExpressionTypeInPlace(...) on any scope object */ + static bool otherSpecifyInPlace(zend_object *scopeObject, zend_object *expr, zval *type, zval *nativeType, zval *certainty) + { + zv::Args args{expr, type, nativeType, certainty}; + zv::Val result = otherPrivate(scopeObject, PT_LC("specifyexpressiontypeinplace"), 4, args, [&](MutatingScope &other) { + if (UNEXPECTED(!other.specifyExpressionTypeInPlace(expr, type, nativeType, certainty))) return zv::Val(); + return zv::Val::null(); + }); + return EXPECTED(!result.isUndef()); + } + + /* private (twin 3637) — an unpublished copy of this scope that in-place + * specification may mutate */ + zv::Val openSpecificationScope() + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + /* the twin passes $this->inFirstLevelStatement, not the getter */ + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + return scopeFactoryCreate(a); + } + + /* private (twin 3660); false = pending exception */ + [[nodiscard]] bool isSpecifyExpressionTypeNoop(zend_object *expr, zval *type, bool &out) + { + out = false; + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + bool isScalar; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_SCALAR, isScalar))) return false; + if (isScalar) { + out = true; + return true; + } + bool isConstFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_CONST_FETCH, isConstFetch))) return false; + if (isConstFetch) { + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + if (UNEXPECTED(!name.deref().isObject())) { + zend_throw_error(NULL, "Call to a member function toString() on %s", zend_zval_value_name(name.deref().raw())); + return false; + } + zv::Val nameString = pt_type_call(name.deref().asObject(), PT_LC("tostring"), 0, NULL); + if (UNEXPECTED(nameString.isUndef())) return false; + zend_string *raw = zval_get_string(nameString.raw()); + if (UNEXPECTED(raw == NULL)) return false; + zend_string *lowered = zend_string_tolower(raw); + zend_string_release(raw); + bool isConstant = zend_string_equals_literal(lowered, "true") + || zend_string_equals_literal(lowered, "false") + || zend_string_equals_literal(lowered, "null"); + zend_string_release(lowered); + if (isConstant) { + out = true; + return true; + } + } + + bool isFuncCall; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_FUNC_CALL, isFuncCall))) return false; + if (!isFuncCall) return true; + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return false; + bool isName; + if (UNEXPECTED(!isInstance(name.deref(), PT_CLASS_NAME, isName))) return false; + if (!isName) return true; + if (UNEXPECTED(Z_TYPE_P(type) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isFalse() on %s", zend_zval_value_name(type)); + return false; + } + zv::Val isFalse = pt_type_call(Z_OBJ_P(type), PT_LC("isfalse"), 0, NULL); + if (UNEXPECTED(isFalse.isUndef())) return false; + if (pt_type_trinary_value(isFalse.raw()) != PT_TRI_YES) return EXPECTED(EG(exception) == NULL); + zv::Ref provider = slot(PT_MS_PROP_REFLECTION_PROVIDER); + if (UNEXPECTED(!provider.isObject())) { + (void) uninitializedProperty("reflectionProvider"); + return false; + } + zv::Args resolveArgs{name.deref().raw(), thisZval()}; + zv::Val functionName = pt_type_call(provider.asObject(), PT_LC("resolvefunctionname"), 2, resolveArgs); + if (UNEXPECTED(functionName.isUndef())) return false; + if (functionName.ref().isNull()) return true; + zend_string *raw = zval_get_string(functionName.raw()); + if (UNEXPECTED(raw == NULL)) return false; + zend_string *lowered = zend_string_tolower(raw); + zend_string_release(raw); + out = zend_string_equals_literal(lowered, "is_dir") + || zend_string_equals_literal(lowered, "is_file") + || zend_string_equals_literal(lowered, "file_exists"); + zend_string_release(lowered); + return true; + } + + /* private (twin 3692) — writes straight into this scope's holder maps; + * only to be called on an unpublished scope (openSpecificationScope()); + * false = pending exception */ + [[nodiscard]] bool specifyExpressionTypeInPlace(zend_object *expr, zval *type, zval *nativeType, zval *certainty) + { + bool noop; + if (UNEXPECTED(!isSpecifyExpressionTypeNoop(expr, type, noop))) return false; + if (noop) return true; + + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + bool isArrayDimFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_ARRAY_DIM_FETCH, isArrayDimFetch))) return false; + if (isArrayDimFetch) { + zv::Ref dim = nodeProp(expr, PT_LC("dim")); + zv::Ref var = nodeProp(expr, PT_LC("var")); + if (UNEXPECTED(dim.raw() == NULL || var.raw() == NULL)) return false; + bool incDec = false; + if (!dim.deref().isNull()) { + for (int classIdx : { PT_CLASS_PRE_INC, PT_CLASS_PRE_DEC, PT_CLASS_POST_DEC, PT_CLASS_POST_INC }) { + bool is; + if (UNEXPECTED(!isInstance(dim.deref(), classIdx, is))) return false; + if (is) { + incDec = true; + break; + } + } + } + if (!dim.deref().isNull() && !incDec) { + if (UNEXPECTED(!dim.deref().isObject() || !var.deref().isObject())) { + pt_throw_should_not_happen(); + return false; + } + if (UNEXPECTED(!specifyArrayDimVarInPlace(dim.deref().asObject(), var.deref().asObject(), type, certainty))) return false; + } + } + + if (pt_type_trinary_value(certainty) == PT_TRI_NO) { + pt_throw_should_not_happen(); + return false; + } + if (UNEXPECTED(EG(exception))) return false; + + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return false; + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return false; + zend_long certaintyValue = pt_type_trinary_value(certainty); + if (UNEXPECTED(certaintyValue < 0)) return false; + zval holder; + pt_holder_create(&holder, &exprZv, type, certaintyValue); + zv::Val ownedHolder = zv::Val::adopt(holder); + zval nativeHolder; + pt_holder_create(&nativeHolder, &exprZv, nativeType, certaintyValue); + zv::Val ownedNativeHolder = zv::Val::adopt(nativeHolder); + if (UNEXPECTED(!writeScopeTable(self, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), key.get(), ownedHolder.raw()) + || !writeScopeTable(self, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes"), key.get(), ownedNativeHolder.raw()))) { + return false; + } + + bool isAlwaysRemembered; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_ALWAYS_REMEMBERED_EXPR, isAlwaysRemembered))) return false; + if (!isAlwaysRemembered) return true; + zv::Ref inner = nodeProp(expr, PT_LC("expr")); + if (UNEXPECTED(inner.raw() == NULL)) return false; + if (UNEXPECTED(!inner.deref().isObject())) { + pt_throw_should_not_happen(); + return false; + } + return specifyExpressionTypeInPlace(inner.deref().asObject(), type, nativeType, certainty); + } + + /* the ArrayDimFetch arm of specifyExpressionTypeInPlace(): the var's + * offset-accessible narrowing, applied in place */ + bool specifyArrayDimVarInPlace(zend_object *dim, zend_object *var, zval *type, zval *certainty) + { + zv::Val rawDimType = getScopeStateType(dim); + if (UNEXPECTED(rawDimType.isUndef())) return false; + if (UNEXPECTED(!rawDimType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function toArrayKey() on %s", zend_zval_value_name(rawDimType.raw())); + return false; + } + zv::Val dimType = pt_type_op(rawDimType.ref().asObject(), PT_OP_TO_ARRAY_KEY, 0, NULL); + if (UNEXPECTED(dimType.isUndef())) return false; + if (UNEXPECTED(!dimType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function isInteger() on %s", zend_zval_value_name(dimType.raw())); + return false; + } + zend_long dimIsInteger = pt_type_op_trinary(dimType.ref().asObject(), PT_OP_IS_INTEGER, 0, NULL); + if (UNEXPECTED(dimIsInteger < 0)) return false; + if (dimIsInteger != PT_TRI_YES) { + zend_long dimIsString = pt_type_op_trinary(dimType.ref().asObject(), PT_OP_IS_STRING, 0, NULL); + if (UNEXPECTED(dimIsString < 0)) return false; + if (dimIsString != PT_TRI_YES) return true; + } + + zv::Val exprVarType = getScopeStateType(var); + if (UNEXPECTED(exprVarType.isUndef())) return false; + if (UNEXPECTED(!exprVarType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function isArray() on %s", zend_zval_value_name(exprVarType.raw())); + return false; + } + zend_long isArray = pt_type_op_trinary(exprVarType.ref().asObject(), PT_OP_IS_ARRAY, 0, NULL); + if (UNEXPECTED(isArray < 0)) return false; + bool isMixed = pt_ce_mixed_type != NULL && instanceof_function(exprVarType.ref().asObject()->ce, pt_ce_mixed_type); + if (isMixed || isArray == PT_TRI_NO) return true; + + zv::Val varType = zv::Val::copyOf(exprVarType.ref()); + if (isArray != PT_TRI_YES) { + zv::Val accessible = dimIsInteger == PT_TRI_YES + ? pt_static_type_factory_int_offset_accessible() + : pt_static_type_factory_general_offset_accessible(); + if (UNEXPECTED(accessible.isUndef())) return false; + zv::Args args{exprVarType.raw(), accessible.raw()}; + varType = pt_type_combinator_intersect(2, args); + if (UNEXPECTED(varType.isUndef())) return false; + } + + zend_class_entry *dimCe = dimType.ref().asObject()->ce; + if ((pt_ce_constant_integer_type != NULL && instanceof_function(dimCe, pt_ce_constant_integer_type)) + || (pt_ce_constant_string_type != NULL && instanceof_function(dimCe, pt_ce_constant_string_type))) { + bool complex_; + if (UNEXPECTED(!isComplexUnionType(varType.raw(), complex_))) return false; + if (!complex_) { + zval hasOffsetZv; + if (UNEXPECTED(!pt_has_offset_value_type_new(&hasOffsetZv, dimType.raw(), type))) return false; + zv::Val hasOffset = zv::Val::adopt(hasOffsetZv); + zv::Args args{varType.raw(), hasOffset.raw()}; + zv::Val narrowed = pt_type_combinator_intersect(2, args); + if (UNEXPECTED(narrowed.isUndef())) return false; + varType = std::move(narrowed); + } + } + + zv::Val varNativeType = getScopeStateNativeType(var); + if (UNEXPECTED(varNativeType.isUndef())) return false; + return specifyExpressionTypeInPlace(var, varType.raw(), varNativeType.raw(), certainty); + } + + /* (twin 3757) */ + zv::Val assignExpression(zend_object *expr, zval *type, zval *nativeType) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val scope = self_(); + bool isPropertyFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_PROPERTY_FETCH, isPropertyFetch))) return zv::Val(); + if (isPropertyFetch) { + scope = thisInvalidateExpression(&exprZv, false, NULL, false); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "invalidateMethodsOnExpression"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Ref var = nodeProp(expr, PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + if (UNEXPECTED(!var.deref().isObject())) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_object *varObject = var.deref().asObject(); + zval varZv; + ZVAL_OBJ(&varZv, varObject); + scope = otherPrivate(scopeObject, PT_LC("invalidatemethodsonexpression"), 1, &varZv, [&](MutatingScope &other) { + return other.invalidateMethodsOnExpression(varObject); + }); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } else { + bool invalidate; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_STATIC_PROPERTY_FETCH, invalidate))) return zv::Val(); + if (!invalidate) { + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_VARIABLE, invalidate))) return zv::Val(); + } + if (invalidate) { + scope = thisInvalidateExpression(&exprZv, false, NULL, false); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + } + } + + zend_object *scopeObject = requireObject(scope, "specifyExpressionType"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Args args{expr, type, nativeType, pt_trinary_singleton(PT_TRI_YES)}; + return pt_type_call(scopeObject, PT_LC("specifyexpressiontype"), 4, args); + } + + /* private (twin 3382): assignExpression() for a value that overwrites + * what an already existing offset holds - a byref alias write or a + * setAlwaysOverwriteTypes() specification. An ArrayDimFetch gets the + * value written into the containing array (setExistingOffsetValueType() + * all the way up) instead of the parent being narrowed with + * HasOffsetValueType(dim, value), which collapses to never when the + * parent still holds the offset's previous constant value. */ + zv::Val overwriteExpression(zval *expr, zval *type, zval *nativeType) + { + bool isArrayDimFetch; + if (UNEXPECTED(!isInstance(zv::Ref(expr), PT_CLASS_ARRAY_DIM_FETCH, isArrayDimFetch))) return zv::Val(); + zv::Ref dim(NULL); + if (isArrayDimFetch) { + dim = nodeProp(Z_OBJ_P(expr), PT_LC("dim")); + if (UNEXPECTED(dim.raw() == NULL)) return zv::Val(); + } + if (!isArrayDimFetch || dim.deref().isNull()) return thisAssignExpression(expr, type, nativeType); + + zv::Ref var = nodeProp(Z_OBJ_P(expr), PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + zv::Val dimType = thisGetType(dim.deref().raw()); + if (UNEXPECTED(dimType.isUndef())) return zv::Val(); + + zv::Val varType = thisGetType(var.deref().raw()); + if (UNEXPECTED(varType.isUndef())) return zv::Val(); + if (UNEXPECTED(!varType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function setExistingOffsetValueType() on %s", zend_zval_value_name(varType.raw())); + return zv::Val(); + } + zv::Args typeArgs{dimType.raw(), type}; + zv::Val newVarType = pt_type_call(varType.ref().asObject(), PT_LC("setexistingoffsetvaluetype"), 2, typeArgs); + if (UNEXPECTED(newVarType.isUndef())) return zv::Val(); + + zv::Val varNativeType = thisGetNativeType(var.deref().raw()); + if (UNEXPECTED(varNativeType.isUndef())) return zv::Val(); + if (UNEXPECTED(!varNativeType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function setExistingOffsetValueType() on %s", zend_zval_value_name(varNativeType.raw())); + return zv::Val(); + } + zv::Args nativeTypeArgs{dimType.raw(), nativeType}; + zv::Val newVarNativeType = pt_type_call(varNativeType.ref().asObject(), PT_LC("setexistingoffsetvaluetype"), 2, nativeTypeArgs); + if (UNEXPECTED(newVarNativeType.isUndef())) return zv::Val(); + + zv::Val scope = overwriteExpression(var.deref().raw(), newVarType.raw(), newVarNativeType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "specifyExpressionType"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Args specifyArgs{expr, type, nativeType, pt_trinary_singleton(PT_TRI_YES)}; + return pt_type_call(scopeObject, PT_LC("specifyexpressiontype"), 4, specifyArgs); + } + + /* (twin 3785) */ + zv::Val assignInitializedProperty(zval *fetchedOnType, zend_string *propertyName) + { + bool inClass; + if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); + if (!inClass) return self_(); + zv::Val thisType = pt_type_call_static_ce(pt_ce_type_utils, PT_LC("findthistype"), 1, fetchedOnType); + if (UNEXPECTED(thisType.isUndef())) return zv::Val(); + if (thisType.ref().isNull()) return self_(); + zv::Args propertyArgs{fetchedOnType, propertyName}; + zv::Val propertyReflection = thisCallByName(PT_LC("getinstancepropertyreflection"), 2, propertyArgs); + if (UNEXPECTED(propertyReflection.isUndef())) return zv::Val(); + if (propertyReflection.ref().isNull()) return self_(); + if (UNEXPECTED(!propertyReflection.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getDeclaringClass() on %s", zend_zval_value_name(propertyReflection.raw())); + return zv::Val(); + } + zv::Val declaringClass = pt_type_call(propertyReflection.ref().asObject(), PT_LC("getdeclaringclass"), 0, NULL); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + if (UNEXPECTED(!declaringClass.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(declaringClass.raw())); + return zv::Val(); + } + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return zv::Val(); + if (UNEXPECTED(!classReflection.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(classReflection.raw())); + return zv::Val(); + } + zv::Val ownName = pt_class_reflection_get_name(classReflection.ref().asObject()); + zv::Val declaringName = pt_class_reflection_get_name(declaringClass.ref().asObject()); + if (UNEXPECTED(ownName.isUndef() || declaringName.isUndef())) return zv::Val(); + if (!ownName.ref().isString() || !declaringName.ref().isString() || !zend_string_equals(ownName.ref().asString(), declaringName.ref().asString())) { + return self_(); + } + zval propertyNameZv; + ZVAL_STR(&propertyNameZv, propertyName); + zv::Val hasNativeProperty = pt_type_call(declaringClass.ref().asObject(), PT_LC("hasnativeproperty"), 1, &propertyNameZv); + if (UNEXPECTED(hasNativeProperty.isUndef())) return zv::Val(); + if (!zend_is_true(hasNativeProperty.raw())) return self_(); + + zv::Val initializationExpr = pt_type_new(PT_CLASS_PROPERTY_INITIALIZATION_EXPR, 1, &propertyNameZv); + if (UNEXPECTED(initializationExpr.isUndef())) return zv::Val(); + zval mixedZv, nativeMixedZv; + if (UNEXPECTED(!pt_mixed_type_new(&mixedZv) || !pt_mixed_type_new(&nativeMixedZv))) return zv::Val(); + zv::Val mixedType = zv::Val::adopt(mixedZv); + zv::Val nativeMixedType = zv::Val::adopt(nativeMixedZv); + zv::Val scope = thisAssignExpression(initializationExpr.raw(), mixedType.raw(), nativeMixedType.raw()); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + + zend_object *scopeObject = requireObject(scope, "getFunction"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + zv::Val function = pt_type_call(scopeObject, PT_LC("getfunction"), 0, NULL); + if (UNEXPECTED(function.isUndef())) return zv::Val(); + bool isMethod; + if (UNEXPECTED(!isInstance(function.ref(), PT_CLASS_METHOD_REFLECTION, isMethod))) return zv::Val(); + if (!isMethod) return scope; + zv::Val functionName = pt_type_call(function.ref().asObject(), PT_LC("getname"), 0, NULL); + if (UNEXPECTED(functionName.isUndef())) return zv::Val(); + zend_string *raw = zval_get_string(functionName.raw()); + if (UNEXPECTED(raw == NULL)) return zv::Val(); + zend_string *lowered = zend_string_tolower(raw); + zend_string_release(raw); + bool isClone = zend_string_equals_literal(lowered, "__clone"); + zend_string_release(lowered); + if (!isClone) return scope; + zval *phpVersion = otherProp(scopeObject, PT_MS_PROP_PHP_VERSION, PT_LC("phpVersion")); + if (UNEXPECTED(phpVersion == NULL)) return zv::Val(); + ZVAL_DEREF(phpVersion); + if (UNEXPECTED(Z_TYPE_P(phpVersion) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function supportsReadonlyPropertyReinitializationOnClone() on %s", zend_zval_value_name(phpVersion)); + return zv::Val(); + } + bool supports; + if (UNEXPECTED(!otherCallBool(Z_OBJ_P(phpVersion), PT_LC("supportsreadonlypropertyreinitializationonclone"), supports))) return zv::Val(); + if (!supports) return scope; + zv::Val reinitializationExpr = pt_type_new(PT_CLASS_CLONE_REINITIALIZATION_EXPR, 1, &propertyNameZv); + if (UNEXPECTED(reinitializationExpr.isUndef())) return zv::Val(); + zval cloneMixedZv, cloneNativeMixedZv; + if (UNEXPECTED(!pt_mixed_type_new(&cloneMixedZv) || !pt_mixed_type_new(&cloneNativeMixedZv))) return zv::Val(); + zv::Val cloneMixed = zv::Val::adopt(cloneMixedZv); + zv::Val cloneNativeMixed = zv::Val::adopt(cloneNativeMixedZv); + return otherAssignExpression(scope, reinitializationExpr.raw(), cloneMixed.raw(), cloneNativeMixed.raw()); + } + + /* (twin 3813) */ + zv::Val invalidateExpression(zval *expressionToInvalidate, bool requireMoreCharacters, zval *invalidatingClass, bool keepPropertyFetches) + { + zv::Val exprString = thisGetNodeKey(expressionToInvalidate); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + zv::Ref exprPrinter = slot(PT_MS_PROP_EXPR_PRINTER); + if (UNEXPECTED(!exprPrinter.isObject())) return uninitializedProperty("exprPrinter"); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) { + return zv::Val(); + } + zv::Val result = pt_scope_ops_invalidate_expression_entries( + thisZval(), + exprPrinter.raw(), + key.get(), + expressionToInvalidate, + requireMoreCharacters, + invalidatingClass, + Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), + keepPropertyFetches); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (result.ref().isNull()) return self_(); + zval *expressionTypes = zend_hash_index_find(Z_ARRVAL_P(result.raw()), 0); + zval *nativeExpressionTypes = zend_hash_index_find(Z_ARRVAL_P(result.raw()), 1); + zval *conditionalExpressions = zend_hash_index_find(Z_ARRVAL_P(result.raw()), 2); + if (UNEXPECTED(expressionTypes == NULL || nativeExpressionTypes == NULL || conditionalExpressions == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: ScopeOps::invalidateExpressionEntries() answered an unexpected shape"); + return zv::Val(); + } + return scopeWith(Z_ARRVAL_P(expressionTypes), Z_ARRVAL_P(nativeExpressionTypes), Z_ARRVAL_P(conditionalExpressions), true); + } + + /* ScopeOps::scopeWith($this, ...) with the twin's argument list: the + * three tables as given, an empty call stack, the rest from $this */ + zv::Val scopeWith(HashTable *expressionTypes, HashTable *nativeExpressionTypes, HashTable *conditionalExpressions, bool emptyCallStack) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions") + || !requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) { + return zv::Val(); + } + return pt_scope_ops_scope_with( + thisZval(), + expressionTypes, + nativeExpressionTypes, + conditionalExpressions, + Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw()), + emptyCallStack ? (HashTable *) &zend_empty_array : Z_ARRVAL_P(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw()), + slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT), + slotBool(PT_MS_PROP_AFTER_EXTRACT_CALL)); + } + + /** @internal called by ScopeOps (twin 3846); false = pending exception */ + [[nodiscard]] bool isPrivatePropertyOfDifferentClass(zend_object *expr, zval *invalidatingClass, bool &out) + { + out = false; + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + bool isFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_STATIC_PROPERTY_FETCH, isFetch))) return false; + if (!isFetch) { + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_PROPERTY_FETCH, isFetch))) return false; + } + if (!isFetch) return true; + zv::Ref finder = slot(PT_MS_PROP_PROPERTY_REFLECTION_FINDER); + if (UNEXPECTED(!finder.isObject())) { + (void) uninitializedProperty("propertyReflectionFinder"); + return false; + } + zv::Args args{&exprZv, thisZval()}; + zv::Val propertyReflection = pt_type_call(finder.asObject(), PT_LC("findpropertyreflectionfromnode"), 2, args); + if (UNEXPECTED(propertyReflection.isUndef())) return false; + if (propertyReflection.ref().isNull()) return true; + if (UNEXPECTED(!propertyReflection.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function isPrivate() on %s", zend_zval_value_name(propertyReflection.raw())); + return false; + } + bool isPrivate; + if (UNEXPECTED(!otherCallBool(propertyReflection.ref().asObject(), PT_LC("isprivate"), isPrivate))) return false; + if (!isPrivate) return true; + zv::Val declaringClass = pt_type_call(propertyReflection.ref().asObject(), PT_LC("getdeclaringclass"), 0, NULL); + if (UNEXPECTED(declaringClass.isUndef())) return false; + if (UNEXPECTED(!declaringClass.ref().isObject() || Z_TYPE_P(invalidatingClass) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getName() on %s", zend_zval_value_name(declaringClass.raw())); + return false; + } + zv::Val declaringName = pt_class_reflection_get_name(declaringClass.ref().asObject()); + zv::Val invalidatingName = pt_class_reflection_get_name(Z_OBJ_P(invalidatingClass)); + if (UNEXPECTED(declaringName.isUndef() || invalidatingName.isUndef())) return false; + out = !(declaringName.ref().isString() && invalidatingName.ref().isString() + && zend_string_equals(declaringName.ref().asString(), invalidatingName.ref().asString())); + return true; + } + + /* private (twin 3863) */ + zv::Val invalidateMethodsOnExpression(zend_object *expressionToInvalidate) + { + zv::Ref exprPrinter = slot(PT_MS_PROP_EXPR_PRINTER); + if (UNEXPECTED(!exprPrinter.isObject())) return uninitializedProperty("exprPrinter"); + zval exprZv; + ZVAL_OBJ(&exprZv, expressionToInvalidate); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + zv::Val result = pt_scope_ops_invalidate_methods_on_expression( + exprPrinter.raw(), + key.get(), + Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw())); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (result.ref().isNull()) return self_(); + zval *expressionTypes = zend_hash_index_find(Z_ARRVAL_P(result.raw()), 0); + zval *nativeExpressionTypes = zend_hash_index_find(Z_ARRVAL_P(result.raw()), 1); + if (UNEXPECTED(expressionTypes == NULL || nativeExpressionTypes == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: ScopeOps::invalidateMethodsOnExpression() answered an unexpected shape"); + return zv::Val(); + } + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) return zv::Val(); + return scopeWith(Z_ARRVAL_P(expressionTypes), Z_ARRVAL_P(nativeExpressionTypes), Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), true); + } + + /* private (twin 3893); its only caller is applySpecifiedTypes() */ + zv::Val setExpressionCertaintyKeepingType(zend_object *expr, zval *certainty) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val exprString = thisGetNodeKey(&exprZv); + if (UNEXPECTED(exprString.isUndef())) return zv::Val(); + zv::Str key = zv::Str::adopt(zval_get_string(exprString.raw())); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + zval *holder = zend_symtable_find(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), key.get()); + if (holder == NULL) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zv::Val exprType = holderType(zv::Ref(holder)); + if (UNEXPECTED(exprType.isUndef())) return zv::Val(); + zval *nativeHolder = zend_symtable_find(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), key.get()); + zv::Val nativeType; + if (nativeHolder != NULL) { + nativeType = holderType(zv::Ref(nativeHolder)); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + } else { + nativeType = zv::Val::copyOf(exprType.ref()); + } + return thisSpecifyExpressionType(&exprZv, exprType.raw(), nativeType.raw(), certainty); + } + + /* private (twin 3916) — a large union whose intersection members carry + * HasOffsetValueType; false = pending exception */ + [[nodiscard]] bool isComplexUnionType(zval *type, bool &out) + { + out = false; + bool isUnion; + if (UNEXPECTED(!pt_type_instanceof_ce(type, pt_ce_union_type, isUnion))) return false; + if (!isUnion) return true; + zv::Val types = pt_type_op(Z_OBJ_P(type), PT_OP_GET_TYPES, 0, NULL); + if (UNEXPECTED(types.isUndef())) return false; + if (zend_hash_num_elements(Z_ARRVAL_P(types.raw())) <= PT_MS_COMPLEX_UNION_TYPE_MEMBER_LIMIT) return true; + for (auto entry : zv::ArrRef(types.raw())) { + bool isIntersection; + if (UNEXPECTED(!pt_type_instanceof_ce(entry.value().deref().raw(), pt_ce_intersection_type, isIntersection))) return false; + if (!isIntersection) continue; + zv::Val innerTypes = pt_type_op(entry.value().deref().asObject(), PT_OP_GET_TYPES, 0, NULL); + if (UNEXPECTED(innerTypes.isUndef())) return false; + for (auto innerEntry : zv::ArrRef(innerTypes.raw())) { + bool hasOffsetValue; + if (UNEXPECTED(!pt_type_instanceof_ce(innerEntry.value().deref().raw(), pt_ce_has_offset_value_type, hasOffsetValue))) return false; + if (hasOffsetValue) { + out = true; + return true; + } + } + } + return true; + } + + /* (twin 3930) */ + zv::Val addTypeToExpression(zend_object *expr, zval *type) + { + zv::Val originalExprType = getScopeStateType(expr); + if (UNEXPECTED(originalExprType.isUndef())) return zv::Val(); + bool complex_; + if (UNEXPECTED(!isComplexUnionType(originalExprType.raw(), complex_))) return zv::Val(); + if (complex_) return self_(); + zv::Val nativeType = getScopeStateNativeType(expr); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + if (UNEXPECTED(!originalExprType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function equals() on %s", zend_zval_value_name(originalExprType.raw())); + return zv::Val(); + } + zv::Val equals = pt_type_op(originalExprType.ref().asObject(), PT_OP_EQUALS, 1, nativeType.raw()); + if (UNEXPECTED(equals.isUndef())) return zv::Val(); + zv::Args intersectArgs{type, originalExprType.raw()}; + zv::Val newType = pt_type_combinator_intersect(2, intersectArgs); + if (UNEXPECTED(newType.isUndef())) return zv::Val(); + if (zend_is_true(equals.raw())) return thisSpecifyExpressionType(&exprZv, newType.raw(), newType.raw(), pt_trinary_singleton(PT_TRI_YES)); + zv::Args nativeIntersectArgs{type, nativeType.raw()}; + zv::Val newNativeType = pt_type_combinator_intersect(2, nativeIntersectArgs); + if (UNEXPECTED(newNativeType.isUndef())) return zv::Val(); + return thisSpecifyExpressionType(&exprZv, newType.raw(), newNativeType.raw(), pt_trinary_singleton(PT_TRI_YES)); + } + + /* (twin 3950) */ + zv::Val removeTypeFromExpression(zend_object *expr, zval *typeToRemove) + { + bool isNever; + if (UNEXPECTED(!pt_type_instanceof_ce(typeToRemove, pt_ce_never_type, isNever))) return zv::Val(); + if (isNever) return self_(); + zv::Val exprType = getScopeStateType(expr); + if (UNEXPECTED(exprType.isUndef())) return zv::Val(); + if (UNEXPECTED(!pt_type_instanceof_ce(exprType.raw(), pt_ce_never_type, isNever))) return zv::Val(); + if (isNever) return self_(); + bool complex_; + if (UNEXPECTED(!isComplexUnionType(exprType.raw(), complex_))) return zv::Val(); + if (complex_) return self_(); + zv::Val removed = pt_type_combinator_remove(exprType.raw(), typeToRemove); + if (UNEXPECTED(removed.isUndef())) return zv::Val(); + zv::Val nativeType = getScopeStateNativeType(expr); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + zv::Val removedNative = pt_type_combinator_remove(nativeType.raw(), typeToRemove); + if (UNEXPECTED(removedNative.isUndef())) return zv::Val(); + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + return thisSpecifyExpressionType(&exprZv, removed.raw(), removedNative.raw(), pt_trinary_singleton(PT_TRI_YES)); + } + + /* }}} */ + + /* {{{ twin 3993-4773: the narrowing application, the + * conditional-expression bookkeeping and the scope merges */ + + /* a table property of another scope object as a HashTable; NULL with an + * exception pending when the property is missing or unwritten */ + static HashTable *otherTable(zend_object *object, uint32_t nativeSlot, const char *name, size_t len) + { + zval *value = otherProp(object, nativeSlot, name, len); + if (UNEXPECTED(value == NULL)) return NULL; + ZVAL_DEREF(value); + if (UNEXPECTED(Z_TYPE_P(value) != IS_ARRAY)) { + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(object->ce->name), name); + return NULL; + } + return Z_ARRVAL_P(value); + } + + /* a bool property of another scope object; false = pending exception */ + [[nodiscard]] static bool otherBool(zend_object *object, uint32_t nativeSlot, const char *name, size_t len, bool &out) + { + zval *value = otherProp(object, nativeSlot, name, len); + if (UNEXPECTED(value == NULL)) return false; + ZVAL_DEREF(value); + if (UNEXPECTED(Z_TYPE_P(value) == IS_UNDEF)) { + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(object->ce->name), name); + return false; + } + out = zend_is_true(value); + return true; + } + + /* $scope->scopeFactory->create(...) on any scope object */ + static zv::Val otherScopeFactoryCreate(zend_object *scopeObject, CreateArgs &args) + { + zval *factory = otherProp(scopeObject, PT_MS_PROP_SCOPE_FACTORY, PT_LC("scopeFactory")); + if (UNEXPECTED(factory == NULL)) return zv::Val(); + ZVAL_DEREF(factory); + if (UNEXPECTED(Z_TYPE_P(factory) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function create() on %s", zend_zval_value_name(factory)); + return zv::Val(); + } + return factoryCreate(Z_OBJ_P(factory), args); + } + + /* $a->isSuperTypeOf($b)->no(); false = pending exception */ + [[nodiscard]] static bool isSuperTypeOfNo(zend_object *a, zval *b, bool &out) + { + zv::Val result = pt_type_op(a, PT_OP_IS_SUPER_TYPE_OF, 1, b); + if (UNEXPECTED(result.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function no() on %s", zend_zval_value_name(result.raw())); + return false; + } + zv::Val no = pt_type_call(Z_OBJ_P(result.raw()), PT_LC("no"), 0, NULL); + if (UNEXPECTED(no.isUndef())) return false; + out = zend_is_true(no.raw()); + return true; + } + + /* TypeCombinator::intersect($a, $b) */ + static zv::Val intersectTypes(zval *a, zval *b) + { + zv::Args args{a, b}; + return pt_type_combinator_intersect(2, args); + } + + /* TypeCombinator::union($a, $b) */ + static zv::Val unionTypes(zval *a, zval *b) + { + zv::Args args{a, b}; + return pt_type_combinator_union(2, args); + } + + /* $holder->getKey() of a ConditionalExpressionHolder: the native key + * builder for a native holder, the method otherwise; NULL = pending + * exception */ + static zend_string *conditionalHolderKey(zv::Ref holder) + { + holder = holder.deref(); + if (UNEXPECTED(!holder.isObject())) { + zend_throw_error(NULL, "Call to a member function getKey() on %s", zend_zval_value_name(holder.raw())); + return NULL; + } + if (EXPECTED(holder.asObject()->ce == pt_ce_cond_expr_holder)) { + zv::ObjRef object(holder.asObject()); + zv::Ref conditions = object.propAt(PT_CEH_PROP_CONDS).deref(); + zv::Ref typeHolder = object.propAt(PT_CEH_PROP_TYPEHOLDER).deref(); + if (UNEXPECTED(!conditions.isArray() || !typeHolder.isObject())) { + zend_throw_error(NULL, "phpstan_turbo: ConditionalExpressionHolder is not initialized"); + return NULL; + } + return pt_ceh_key_build(conditions.asArrayTable(), typeHolder.raw()); + } + zv::Val key = pt_type_call(holder.asObject(), PT_LC("getkey"), 0, NULL); + if (UNEXPECTED(key.isUndef())) return NULL; + return zval_get_string(key.raw()); + } + + /* $holder->getTypeHolder() / ->getConditionExpressionTypeHolders() of a + * ConditionalExpressionHolder: its slots when it is the native class, + * its methods otherwise */ + static zv::Val conditionalHolderRead(zv::Ref holder, uint32_t slot, const char *lcname, size_t len) + { + holder = holder.deref(); + if (UNEXPECTED(!holder.isObject())) { + zend_throw_error(NULL, "Call to a member function %s() on %s", lcname, zend_zval_value_name(holder.raw())); + return zv::Val(); + } + if (EXPECTED(holder.asObject()->ce == pt_ce_cond_expr_holder)) return zv::Val::copyOf(zv::ObjRef(holder.asObject()).propAt(slot).deref()); + return pt_type_call(holder.asObject(), lcname, len, 0, NULL); + } + + static zv::Val conditionalTypeHolder(zv::Ref holder) { return conditionalHolderRead(holder, PT_CEH_PROP_TYPEHOLDER, PT_LC("gettypeholder")); } + static zv::Val conditionalConditions(zv::Ref holder) { return conditionalHolderRead(holder, PT_CEH_PROP_CONDS, PT_LC("getconditionexpressiontypeholders")); } + + /* new ConditionalExpressionHolder($conditions, $typeHolder) — the + * shadowed class through its own class entry (rule 4) */ + static zv::Val newConditionalExpressionHolder(zv::Ref conditions, zv::Ref typeHolder) + { + zval raw; + object_init_ex(&raw, pt_ce_cond_expr_holder); + zv::Val holder = zv::Val::adopt(raw); + zv::ObjRef object(holder.ref().asObject()); + object.propAtWrite(PT_CEH_PROP_CONDS, zv::Val::copyOf(conditions.deref())); + object.propAtWrite(PT_CEH_PROP_TYPEHOLDER, zv::Val::copyOf(typeHolder.deref())); + return holder; + } + + /* (twin 3993 / 4003) */ + zv::Val filterByValue(zend_object *expr, bool truthy) + { + zv::Ref typeSpecifier = slot(PT_MS_PROP_TYPE_SPECIFIER); + if (UNEXPECTED(!typeSpecifier.isObject())) return uninitializedProperty("typeSpecifier"); + zv::Val context; + if (truthy) { + context = pt_type_call_static(PT_CLASS_TYPE_SPECIFIER_CONTEXT, PT_LC("createtruthy"), 0, NULL); + } else { + context = pt_type_call_static(PT_CLASS_TYPE_SPECIFIER_CONTEXT, PT_LC("createfalsey"), 0, NULL); + } + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Args args{thisZval(), expr, context.raw()}; + zv::Val specifiedTypes = pt_type_call(typeSpecifier.asObject(), PT_LC("specifytypesincondition"), 3, args); + if (UNEXPECTED(specifiedTypes.isUndef())) return zv::Val(); + + /* if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) { + * $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + * $expr, new ConstantBooleanType($truthy), TypeSpecifierContext::createTrue(), $this)); } */ + zv::Val isEquality = pt_type_call(Z_OBJ_P(specifiedTypes.raw()), PT_LC("isequality"), 0, NULL); + if (UNEXPECTED(isEquality.isUndef())) return zv::Val(); + if (zend_is_true(isEquality.raw())) { + zval exprValue; + ZVAL_OBJ(&exprValue, expr); + zv::Val exprType = thisGetType(&exprValue); + if (UNEXPECTED(exprType.isUndef())) return zv::Val(); + zend_long isBoolean = pt_type_op_trinary(Z_OBJ_P(exprType.raw()), PT_OP_IS_BOOLEAN, 0, NULL); + if (UNEXPECTED(isBoolean < 0)) return zv::Val(); + if (isBoolean == PT_TRI_YES) { + zval constantBoolean; + if (UNEXPECTED(!pt_constant_boolean_type_new(&constantBoolean, truthy))) return zv::Val(); + zv::Val booleanType = zv::Val::adopt(constantBoolean); + zv::Val trueContext = pt_type_call_static(PT_CLASS_TYPE_SPECIFIER_CONTEXT, PT_LC("createtrue"), 0, NULL); + if (UNEXPECTED(trueContext.isUndef())) return zv::Val(); + zval createArgs[4]; + ZVAL_OBJ(&createArgs[0], expr); + ZVAL_COPY_VALUE(&createArgs[1], booleanType.raw()); + ZVAL_COPY_VALUE(&createArgs[2], trueContext.raw()); + ZVAL_COPY_VALUE(&createArgs[3], thisZval()); + zv::Val equalityTypes = pt_type_call(typeSpecifier.asObject(), PT_LC("create"), 4, createArgs); + if (UNEXPECTED(equalityTypes.isUndef())) return zv::Val(); + specifiedTypes = pt_type_call(Z_OBJ_P(specifiedTypes.raw()), PT_LC("unionwith"), 1, equalityTypes.raw()); + if (UNEXPECTED(specifiedTypes.isUndef())) return zv::Val(); + } + } + + return thisApplySpecifiedTypes(specifiedTypes.raw()); + } + + /* one entry of applySpecifiedTypes()' sorted batch; exprString is + * borrowed from the batch's key owner, expr / type / terms from the + * SpecifiedTypes tables the caller holds */ + struct TypeSpecification + { + bool sure; + zend_string *exprString; + zval *expr; + zval *type; + zval *terms; + }; + + /* an owned copy of an array key, kept alive by $owner */ + static zend_string *ownedKey(zv::Arr &owner, zend_string *skey, zend_ulong idx) + { + zend_string *key = skey != NULL ? zend_string_copy(skey) : zend_long_to_str((zend_long) idx); + zval value; + ZVAL_STR(&value, key); + owner.push(zv::Val::adopt(value)); + return key; + } + + /* $expr instanceof Node\Scalar || $expr instanceof Array_ || $expr + * instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar */ + static bool isNeverSpecifiedExpr(zval *expr, bool &out) + { + out = false; + bool is; + if (UNEXPECTED(!isInstance(zv::Ref(expr), PT_CLASS_SCALAR, is))) return false; + if (is) { + out = true; + return true; + } + if (UNEXPECTED(!isInstance(zv::Ref(expr), PT_CLASS_ARRAY_EXPR, is))) return false; + if (is) { + out = true; + return true; + } + if (UNEXPECTED(!isInstance(zv::Ref(expr), PT_CLASS_UNARY_MINUS, is))) return false; + if (!is) return true; + zv::Ref inner = nodeProp(Z_OBJ_P(expr), PT_LC("expr")); + if (UNEXPECTED(inner.raw() == NULL)) return false; + return isInstance(inner.deref(), PT_CLASS_SCALAR, out); + } + + /* one of the three SpecifiedTypes tables into the batch */ + static bool collectTypeSpecifications(zval *table, bool sure, bool alternative, std::vector &out, zv::Arr &keyOwner) + { + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: SpecifiedTypes did not answer with an array"); + return false; + } + for (auto entry : zv::ArrRef(table)) { + zv::Ref pair = entry.value().deref(); + if (UNEXPECTED(!pair.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: a SpecifiedTypes entry is not an array"); + return false; + } + zval *expr = zend_hash_index_find(pair.asArrayTable(), 0); + zval *second = zend_hash_index_find(pair.asArrayTable(), 1); + if (UNEXPECTED(expr == NULL || second == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: a SpecifiedTypes entry has an unexpected shape"); + return false; + } + ZVAL_DEREF(expr); + ZVAL_DEREF(second); + if (UNEXPECTED(Z_TYPE_P(expr) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: a SpecifiedTypes entry has no expression"); + return false; + } + bool skip; + if (UNEXPECTED(!isNeverSpecifiedExpr(expr, skip))) return false; + if (skip) continue; + TypeSpecification spec; + spec.sure = sure; + spec.exprString = ownedKey(keyOwner, entry.stringKeyOrNull(), entry.indexKey()); + spec.expr = expr; + spec.type = alternative ? NULL : second; + spec.terms = alternative ? second : NULL; + out.push_back(spec); + } + return true; + } + + /* $specifiedTypes->shouldOverwrite(); false = pending exception */ + [[nodiscard]] static bool shouldOverwrite(zend_object *specifiedTypes, bool &out) + { + zv::Val result = pt_type_call(specifiedTypes, PT_LC("shouldoverwrite"), 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* $scope->isComplexUnionType($type) on any scope object */ + static bool otherIsComplexUnionType(zend_object *scopeObject, zval *type, bool &out) + { + zv::Val result = otherPrivate(scopeObject, PT_LC("iscomplexuniontype"), 1, type, [&](MutatingScope &other) { + bool value; + if (UNEXPECTED(!other.isComplexUnionType(type, value))) return zv::Val(); + return zv::Val::boolean(value); + }); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* + * applySpecifiedTypes()' alternative-form evaluator: the union over the + * entry's terms of `(sure ?? current) minus subtract`. $current may be + * NULL; $isNull is the twin's null answer (a current-type-dependent term + * with no known current type). false = pending exception. + */ + [[nodiscard]] static bool evaluateAlternativeTerms(zval *terms, zval *current, zv::Val &out, bool &isNull) + { + isNull = false; + if (UNEXPECTED(Z_TYPE_P(terms) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: an alternative SpecifiedTypes entry has no terms"); + return false; + } + uint32_t count = zend_hash_num_elements(Z_ARRVAL_P(terms)); + std::vector argv; + std::vector owned; + argv.reserve(count); + owned.reserve(count); + for (auto entry : zv::ArrRef(terms)) { + zv::Ref term = entry.value().deref(); + if (UNEXPECTED(!term.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: an alternative term is not an array"); + return false; + } + zval *sure = zend_hash_index_find(term.asArrayTable(), 0); + zval *subtract = zend_hash_index_find(term.asArrayTable(), 1); + if (UNEXPECTED(sure == NULL || subtract == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: an alternative term has an unexpected shape"); + return false; + } + ZVAL_DEREF(sure); + ZVAL_DEREF(subtract); + zval *base = Z_TYPE_P(sure) != IS_NULL ? sure : current; + if (base == NULL || Z_TYPE_P(base) == IS_NULL) { + isNull = true; + return true; + } + if (Z_TYPE_P(subtract) != IS_NULL) { + zv::Val removed = pt_type_combinator_remove(base, subtract); + if (UNEXPECTED(removed.isUndef())) return false; + argv.push_back(*removed.raw()); + owned.push_back(std::move(removed)); + } else { + argv.push_back(*base); + } + } + out = pt_type_combinator_union((uint32_t) argv.size(), argv.data()); + return EXPECTED(!out.isUndef()); + } + + /* applySpecifiedTypes()' in-place specification step: the batch's one + * unpublished working copy opens on the first specification and takes + * every later one. false = pending exception */ + [[nodiscard]] bool specifyInBatch(zend_object *expr, zval *newType, zval *newNativeType, zv::Val &scope, zend_object *&scopeObject, bool &scopeIsWorkingCopy) + { + bool noop; + if (UNEXPECTED(!isSpecifyExpressionTypeNoop(expr, newType, noop))) return false; + if (noop) return true; + if (!scopeIsWorkingCopy) { + zv::Val opened = otherPrivate(scopeObject, PT_LC("openspecificationscope"), 0, NULL, [&](MutatingScope &other) { return other.openSpecificationScope(); }); + if (UNEXPECTED(opened.isUndef())) return false; + zend_object *openedObject = requireObject(opened, "specifyExpressionTypeInPlace"); + if (UNEXPECTED(openedObject == NULL)) return false; + scope = std::move(opened); + scopeObject = openedObject; + scopeIsWorkingCopy = true; + } + return otherSpecifyInPlace(scopeObject, expr, newType, newNativeType, pt_trinary_singleton(PT_TRI_YES)); + } + + /* $specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $holderType) */ + static bool recordSpecifiedExpression(zv::Arr &specifiedExpressions, zend_object *scopeObject, zend_string *exprString, zend_object *expr, zval *fallbackType) + { + HashTable *table = otherTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(table == NULL)) return false; + zval *existing = zend_symtable_find(table, exprString); + zv::Val trackedType; + if (existing != NULL) { + trackedType = holderType(zv::Ref(existing)); + if (UNEXPECTED(trackedType.isUndef())) return false; + } else { + trackedType = zv::Val::copyOf(zv::Ref(fallbackType)); + } + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zval holder; + pt_holder_create(&holder, &exprZv, trackedType.raw(), PT_TRI_YES); + specifiedExpressions.set(exprString, zv::Val::adopt(holder)); + return true; + } + + /* (twin 4020) */ + zv::Val applySpecifiedTypes(zval *specifiedTypesArg) + { + if (UNEXPECTED(Z_TYPE_P(specifiedTypesArg) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getDeferredAugments() on %s", zend_zval_value_name(specifiedTypesArg)); + return zv::Val(); + } + zv::Val specifiedTypes = zv::Val::copyOf(zv::Ref(specifiedTypesArg)); + + /* the deferred augments see this scope's pre-application state — the + * application point of the narrowing; their entries join this batch */ + zv::Val augments = pt_type_call(Z_OBJ_P(specifiedTypes.raw()), PT_LC("getdeferredaugments"), 0, NULL); + if (UNEXPECTED(augments.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(augments.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: getDeferredAugments() did not answer with an array"); + return zv::Val(); + } + if (zend_hash_num_elements(Z_ARRVAL_P(augments.raw())) > 0) { + zv::Arr pending = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(augments.raw()))); + for (auto entry : zv::ArrRef(augments.raw())) { + pending.push(entry.value().deref()); + } + for (zend_ulong cursor = 0;; cursor++) { + zval *slotZv = zend_hash_index_find(pending.table(), cursor); + if (slotZv == NULL) break; + zv::Val augment = zv::Val::copyOf(zv::Ref(slotZv).deref()); + if (UNEXPECTED(Z_TYPE_P(augment.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function evaluate() on %s", zend_zval_value_name(augment.raw())); + return zv::Val(); + } + zv::Val augmentTypes = pt_type_call(Z_OBJ_P(augment.raw()), PT_LC("evaluate"), 1, thisZval()); + if (UNEXPECTED(augmentTypes.isUndef())) return zv::Val(); + if (Z_TYPE_P(augmentTypes.raw()) == IS_NULL) continue; + if (UNEXPECTED(Z_TYPE_P(augmentTypes.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getDeferredAugments() on %s", zend_zval_value_name(augmentTypes.raw())); + return zv::Val(); + } + zv::Val nested = pt_type_call(Z_OBJ_P(augmentTypes.raw()), PT_LC("getdeferredaugments"), 0, NULL); + if (UNEXPECTED(nested.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(nested.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: getDeferredAugments() did not answer with an array"); + return zv::Val(); + } + for (auto entry : zv::ArrRef(nested.raw())) { + pending.push(entry.value().deref()); + } + zv::Val united = pt_type_call(Z_OBJ_P(specifiedTypes.raw()), PT_LC("unionwith"), 1, augmentTypes.raw()); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(united.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: unionWith() did not answer with an object"); + return zv::Val(); + } + specifiedTypes = std::move(united); + } + } + + zend_object *specifiedTypesObject = Z_OBJ_P(specifiedTypes.raw()); + zv::Val sureTypes = pt_type_call(specifiedTypesObject, PT_LC("getsuretypes"), 0, NULL); + if (UNEXPECTED(sureTypes.isUndef())) return zv::Val(); + zv::Val sureNotTypes = pt_type_call(specifiedTypesObject, PT_LC("getsurenottypes"), 0, NULL); + if (UNEXPECTED(sureNotTypes.isUndef())) return zv::Val(); + zv::Val alternativeTypes = pt_type_call(specifiedTypesObject, PT_LC("getalternativetypes"), 0, NULL); + if (UNEXPECTED(alternativeTypes.isUndef())) return zv::Val(); + + std::vector typeSpecifications; + zv::Arr keyOwner = zv::Arr::create(8); + if (UNEXPECTED(!collectTypeSpecifications(sureTypes.raw(), true, false, typeSpecifications, keyOwner) + || !collectTypeSpecifications(sureNotTypes.raw(), false, false, typeSpecifications, keyOwner) + || !collectTypeSpecifications(alternativeTypes.raw(), true, true, typeSpecifications, keyOwner))) { + return zv::Val(); + } + + /* the twin's usort(): shorter keys first, sure specifications before + * sure-not ones; PHP's sort is stable */ + std::stable_sort(typeSpecifications.begin(), typeSpecifications.end(), [](const TypeSpecification &a, const TypeSpecification &b) { + if (ZSTR_LEN(a.exprString) != ZSTR_LEN(b.exprString)) return ZSTR_LEN(a.exprString) < ZSTR_LEN(b.exprString); + return a.sure && !b.sure; + }); + + zv::Val scope = self_(); + zend_object *scopeObject = self; + /* one unpublished working copy takes all in-place specifications of + * the batch; operations that go through other scope derivations + * publish it and a fresh copy opens on the next specification */ + bool scopeIsWorkingCopy = false; + zv::Arr specifiedExpressions = zv::Arr::create((uint32_t) typeSpecifications.size()); + + for (const TypeSpecification &specification : typeSpecifications) { + zend_object *expr = Z_OBJ_P(specification.expr); + zend_string *exprString = specification.exprString; + + bool isIssetExpr; + if (UNEXPECTED(!isInstance(zv::Ref(specification.expr), PT_CLASS_ISSET_EXPR, isIssetExpr))) return zv::Val(); + if (isIssetExpr) { + zv::Val inner = pt_type_call(expr, PT_LC("getexpr"), 0, NULL); + if (UNEXPECTED(inner.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(inner.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: IssetExpr::getExpr() did not answer with an object"); + return zv::Val(); + } + zv::Val next; + if (specification.sure) { + zv::Args args{inner.raw(), pt_trinary_singleton(PT_TRI_MAYBE)}; + next = otherPrivate(scopeObject, PT_LC("setexpressioncertaintykeepingtype"), 2, args, [&](MutatingScope &other) { + return other.setExpressionCertaintyKeepingType(Z_OBJ_P(inner.raw()), &args[1]); + }); + } else { + zval arg; + ZVAL_COPY_VALUE(&arg, inner.raw()); + next = otherPrivate(scopeObject, PT_LC("unsetexpression"), 1, &arg, [&](MutatingScope &other) { + return other.unsetExpression(Z_OBJ_P(inner.raw())); + }); + } + if (UNEXPECTED(next.isUndef())) return zv::Val(); + zend_object *nextObject = requireObject(next, "applySpecifiedTypes"); + if (UNEXPECTED(nextObject == NULL)) return zv::Val(); + scope = std::move(next); + scopeObject = nextObject; + scopeIsWorkingCopy = false; + continue; + } + + if (!specification.sure) { + /* removing type from a certainly-undefined variable cannot + * make it defined; a sure specification still can */ + bool isVariable; + if (UNEXPECTED(!isInstance(zv::Ref(specification.expr), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (isVariable) { + zv::Ref nameProp = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(nameProp.raw() == NULL)) return zv::Val(); + zv::Ref name = nameProp.deref(); + if (name.isString()) { + zval nameZv; + ZVAL_STR(&nameZv, name.asString()); + zv::Val has = pt_type_call(scopeObject, PT_LC("hasvariabletype"), 1, &nameZv); + if (UNEXPECTED(has.isUndef())) return zv::Val(); + zend_long value = pt_type_trinary_value(has.raw()); + if (UNEXPECTED(value < 0)) return zv::Val(); + if (value == PT_TRI_NO) continue; + } + } + } + + /* only Yes-certainty holders hold the current type of the + * expression */ + zv::Val trackedType, trackedNativeType; + bool hasTracked = false, hasTrackedNative = false; + { + HashTable *table = otherTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(table == NULL)) return zv::Val(); + zval *holder = zend_symtable_find(table, exprString); + if (holder != NULL) { + zend_long certainty = holderCertainty(zv::Ref(holder)); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_YES) { + trackedType = holderType(zv::Ref(holder)); + if (UNEXPECTED(trackedType.isUndef())) return zv::Val(); + hasTracked = true; + } + } + } + { + HashTable *table = otherTable(scopeObject, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + if (UNEXPECTED(table == NULL)) return zv::Val(); + zval *holder = zend_symtable_find(table, exprString); + if (holder != NULL) { + zend_long certainty = holderCertainty(zv::Ref(holder)); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_YES) { + trackedNativeType = holderType(zv::Ref(holder)); + if (UNEXPECTED(trackedNativeType.isUndef())) return zv::Val(); + hasTrackedNative = true; + } + } + } + bool skipSpecification = false; + if (!hasTracked) { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val currentTypes = otherPrivate(scopeObject, PT_LC("getcurrenttypesofspecifiedexpr"), 1, &exprZv, [&](MutatingScope &other) { + return other.getCurrentTypesOfSpecifiedExpr(expr); + }); + if (UNEXPECTED(currentTypes.isUndef())) return zv::Val(); + if (Z_TYPE_P(currentTypes.raw()) != IS_NULL) { + if (UNEXPECTED(Z_TYPE_P(currentTypes.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: getCurrentTypesOfSpecifiedExpr() answered an unexpected shape"); + return zv::Val(); + } + zval *phpDoc = zend_hash_index_find(Z_ARRVAL_P(currentTypes.raw()), 0); + zval *native = zend_hash_index_find(Z_ARRVAL_P(currentTypes.raw()), 1); + if (UNEXPECTED(phpDoc == NULL || native == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: getCurrentTypesOfSpecifiedExpr() answered an unexpected shape"); + return zv::Val(); + } + bool complexUnion; + if (UNEXPECTED(!otherIsComplexUnionType(scopeObject, phpDoc, complexUnion))) return zv::Val(); + if (complexUnion) continue; + trackedType = zv::Val::copyOf(zv::Ref(phpDoc)); + hasTracked = true; + if (!hasTrackedNative) { + trackedNativeType = zv::Val::copyOf(zv::Ref(native)); + hasTrackedNative = true; + } + } + } else { + bool overwrite; + if (UNEXPECTED(!shouldOverwrite(specifiedTypesObject, overwrite))) return zv::Val(); + if (!overwrite) { + /* mirrors addTypeToExpression()/removeTypeFromExpression() */ + bool complexUnion; + if (UNEXPECTED(!otherIsComplexUnionType(scopeObject, trackedType.raw(), complexUnion))) return zv::Val(); + if (complexUnion) { + skipSpecification = true; + } + } + } + if (skipSpecification) continue; + + if (specification.terms != NULL) { + /* an alternative-form entry: the union over its terms of + * `(sure ?? current) minus subtract`, evaluated here at the + * application point */ + zv::Val evaluated; + bool isNull; + if (UNEXPECTED(!evaluateAlternativeTerms(specification.terms, hasTracked ? trackedType.raw() : NULL, evaluated, isNull))) return zv::Val(); + if (isNull) continue; + zval *nativeCurrent = hasTrackedNative ? trackedNativeType.raw() : (hasTracked ? trackedType.raw() : NULL); + zv::Val evaluatedNative; + bool nativeIsNull; + if (UNEXPECTED(!evaluateAlternativeTerms(specification.terms, nativeCurrent, evaluatedNative, nativeIsNull))) return zv::Val(); + if (nativeIsNull) { + evaluatedNative = zv::Val::copyOf(evaluated.ref()); + } + zv::Val newType = hasTracked ? intersectTypes(evaluated.raw(), trackedType.raw()) : zv::Val::copyOf(evaluated.ref()); + if (UNEXPECTED(newType.isUndef())) return zv::Val(); + zv::Val newNativeType = hasTrackedNative ? intersectTypes(evaluatedNative.raw(), trackedNativeType.raw()) : std::move(evaluatedNative); + if (UNEXPECTED(newNativeType.isUndef())) return zv::Val(); + if (UNEXPECTED(!specifyInBatch(expr, newType.raw(), newNativeType.raw(), scope, scopeObject, scopeIsWorkingCopy))) return zv::Val(); + if (UNEXPECTED(!recordSpecifiedExpression(specifiedExpressions, scopeObject, exprString, expr, newType.raw()))) return zv::Val(); + continue; + } + + zval *type = specification.type; + if (specification.sure) { + bool overwrite; + if (UNEXPECTED(!shouldOverwrite(specifiedTypesObject, overwrite))) return zv::Val(); + if (overwrite) { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val assigned = otherOverwriteExpression(scope, &exprZv, type, type); + if (UNEXPECTED(assigned.isUndef())) return zv::Val(); + zend_object *assignedObject = requireObject(assigned, "applySpecifiedTypes"); + if (UNEXPECTED(assignedObject == NULL)) return zv::Val(); + scope = std::move(assigned); + scopeObject = assignedObject; + scopeIsWorkingCopy = false; + } else { + zv::Val newType = hasTracked ? intersectTypes(type, trackedType.raw()) : zv::Val::copyOf(zv::Ref(type)); + if (UNEXPECTED(newType.isUndef())) return zv::Val(); + zv::Val newNativeType = hasTrackedNative ? intersectTypes(type, trackedNativeType.raw()) : zv::Val::copyOf(zv::Ref(type)); + if (UNEXPECTED(newNativeType.isUndef())) return zv::Val(); + if (UNEXPECTED(!specifyInBatch(expr, newType.raw(), newNativeType.raw(), scope, scopeObject, scopeIsWorkingCopy))) return zv::Val(); + } + } else { + bool isNever; + if (UNEXPECTED(!pt_type_instanceof_ce(type, pt_ce_never_type, isNever))) return zv::Val(); + if (!isNever && hasTracked) { + if (UNEXPECTED(!pt_type_instanceof_ce(trackedType.raw(), pt_ce_never_type, isNever))) return zv::Val(); + } + if (isNever) continue; + if (!hasTracked) { + /* the expression is not tracked - there is nothing to + * subtract from */ + continue; + } + zv::Val newType = pt_type_combinator_remove(trackedType.raw(), type); + if (UNEXPECTED(newType.isUndef())) return zv::Val(); + zv::Val newNativeType = hasTrackedNative ? pt_type_combinator_remove(trackedNativeType.raw(), type) : zv::Val::copyOf(newType.ref()); + if (UNEXPECTED(newNativeType.isUndef())) return zv::Val(); + if (UNEXPECTED(!specifyInBatch(expr, newType.raw(), newNativeType.raw(), scope, scopeObject, scopeIsWorkingCopy))) return zv::Val(); + } + + if (UNEXPECTED(!recordSpecifiedExpression(specifiedExpressions, scopeObject, exprString, expr, type))) return zv::Val(); + } + + { + zval specifiedExpressionsZv; + ZVAL_COPY_VALUE(&specifiedExpressionsZv, specifiedExpressions.raw()); + zv::Val processed = otherPrivate(scopeObject, PT_LC("processconditionalexpressionsafterspecifying"), 1, &specifiedExpressionsZv, [&](MutatingScope &other) { + return other.processConditionalExpressionsAfterSpecifying(specifiedExpressions.table()); + }); + if (UNEXPECTED(processed.isUndef())) return zv::Val(); + zend_object *processedObject = requireObject(processed, "applySpecifiedTypes"); + if (UNEXPECTED(processedObject == NULL)) return zv::Val(); + scope = std::move(processed); + scopeObject = processedObject; + } + + zv::Val newHolders = pt_type_call(specifiedTypesObject, PT_LC("getnewconditionalexpressionholders"), 0, NULL); + if (UNEXPECTED(newHolders.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(newHolders.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: getNewConditionalExpressionHolders() did not answer with an array"); + return zv::Val(); + } + zv::Arr newConditionalExpressionHolders = zv::Arr::copyOfTable(Z_ARRVAL_P(newHolders.raw())); + zv::Val recipes = pt_type_call(specifiedTypesObject, PT_LC("getconditionalexpressionholderrecipes"), 0, NULL); + if (UNEXPECTED(recipes.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(recipes.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: getConditionalExpressionHolderRecipes() did not answer with an array"); + return zv::Val(); + } + for (auto recipeEntry : zv::ArrRef(recipes.raw())) { + zv::Ref recipe = recipeEntry.value().deref(); + if (UNEXPECTED(!recipe.isObject())) { + zend_throw_error(NULL, "Call to a member function evaluate() on %s", zend_zval_value_name(recipe.raw())); + return zv::Val(); + } + /* the recipes' state-dependent math runs here, against this + * scope's pre-application state */ + zv::Val evaluated = pt_type_call(recipe.asObject(), PT_LC("evaluate"), 1, thisZval()); + if (UNEXPECTED(evaluated.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(evaluated.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a ConditionalExpressionHolderRecipe did not answer with an array"); + return zv::Val(); + } + for (auto entry : zv::ArrRef(evaluated.raw())) { + zv::Ref recipeHolders = entry.value().deref(); + if (UNEXPECTED(!recipeHolders.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: a recipe entry is not an array"); + return zv::Val(); + } + newConditionalExpressionHolders.separate(); + zend_string *key = entry.stringKeyOrNull(); + zend_ulong index = entry.indexKey(); + zval *inner = pt_ht_find(newConditionalExpressionHolders.table(), key, index); + if (inner == NULL) { + zval fresh; + array_init(&fresh); + pt_ht_add_new(newConditionalExpressionHolders.table(), key, index, &fresh); + inner = pt_ht_find(newConditionalExpressionHolders.table(), key, index); + } else { + ZVAL_DEREF(inner); + if (UNEXPECTED(Z_TYPE_P(inner) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return zv::Val(); + } + SEPARATE_ARRAY(inner); + } + for (auto holderEntry : zv::TableRef(recipeHolders.asArrayTable())) { + zval copy; + ZVAL_COPY(©, holderEntry.value().deref().raw()); + pt_ht_update(Z_ARRVAL_P(inner), holderEntry.stringKeyOrNull(), holderEntry.indexKey(), ©); + } + } + } + + CreateArgs a; + if (UNEXPECTED(!fillCreateArgsFromOther(a, scopeObject))) return zv::Val(); + HashTable *scopeConditionalExpressions = otherTable(scopeObject, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + if (UNEXPECTED(scopeConditionalExpressions == NULL)) return zv::Val(); + PT_MS_ARG_CREATE(a, CreateArgs::CONDITIONAL_EXPRESSIONS, mergeConditionalExpressions(newConditionalExpressionHolders.table(), scopeConditionalExpressions)); + return otherScopeFactoryCreate(scopeObject, a); + } + + /* the create() argument list every narrowing-application site builds + * out of another scope: its properties as the twin spells them, its dispatched + * isDeclareStrictTypes() / getFunction() / getNamespace() */ + static bool fillCreateArgsFromOther(CreateArgs &a, zend_object *scopeObject) + { + struct + { + uint32_t arg; + uint32_t slot; + const char *name; + size_t len; + } properties[] = { + { CreateArgs::CONTEXT, PT_MS_PROP_CONTEXT, PT_LC("context") }, + { CreateArgs::EXPRESSION_TYPES, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes") }, + { CreateArgs::NATIVE_EXPRESSION_TYPES, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes") }, + { CreateArgs::CONDITIONAL_EXPRESSIONS, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions") }, + { CreateArgs::IN_CLOSURE_BIND_SCOPE_CLASSES, PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES, PT_LC("inClosureBindScopeClasses") }, + { CreateArgs::ANONYMOUS_FUNCTION_REFLECTION, PT_MS_PROP_ANONYMOUS_FUNCTION_REFLECTION, PT_LC("anonymousFunctionReflection") }, + { CreateArgs::IN_FIRST_LEVEL_STATEMENT, PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, PT_LC("inFirstLevelStatement") }, + { CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS, PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, PT_LC("currentlyAssignedExpressions") }, + { CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, PT_LC("currentlyAllowedUndefinedExpressions") }, + { CreateArgs::IN_FUNCTION_CALLS_STACK, PT_MS_PROP_IN_FUNCTION_CALLS_STACK, PT_LC("inFunctionCallsStack") }, + { CreateArgs::AFTER_EXTRACT_CALL, PT_MS_PROP_AFTER_EXTRACT_CALL, PT_LC("afterExtractCall") }, + { CreateArgs::PARENT_SCOPE, PT_MS_PROP_PARENT_SCOPE, PT_LC("parentScope") }, + { CreateArgs::NATIVE_TYPES_PROMOTED, PT_MS_PROP_NATIVE_TYPES_PROMOTED, PT_LC("nativeTypesPromoted") }, + { CreateArgs::TEMPLATE_ARGUMENT_FRAME, PT_MS_PROP_TEMPLATE_ARGUMENT_FRAME, PT_LC("templateArgumentFrame") }, + { CreateArgs::TEMPLATE_ARGUMENT_CONSTRAINTS, PT_MS_PROP_TEMPLATE_ARGUMENT_CONSTRAINTS, PT_LC("templateArgumentConstraints") }, + }; + for (auto &property : properties) { + zval *value = otherProp(scopeObject, property.slot, property.name, property.len); + if (UNEXPECTED(value == NULL)) return false; + ZVAL_DEREF(value); + if (UNEXPECTED(Z_TYPE_P(value) == IS_UNDEF)) { + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(scopeObject->ce->name), property.name); + return false; + } + a.set(property.arg, zv::Ref(value)); + } + zv::Val declareStrictTypes = pt_type_call(scopeObject, PT_LC("isdeclarestricttypes"), 0, NULL); + if (UNEXPECTED(declareStrictTypes.isUndef())) return false; + a.setBool(CreateArgs::DECLARE_STRICT_TYPES, zend_is_true(declareStrictTypes.raw())); + zv::Val function = pt_type_call(scopeObject, PT_LC("getfunction"), 0, NULL); + if (UNEXPECTED(function.isUndef())) return false; + a.setOwned(CreateArgs::FUNCTION, std::move(function)); + zv::Val namespace_ = pt_type_call(scopeObject, PT_LC("getnamespace"), 0, NULL); + if (UNEXPECTED(namespace_.isUndef())) return false; + a.setOwned(CreateArgs::NAMESPACE_, std::move(namespace_)); + return true; + } + + /* $a->equalTypes($b): the native holders' body when both are native, the + * method of anything else; false = pending exception */ + [[nodiscard]] static bool holderEqualTypes(zv::Ref a, zv::Ref b, bool &out) + { + a = a.deref(); + b = b.deref(); + if (UNEXPECTED(!a.isObject())) { + zend_throw_error(NULL, "Call to a member function equalTypes() on %s", zend_zval_value_name(a.raw())); + return false; + } + if (EXPECTED(a.asObject()->ce == pt_ce_expr_type_holder && b.isObject() && b.asObject()->ce == pt_ce_expr_type_holder)) { + return pt_holder_equal_types(a.raw(), b.raw(), &out); + } + zv::Val result = pt_type_call(a.asObject(), PT_LC("equaltypes"), 1, b.raw()); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* $table[$exprKey][$key] = $holder, vivifying the inner array like PHP */ + static bool appendConditionalHolder(zv::Arr &result, zend_string *exprKey, zend_ulong exprIdx, zend_string *key, zend_ulong keyIdx, zv::Ref holder) + { + result.separate(); + zval *inner = pt_ht_find(result.table(), exprKey, exprIdx); + if (inner == NULL) { + zval fresh; + array_init(&fresh); + pt_ht_add_new(result.table(), exprKey, exprIdx, &fresh); + inner = pt_ht_find(result.table(), exprKey, exprIdx); + } else { + ZVAL_DEREF(inner); + if (UNEXPECTED(Z_TYPE_P(inner) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return false; + } + SEPARATE_ARRAY(inner); + } + zval copy; + ZVAL_COPY(©, holder.deref().raw()); + pt_ht_update(Z_ARRVAL_P(inner), key, keyIdx, ©); + return true; + } + + /* isset($table[$exprKey][$key]) */ + static bool conditionalHolderIsSet(zv::Arr &table, zend_string *exprKey, zend_ulong exprIdx, zend_string *key, zend_ulong keyIdx) + { + zval *inner = pt_ht_find(table.table(), exprKey, exprIdx); + if (inner == NULL) return false; + ZVAL_DEREF(inner); + if (Z_TYPE_P(inner) != IS_ARRAY) return false; + zval *found = pt_ht_find(Z_ARRVAL_P(inner), key, keyIdx); + return found != NULL && Z_TYPE_P(found) != IS_NULL; + } + + /* + * private (twin 4283) — matches the registered conditional expressions + * against the just-specified holders and applies the consequences. + * Mutates and returns $this. + */ + zv::Val processConditionalExpressionsAfterSpecifying(HashTable *specifiedExpressions) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") + || !requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) { + return zv::Val(); + } + zv::Val matched = pt_scope_ops_match_conditional_expressions(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), specifiedExpressions); + if (UNEXPECTED(matched.isUndef())) return zv::Val(); + zval *conditions = zend_hash_index_find(Z_ARRVAL_P(matched.raw()), 0); + if (UNEXPECTED(conditions == NULL || Z_TYPE_P(conditions) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: ScopeOps::matchConditionalExpressions() answered an unexpected shape"); + return zv::Val(); + } + zv::Arr matchedConditions = zv::Arr::copyOfTable(Z_ARRVAL_P(conditions)); + for (auto entry : zv::TableRef(matchedConditions.table())) { + zend_string *skey = entry.stringKeyOrNull(); + zv::Str conditionalExprString = zv::Str::adopt(skey != NULL ? zend_string_copy(skey) : zend_long_to_str((zend_long) entry.indexKey())); + zv::Ref expressions = entry.value().deref(); + if (UNEXPECTED(!expressions.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: a matched conditional expressions entry is not an array"); + return zv::Val(); + } + HashTable *holders = expressions.asArrayTable(); + + /* TrinaryLogic::lazyExtremeIdentity(): the operands' value when + * they all agree, Maybe when any differs */ + zend_long certainty = 0; + bool first = true, differs = false; + for (auto holderEntry : zv::TableRef(holders)) { + zv::Val typeHolder = conditionalTypeHolder(holderEntry.value()); + if (UNEXPECTED(typeHolder.isUndef())) return zv::Val(); + zend_long value = holderCertainty(typeHolder.ref()); + if (UNEXPECTED(value < 0)) return zv::Val(); + if (first) { + certainty = value; + first = false; + continue; + } + if (certainty != value) { + differs = true; + break; + } + } + if (UNEXPECTED(first)) { + pt_throw_should_not_happen(); + return zv::Val(); + } + if (differs) { + certainty = PT_TRI_MAYBE; + } + + if (certainty == PT_TRI_NO) { + if (UNEXPECTED(!writeScopeTable(self, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), conditionalExprString.get(), NULL))) { + return zv::Val(); + } + continue; + } + + zval *existing = zend_symtable_find(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), conditionalExprString.get()); + if (existing == NULL) { + zv::Val typeHolder = conditionalTypeHolder((*zv::TableRef(holders).begin()).value()); + if (UNEXPECTED(typeHolder.isUndef())) return zv::Val(); + if (UNEXPECTED(!writeScopeTable(self, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), conditionalExprString.get(), typeHolder.raw()))) { + return zv::Val(); + } + continue; + } + + zv::Val type; + for (auto holderEntry : zv::TableRef(holders)) { + zv::Val typeHolder = conditionalTypeHolder(holderEntry.value()); + if (UNEXPECTED(typeHolder.isUndef())) return zv::Val(); + zv::Val holderTypeValue = holderType(typeHolder.ref()); + if (UNEXPECTED(holderTypeValue.isUndef())) return zv::Val(); + if (type.isUndef()) { + type = std::move(holderTypeValue); + continue; + } + zv::Val intersected = intersectTypes(type.raw(), holderTypeValue.raw()); + if (UNEXPECTED(intersected.isUndef())) return zv::Val(); + type = std::move(intersected); + } + zv::Val existingExpr = holderExpr(zv::Ref(existing)); + zv::Val existingType = holderType(zv::Ref(existing)); + if (UNEXPECTED(existingExpr.isUndef() || existingType.isUndef())) return zv::Val(); + zend_long existingCertainty = holderCertainty(zv::Ref(existing)); + if (UNEXPECTED(existingCertainty < 0)) return zv::Val(); + zv::Val intersected = intersectTypes(existingType.raw(), type.raw()); + if (UNEXPECTED(intersected.isUndef())) return zv::Val(); + /* TrinaryLogic::maxMin() */ + zend_long maxMin = ((existingCertainty | certainty) == PT_TRI_YES) ? PT_TRI_YES : (existingCertainty & certainty); + zval holder; + pt_holder_create(&holder, existingExpr.raw(), intersected.raw(), maxMin); + zv::Val holderValue = zv::Val::adopt(holder); + if (UNEXPECTED(!writeScopeTable(self, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), conditionalExprString.get(), holderValue.raw()))) { + return zv::Val(); + } + } + + return self_(); + } + + /* (twin 4316) */ + zv::Val getConditionalExpressions() + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) return zv::Val(); + return copyOfSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS); + } + + /* ScopeOps::scopeWith($this, ...) with the twin's full argument list */ + zv::Val scopeWithTables(HashTable *expressionTypes, HashTable *nativeExpressionTypes, HashTable *conditionalExpressions, HashTable *currentlyAssignedExpressions, HashTable *currentlyAllowedUndefinedExpressions, HashTable *inFunctionCallsStack, bool inFirstLevelStatement, bool afterExtractCall) + { + return pt_scope_ops_scope_with(thisZval(), expressionTypes, nativeExpressionTypes, conditionalExpressions, currentlyAssignedExpressions, currentlyAllowedUndefinedExpressions, inFunctionCallsStack, inFirstLevelStatement, afterExtractCall); + } + + /* (twin 4324) */ + zv::Val addConditionalExpressions(zend_string *exprString, HashTable *conditionalExpressionHolders) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") + || !requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions") + || !requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) { + return zv::Val(); + } + zv::Arr conditionalExpressions = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw())); + zval *entry = zend_symtable_find(conditionalExpressions.table(), exprString); + if (entry != NULL) { + ZVAL_DEREF(entry); + } + /* Merge rather than overwrite: holder keys disambiguate identical + * entries so the earlier bindings survive */ + zv::Arr existing = (entry != NULL && Z_TYPE_P(entry) == IS_ARRAY) + ? zv::Arr::copyOfTable(Z_ARRVAL_P(entry)) + : zv::Arr::create(zend_hash_num_elements(conditionalExpressionHolders)); + for (auto holderEntry : zv::TableRef(conditionalExpressionHolders)) { + zend_string *key = conditionalHolderKey(holderEntry.value()); + if (UNEXPECTED(key == NULL)) return zv::Val(); + zv::Str keyString = zv::Str::adopt(key); + existing.separate(); + zval copy; + ZVAL_COPY(©, holderEntry.value().deref().raw()); + zend_hash_update(existing.table(), keyString.get(), ©); + } + conditionalExpressions.set(exprString, zv::Val(std::move(existing))); + + return scopeWithTables( + Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), + conditionalExpressions.table(), + Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw()), + slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT), + slotBool(PT_MS_PROP_AFTER_EXTRACT_CALL)); + } + + /* (twin 4354) */ + zv::Val exitFirstLevelStatements() + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, "inFirstLevelStatement"))) return zv::Val(); + if (!slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)) return self_(); + zv::Ref memo = slot(PT_MS_PROP_SCOPE_OUT_OF_FIRST_LEVEL_STATEMENT); + if (!memo.isUndef() && Z_TYPE_P(memo.raw()) != IS_NULL) return zv::Val::copyOf(memo); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions") + || !requireSlot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK, "inFunctionCallsStack"))) { + return zv::Val(); + } + zv::Val scope = scopeWithTables( + Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw()), + Z_ARRVAL_P(slot(PT_MS_PROP_IN_FUNCTION_CALLS_STACK).raw()), + false, + slotBool(PT_MS_PROP_AFTER_EXTRACT_CALL)); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + if (UNEXPECTED(!assignResolvedTypes(scope))) return zv::Val(); + writeSlot(PT_MS_PROP_SCOPE_OUT_OF_FIRST_LEVEL_STATEMENT, zv::Val::copyOf(scope.ref())); + return scope; + } + + /* (twin 4388) */ + zv::Val mergeWith(zval *otherScope, bool preserveVacuousConditionals) + { + zv::Val merged = mergeWithVariableState(otherScope, preserveVacuousConditionals); + if (UNEXPECTED(merged.isUndef())) return zv::Val(); + zend_object *mergedObject = requireObject(merged, "addTemplateArgumentConstraints"); + if (UNEXPECTED(mergedObject == NULL)) return zv::Val(); + zv::Val constraints; + if (otherScope != NULL && Z_TYPE_P(otherScope) == IS_OBJECT) { + constraints = pt_type_call(Z_OBJ_P(otherScope), PT_LC("gettemplateargumentconstraints"), 0, NULL); + if (UNEXPECTED(constraints.isUndef())) return zv::Val(); + } else { + constraints = zv::Val::null(); + } + return pt_type_call(mergedObject, PT_LC("addtemplateargumentconstraints"), 1, constraints.raw()); + } + + /* private (twin 4393) */ + zv::Val mergeWithVariableState(zval *otherScopeZval, bool preserveVacuousConditionals) + { + if (otherScopeZval == NULL || Z_TYPE_P(otherScopeZval) != IS_OBJECT) return self_(); + zend_object *otherScope = Z_OBJ_P(otherScopeZval); + if (otherScope == self) return self_(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions") + || !requireSlot(PT_MS_PROP_AFTER_EXTRACT_CALL, "afterExtractCall") + || !requireSlot(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT, "inFirstLevelStatement"))) { + return zv::Val(); + } + HashTable *theirExpressionTypesTable = otherTable(otherScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + HashTable *theirNativeExpressionTypesTable = theirExpressionTypesTable == NULL ? NULL : otherTable(otherScope, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + HashTable *theirConditionalExpressionsTable = theirNativeExpressionTypesTable == NULL ? NULL : otherTable(otherScope, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + if (UNEXPECTED(theirConditionalExpressionsTable == NULL)) return zv::Val(); + bool theirAfterExtractCall; + if (UNEXPECTED(!otherBool(otherScope, PT_MS_PROP_AFTER_EXTRACT_CALL, PT_LC("afterExtractCall"), theirAfterExtractCall))) return zv::Val(); + /* every table is held for the whole merge: the ScopeOps bodies read + * them repeatedly and a foreign scope's slot may be rewritten */ + zv::Arr ourExpressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())); + zv::Arr theirExpressionTypes = zv::Arr::copyOfTable(theirExpressionTypesTable); + zv::Arr ourNativeExpressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw())); + zv::Arr theirNativeExpressionTypes = zv::Arr::copyOfTable(theirNativeExpressionTypesTable); + zv::Arr ourConditionalExpressions = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw())); + zv::Arr theirConditionalExpressions = zv::Arr::copyOfTable(theirConditionalExpressionsTable); + + zv::Arr differingExpressionKeys = zv::Arr::create(8); + zv::Val mergedExpressionTypes = pt_scope_ops_merge_variable_holders(ourExpressionTypes.table(), theirExpressionTypes.table(), differingExpressionKeys.table()); + if (UNEXPECTED(mergedExpressionTypes.isUndef())) return zv::Val(); + zv::Val differing = withoutPreciseClassConstantFetches(differingExpressionKeys.table(), ourExpressionTypes.table(), theirExpressionTypes.table()); + if (UNEXPECTED(differing.isUndef())) return zv::Val(); + zv::Val conditionalExpressions = pt_scope_ops_intersect_conditional_expressions(ourConditionalExpressions.table(), theirConditionalExpressions.table()); + if (UNEXPECTED(conditionalExpressions.isUndef())) return zv::Val(); + if (preserveVacuousConditionals) { + zv::Val preserved = preserveVacuousConditionalExpressions(Z_ARRVAL_P(conditionalExpressions.raw()), ourConditionalExpressions.table(), theirExpressionTypes.table()); + if (UNEXPECTED(preserved.isUndef())) return zv::Val(); + conditionalExpressions = std::move(preserved); + zv::Val preservedTheirs = preserveVacuousConditionalExpressions(Z_ARRVAL_P(conditionalExpressions.raw()), theirConditionalExpressions.table(), ourExpressionTypes.table()); + if (UNEXPECTED(preservedTheirs.isUndef())) return zv::Val(); + conditionalExpressions = std::move(preservedTheirs); + } + zv::Val sameGuard = mergeSameGuardConditionalExpressions(Z_ARRVAL_P(conditionalExpressions.raw()), ourConditionalExpressions.table(), theirConditionalExpressions.table()); + if (UNEXPECTED(sameGuard.isUndef())) return zv::Val(); + conditionalExpressions = std::move(sameGuard); + zv::Val created = pt_scope_ops_create_conditional_expressions(Z_ARRVAL_P(conditionalExpressions.raw()), ourExpressionTypes.table(), theirExpressionTypes.table(), Z_ARRVAL_P(mergedExpressionTypes.raw()), Z_ARRVAL_P(differing.raw())); + if (UNEXPECTED(created.isUndef())) return zv::Val(); + conditionalExpressions = std::move(created); + zv::Val createdReversed = pt_scope_ops_create_conditional_expressions(Z_ARRVAL_P(conditionalExpressions.raw()), theirExpressionTypes.table(), ourExpressionTypes.table(), Z_ARRVAL_P(mergedExpressionTypes.raw()), Z_ARRVAL_P(differing.raw())); + if (UNEXPECTED(createdReversed.isUndef())) return zv::Val(); + conditionalExpressions = std::move(createdReversed); + + zv::Val finished = pt_scope_ops_finish_merge(Z_ARRVAL_P(mergedExpressionTypes.raw()), ourExpressionTypes.table(), theirExpressionTypes.table(), ourNativeExpressionTypes.table(), theirNativeExpressionTypes.table()); + if (UNEXPECTED(finished.isUndef())) return zv::Val(); + zval *mergedTypes = zend_hash_index_find(Z_ARRVAL_P(finished.raw()), 0); + zval *mergedNativeTypes = zend_hash_index_find(Z_ARRVAL_P(finished.raw()), 1); + if (UNEXPECTED(mergedTypes == NULL || mergedNativeTypes == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: ScopeOps::finishMerge() answered an unexpected shape"); + return zv::Val(); + } + + return scopeWithTables( + Z_ARRVAL_P(mergedTypes), + Z_ARRVAL_P(mergedNativeTypes), + Z_ARRVAL_P(conditionalExpressions.raw()), + (HashTable *) &zend_empty_array, + (HashTable *) &zend_empty_array, + (HashTable *) &zend_empty_array, + slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT), + slotBool(PT_MS_PROP_AFTER_EXTRACT_CALL) && theirAfterExtractCall); + } + + /* private (twin 4476) — drops the keys of class-constant fetches that + * resolve to their declared value */ + zv::Val withoutPreciseClassConstantFetches(HashTable *differingExpressionKeys, HashTable *ourExpressionTypes, HashTable *theirExpressionTypes) + { + zv::Arr result = zv::Arr::copyOfTable(differingExpressionKeys); + zv::Arr keys = zv::Arr::create(zend_hash_num_elements(differingExpressionKeys)); + for (auto entry : zv::TableRef(differingExpressionKeys)) { + zval key; + if (entry.stringKeyOrNull() != NULL) { + ZVAL_STR_COPY(&key, entry.stringKeyOrNull()); + } else { + ZVAL_LONG(&key, (zend_long) entry.indexKey()); + } + keys.push(zv::Val::adopt(key)); + } + for (auto keyEntry : zv::TableRef(keys.table())) { + zv::Ref keyValue = keyEntry.value().deref(); + zend_string *skey = keyValue.isString() ? keyValue.asString() : NULL; + zend_ulong index = keyValue.isString() ? 0 : (zend_ulong) Z_LVAL_P(keyValue.raw()); + zval *holder = pt_ht_find(ourExpressionTypes, skey, index); + if (holder == NULL) { + holder = pt_ht_find(theirExpressionTypes, skey, index); + } + if (holder == NULL) continue; + zv::Val expr = holderExpr(zv::Ref(holder)); + if (UNEXPECTED(expr.isUndef())) return zv::Val(); + bool is; + if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_CLASS_CONST_FETCH, is))) return zv::Val(); + if (!is) continue; + zv::Ref classNode = nodeProp(Z_OBJ_P(expr.raw()), PT_LC("class")); + zv::Ref nameNode = nodeProp(Z_OBJ_P(expr.raw()), PT_LC("name")); + if (UNEXPECTED(classNode.raw() == NULL || nameNode.raw() == NULL)) return zv::Val(); + if (UNEXPECTED(!isInstance(classNode.deref(), PT_CLASS_NAME, is))) return zv::Val(); + if (!is) continue; + if (UNEXPECTED(!isInstance(nameNode.deref(), PT_CLASS_IDENTIFIER, is))) return zv::Val(); + if (!is) continue; + /* static::CONST is late-bound */ + zv::Val lowerClassName = pt_type_call(classNode.deref().asObject(), PT_LC("tolowerstring"), 0, NULL); + if (UNEXPECTED(lowerClassName.isUndef())) return zv::Val(); + if (Z_TYPE_P(lowerClassName.raw()) == IS_STRING && zend_string_equals_literal(Z_STR_P(lowerClassName.raw()), "static")) continue; + zv::Val className = thisResolveName(classNode.deref().raw()); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Val constantName = pt_type_call(nameNode.deref().asObject(), PT_LC("tostring"), 0, NULL); + if (UNEXPECTED(constantName.isUndef())) return zv::Val(); + zv::Ref constantResolver = slot(PT_MS_PROP_CONSTANT_RESOLVER); + if (UNEXPECTED(!constantResolver.isObject())) { + (void) uninitializedProperty("constantResolver"); + return zv::Val(); + } + zv::Args args{className.raw(), constantName.raw()}; + zv::Val dynamic = pt_type_call(constantResolver.asObject(), PT_LC("isdynamicclassconstant"), 2, args); + if (UNEXPECTED(dynamic.isUndef())) return zv::Val(); + if (zend_is_true(dynamic.raw())) continue; + result.separate(); + pt_ht_del(result.table(), skey, index); + } + + return zv::Val(std::move(result)); + } + + /* private (twin 4527) — rescues one-sided conditional holders across an + * if-merge */ + static zv::Val preserveVacuousConditionalExpressions(HashTable *currentConditionalExpressions, HashTable *sourceConditionalExpressions, HashTable *otherExpressionTypes) + { + zv::Arr result = zv::Arr::copyOfTable(currentConditionalExpressions); + for (auto sourceEntry : zv::TableRef(sourceConditionalExpressions)) { + zend_string *exprKey = sourceEntry.stringKeyOrNull(); + zend_ulong exprIndex = sourceEntry.indexKey(); + zv::Ref holders = sourceEntry.value().deref(); + if (UNEXPECTED(!holders.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return zv::Val(); + } + for (auto holderEntry : zv::TableRef(holders.asArrayTable())) { + zend_string *key = holderEntry.stringKeyOrNull(); + zend_ulong keyIndex = holderEntry.indexKey(); + if (conditionalHolderIsSet(result, exprKey, exprIndex, key, keyIndex)) continue; + zv::Ref holder = holderEntry.value().deref(); + zv::Val typeHolder = conditionalTypeHolder(holder); + if (UNEXPECTED(typeHolder.isUndef())) return zv::Val(); + zend_long certainty = holderCertainty(typeHolder.ref()); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_NO) { + zv::Val typeHolderExpr = holderExpr(typeHolder.ref()); + if (UNEXPECTED(typeHolderExpr.isUndef())) return zv::Val(); + bool isVariable; + if (UNEXPECTED(!isInstance(typeHolderExpr.ref(), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (!isVariable) continue; + } + + zv::Val conditions = conditionalConditions(holder); + if (UNEXPECTED(conditions.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(conditions.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a ConditionalExpressionHolder has no conditions"); + return zv::Val(); + } + bool vacuous = false; + for (auto guardEntry : zv::ArrRef(conditions.raw())) { + zval *otherHolder = pt_ht_find(otherExpressionTypes, guardEntry.stringKeyOrNull(), guardEntry.indexKey()); + if (otherHolder == NULL) continue; + zv::Val otherType = holderType(zv::Ref(otherHolder)); + zv::Val guardType = holderType(guardEntry.value()); + if (UNEXPECTED(otherType.isUndef() || guardType.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(otherType.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isSuperTypeOf() on %s", zend_zval_value_name(otherType.raw())); + return zv::Val(); + } + bool no; + if (UNEXPECTED(!isSuperTypeOfNo(Z_OBJ_P(otherType.raw()), guardType.raw(), no))) return zv::Val(); + if (no) { + vacuous = true; + break; + } + } + if (vacuous) { + if (UNEXPECTED(!appendConditionalHolder(result, exprKey, exprIndex, key, keyIndex, holder))) return zv::Val(); + continue; + } + + if (certainty == PT_TRI_NO) continue; + zval *otherTargetHolder = pt_ht_find(otherExpressionTypes, exprKey, exprIndex); + if (otherTargetHolder == NULL) continue; + zend_long otherTargetCertainty = holderCertainty(zv::Ref(otherTargetHolder)); + if (UNEXPECTED(otherTargetCertainty < 0)) return zv::Val(); + if (otherTargetCertainty != PT_TRI_YES && otherTargetCertainty != certainty) continue; + zv::Val typeHolderType = holderType(typeHolder.ref()); + zv::Val otherTargetType = holderType(zv::Ref(otherTargetHolder)); + if (UNEXPECTED(typeHolderType.isUndef() || otherTargetType.isUndef())) return zv::Val(); + /* an ErrorType consequent or other-branch state would look + * "already satisfied" and hide the underlying error */ + bool isError; + if (UNEXPECTED(!pt_type_instanceof_ce(typeHolderType.raw(), pt_ce_error_type, isError))) return zv::Val(); + if (!isError) { + if (UNEXPECTED(!pt_type_instanceof_ce(otherTargetType.raw(), pt_ce_error_type, isError))) return zv::Val(); + } + if (isError) continue; + if (UNEXPECTED(Z_TYPE_P(typeHolderType.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isSuperTypeOf() on %s", zend_zval_value_name(typeHolderType.raw())); + return zv::Val(); + } + bool yes; + if (UNEXPECTED(!isSuperTypeOfYes(Z_OBJ_P(typeHolderType.raw()), otherTargetType.raw(), yes))) return zv::Val(); + if (!yes) continue; + if (UNEXPECTED(!appendConditionalHolder(result, exprKey, exprIndex, key, keyIndex, holder))) return zv::Val(); + } + } + + return zv::Val(std::move(result)); + } + + /* private (twin 4599) — merges one-sided holders that share a target and + * an identical guard set */ + static zv::Val mergeSameGuardConditionalExpressions(HashTable *currentConditionalExpressions, HashTable *ourConditionalExpressions, HashTable *theirConditionalExpressions) + { + zv::Arr result = zv::Arr::copyOfTable(currentConditionalExpressions); + for (auto ourEntry : zv::TableRef(ourConditionalExpressions)) { + zend_string *exprKey = ourEntry.stringKeyOrNull(); + zend_ulong exprIndex = ourEntry.indexKey(); + zval *theirSlot = pt_ht_find(theirConditionalExpressions, exprKey, exprIndex); + if (theirSlot == NULL) continue; + ZVAL_DEREF(theirSlot); + zv::Ref ourHolders = ourEntry.value().deref(); + if (UNEXPECTED(!ourHolders.isArray() || Z_TYPE_P(theirSlot) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return zv::Val(); + } + zv::Arr theirHolders = zv::Arr::copyOfTable(Z_ARRVAL_P(theirSlot)); + for (auto ourHolderEntry : zv::TableRef(ourHolders.asArrayTable())) { + if (conditionalHolderIsSet(result, exprKey, exprIndex, ourHolderEntry.stringKeyOrNull(), ourHolderEntry.indexKey())) continue; + zv::Ref ourHolder = ourHolderEntry.value().deref(); + zv::Val ourTypeHolder = conditionalTypeHolder(ourHolder); + if (UNEXPECTED(ourTypeHolder.isUndef())) return zv::Val(); + zend_long ourCertainty = holderCertainty(ourTypeHolder.ref()); + if (UNEXPECTED(ourCertainty < 0)) return zv::Val(); + if (ourCertainty == PT_TRI_NO) continue; + zv::Val ourGuards = conditionalConditions(ourHolder); + if (UNEXPECTED(ourGuards.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(ourGuards.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a ConditionalExpressionHolder has no conditions"); + return zv::Val(); + } + for (auto theirHolderEntry : zv::TableRef(theirHolders.table())) { + if (conditionalHolderIsSet(result, exprKey, exprIndex, theirHolderEntry.stringKeyOrNull(), theirHolderEntry.indexKey())) continue; + zv::Ref theirHolder = theirHolderEntry.value().deref(); + zv::Val theirTypeHolder = conditionalTypeHolder(theirHolder); + if (UNEXPECTED(theirTypeHolder.isUndef())) return zv::Val(); + zend_long theirCertainty = holderCertainty(theirTypeHolder.ref()); + if (UNEXPECTED(theirCertainty < 0)) return zv::Val(); + if (theirCertainty != ourCertainty) continue; + zv::Val theirGuards = conditionalConditions(theirHolder); + if (UNEXPECTED(theirGuards.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(theirGuards.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a ConditionalExpressionHolder has no conditions"); + return zv::Val(); + } + if (zend_hash_num_elements(Z_ARRVAL_P(ourGuards.raw())) != zend_hash_num_elements(Z_ARRVAL_P(theirGuards.raw()))) continue; + bool sameGuards = true; + for (auto guardEntry : zv::ArrRef(ourGuards.raw())) { + zval *theirGuard = pt_ht_find(Z_ARRVAL_P(theirGuards.raw()), guardEntry.stringKeyOrNull(), guardEntry.indexKey()); + if (theirGuard == NULL) { + sameGuards = false; + break; + } + bool equal; + if (UNEXPECTED(!holderEquals(guardEntry.value(), zv::Ref(theirGuard), equal))) return zv::Val(); + if (!equal) { + sameGuards = false; + break; + } + } + if (!sameGuards) continue; + zv::Val ourType = holderType(ourTypeHolder.ref()); + zv::Val theirType = holderType(theirTypeHolder.ref()); + zv::Val ourExpr = holderExpr(ourTypeHolder.ref()); + if (UNEXPECTED(ourType.isUndef() || theirType.isUndef() || ourExpr.isUndef())) return zv::Val(); + zv::Val united = unionTypes(ourType.raw(), theirType.raw()); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + zval typeHolder; + pt_holder_create(&typeHolder, ourExpr.raw(), united.raw(), ourCertainty); + zv::Val typeHolderValue = zv::Val::adopt(typeHolder); + zv::Val unionHolder = newConditionalExpressionHolder(ourGuards.ref(), typeHolderValue.ref()); + zend_string *unionKey = conditionalHolderKey(unionHolder.ref()); + if (UNEXPECTED(unionKey == NULL)) return zv::Val(); + zv::Str unionKeyString = zv::Str::adopt(unionKey); + if (UNEXPECTED(!appendConditionalHolder(result, exprKey, exprIndex, unionKeyString.get(), 0, unionHolder.ref()))) return zv::Val(); + } + } + } + + return zv::Val(std::move(result)); + } + + /* private (twin 4667) */ + static zv::Val mergeConditionalExpressions(HashTable *newConditionalExpressions, HashTable *existingConditionalExpressions) + { + zv::Arr result = zv::Arr::copyOfTable(existingConditionalExpressions); + for (auto entry : zv::TableRef(newConditionalExpressions)) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong index = entry.indexKey(); + zv::Ref holders = entry.value().deref(); + result.separate(); + zval *existing = pt_ht_find(result.table(), key, index); + if (existing == NULL) { + zval copy; + ZVAL_COPY(©, holders.raw()); + pt_ht_add_new(result.table(), key, index, ©); + continue; + } + ZVAL_DEREF(existing); + if (UNEXPECTED(Z_TYPE_P(existing) != IS_ARRAY || !holders.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return zv::Val(); + } + zv::Val merged = arrayMerge(Z_ARRVAL_P(existing), holders.asArrayTable()); + zval mergedZv = merged.take(); + pt_ht_update(result.table(), key, index, &mergedZv); + } + + return zv::Val(std::move(result)); + } + + /* (twin 4681) */ + zv::Val mergeInitializedProperties(zend_object *calledMethodScope) + { + static const char prefix[] = "__phpstanPropertyInitialization("; + zv::Val scope = self_(); + zend_object *scopeObject = self; + HashTable *calledTable = otherTable(calledMethodScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(calledTable == NULL)) return zv::Val(); + zv::Arr calledExpressionTypes = zv::Arr::copyOfTable(calledTable); + for (auto entry : zv::TableRef(calledExpressionTypes.table())) { + zend_string *skey = entry.stringKeyOrNull(); + zv::Str exprString = zv::Str::adopt(skey != NULL ? zend_string_copy(skey) : zend_long_to_str((zend_long) entry.indexKey())); + if (ZSTR_LEN(exprString.get()) < sizeof(prefix) - 1 + || memcmp(ZSTR_VAL(exprString.get()), prefix, sizeof(prefix) - 1) != 0) { + continue; + } + size_t start = sizeof(prefix) - 1; + size_t length = ZSTR_LEN(exprString.get()) > start ? ZSTR_LEN(exprString.get()) - start - 1 : 0; + zv::Str propertyName = zv::Str::adopt(zend_string_init(ZSTR_VAL(exprString.get()) + start, length, 0)); + zval propertyNameZv; + ZVAL_STR(&propertyNameZv, propertyName.get()); + zv::Val propertyExpr = pt_type_new(PT_CLASS_PROPERTY_INITIALIZATION_EXPR, 1, &propertyNameZv); + if (UNEXPECTED(propertyExpr.isUndef())) return zv::Val(); + HashTable *scopeTable = otherTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(scopeTable == NULL)) return zv::Val(); + zval *existing = zend_symtable_find(scopeTable, exprString.get()); + zend_long certainty = 0; + if (existing != NULL) { + certainty = holderCertainty(zv::Ref(existing)); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + } + zval mixedZv, nativeMixedZv; + if (UNEXPECTED(!pt_mixed_type_new(&mixedZv) || !pt_mixed_type_new(&nativeMixedZv))) return zv::Val(); + zv::Val mixed = zv::Val::adopt(mixedZv); + zv::Val nativeMixed = zv::Val::adopt(nativeMixedZv); + zv::Val assigned = otherAssignExpression(scope, propertyExpr.raw(), mixed.raw(), nativeMixed.raw()); + if (UNEXPECTED(assigned.isUndef())) return zv::Val(); + zend_object *assignedObject = requireObject(assigned, "mergeInitializedProperties"); + if (UNEXPECTED(assignedObject == NULL)) return zv::Val(); + scope = std::move(assigned); + scopeObject = assignedObject; + if (existing == NULL) { + zval holder; + ZVAL_COPY_VALUE(&holder, entry.value().deref().raw()); + if (UNEXPECTED(!writeScopeTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), exprString.get(), &holder))) { + return zv::Val(); + } + continue; + } + zv::Val theirExpr = holderExpr(entry.value()); + zv::Val theirType = holderType(entry.value()); + if (UNEXPECTED(theirExpr.isUndef() || theirType.isUndef())) return zv::Val(); + zend_long theirCertainty = holderCertainty(entry.value()); + if (UNEXPECTED(theirCertainty < 0)) return zv::Val(); + zval mergedHolder; + pt_holder_create(&mergedHolder, theirExpr.raw(), theirType.raw(), theirCertainty | certainty); + zv::Val mergedHolderValue = zv::Val::adopt(mergedHolder); + if (UNEXPECTED(!writeScopeTable(scopeObject, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes"), exprString.get(), mergedHolderValue.raw()))) { + return zv::Val(); + } + } + + return scope; + } + + /* (twin 4709) */ + zv::Val processFinallyScope(zend_object *finallyScope, zend_object *originalFinallyScope) + { + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + HashTable *finallyTypes = otherTable(finallyScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + HashTable *originalTypes = finallyTypes == NULL ? NULL : otherTable(originalFinallyScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + HashTable *finallyNativeTypes = originalTypes == NULL ? NULL : otherTable(finallyScope, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + HashTable *originalNativeTypes = finallyNativeTypes == NULL ? NULL : otherTable(originalFinallyScope, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + HashTable *finallyConditional = originalNativeTypes == NULL ? NULL : otherTable(finallyScope, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + if (UNEXPECTED(finallyConditional == NULL)) return zv::Val(); + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) { + return zv::Val(); + } + PT_MS_ARG_CREATE(a, CreateArgs::EXPRESSION_TYPES, processFinallyScopeVariableTypeHolders(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), finallyTypes, originalTypes)); + PT_MS_ARG_CREATE(a, CreateArgs::NATIVE_EXPRESSION_TYPES, processFinallyScopeVariableTypeHolders(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), finallyNativeTypes, originalNativeTypes)); + PT_MS_ARG_CREATE(a, CreateArgs::CONDITIONAL_EXPRESSIONS, pt_scope_ops_intersect_conditional_expressions(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), finallyConditional)); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + return scopeFactoryCreate(a); + } + + /* private (twin 4747) */ + static zv::Val processFinallyScopeVariableTypeHolders(HashTable *ourVariableTypeHolders, HashTable *finallyVariableTypeHolders, HashTable *originalVariableTypeHolders) + { + zv::Arr result = zv::Arr::copyOfTable(ourVariableTypeHolders); + for (auto entry : zv::TableRef(finallyVariableTypeHolders)) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong index = entry.indexKey(); + zval *original = pt_ht_find(originalVariableTypeHolders, key, index); + if (original != NULL && Z_TYPE_P(original) != IS_NULL) { + bool equal; + if (UNEXPECTED(!holderEqualTypes(zv::Ref(original), entry.value(), equal))) return zv::Val(); + if (equal) continue; + } + result.separate(); + zval copy; + ZVAL_COPY(©, entry.value().deref().raw()); + pt_ht_update(result.table(), key, index, ©); + } + + return zv::Val(std::move(result)); + } + + /* }}} */ + + /* {{{ twin 4775-5884: the closure and loop scopes, the + * generalization, the scope comparison, the member-access queries and + * the remaining readers */ + + /* $a->equals($b); false = pending exception */ + [[nodiscard]] static bool typeEquals(zval *a, zval *b, bool &out) + { + if (UNEXPECTED(Z_TYPE_P(a) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function equals() on %s", zend_zval_value_name(a)); + return false; + } + return pt_type_op_bool(Z_OBJ_P(a), PT_OP_EQUALS, 1, b, out); + } + + /* TypeCombinator::union(...$first, ...$second) over two list tables + * ($second may be NULL for the one-list spelling) */ + static zv::Val unionOfLists(HashTable *first, HashTable *second) + { + std::vector args; + args.reserve(zend_hash_num_elements(first) + (second != NULL ? zend_hash_num_elements(second) : 0)); + for (auto entry : zv::TableRef(first)) { + args.push_back(*entry.value().deref().raw()); + } + if (second != NULL) { + for (auto entry : zv::TableRef(second)) { + args.push_back(*entry.value().deref().raw()); + } + } + return pt_type_combinator_union((uint32_t) args.size(), args.empty() ? NULL : args.data()); + } + + static zv::Val unionOfList(HashTable *list) { return unionOfLists(list, NULL); } + + /* $type->generalize(GeneralizePrecision::moreSpecific()) */ + static zv::Val generalizeMoreSpecific(zval *type) + { + if (UNEXPECTED(Z_TYPE_P(type) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function generalize() on %s", zend_zval_value_name(type)); + return zv::Val(); + } + zv::Val precision = pt_type_call_static(PT_CLASS_GENERALIZE_PRECISION, PT_LC("morespecific"), 0, NULL); + if (UNEXPECTED(precision.isUndef())) return zv::Val(); + return pt_type_call(Z_OBJ_P(type), PT_LC("generalize"), 1, precision.raw()); + } + + /* the PT_TRI_* value of a TrinaryLogic-returning op; -1 = pending exception */ + [[nodiscard]] static zend_long typeOpTrinary(zend_object *type, pt_type_op_id op, uint32_t argc, zval *argv) + { + zv::Val result = pt_type_op(type, op, argc, argv); + if (UNEXPECTED(result.isUndef())) return -1; + return pt_type_trinary_value(result.raw()); + } + + /* an array key as the string PHP's `foreach ($a as $k => ...)` would + * hand a `string $k` parameter */ + static zv::Str entryKey(zend_string *key, zend_ulong index) + { + return zv::Str::adopt(key != NULL ? zend_string_copy(key) : zend_long_to_str((zend_long) index)); + } + + /* (twin 4775) */ + zv::Val processClosureScope(zend_object *closureScope, zval *prevScope, HashTable *byRefUses) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) { + return zv::Val(); + } + zv::Arr nativeExpressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw())); + zv::Arr expressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())); + if (zend_hash_num_elements(byRefUses) == 0) return self_(); + + for (auto entry : zv::TableRef(byRefUses)) { + zv::Ref use = entry.value().deref(); + if (UNEXPECTED(!use.isObject())) { + zend_throw_error(NULL, "phpstan_turbo: a by-ref use is not a node"); + return zv::Val(); + } + zv::Ref var = nodeProp(use.asObject(), PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + var = var.deref(); + if (UNEXPECTED(!var.isObject())) { + zend_throw_error(NULL, "phpstan_turbo: a by-ref use has no variable node"); + return zv::Val(); + } + zv::Ref name = nodeProp(var.asObject(), PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return zv::Val(); + name = name.deref(); + if (!name.isString()) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zend_string *variableName = name.asString(); + zv::Str variableExprString = zv::Str::adopt(dollarName(variableName)); + + zval variableNameZv; + ZVAL_STR(&variableNameZv, variableName); + zend_long hasVariableType = pt_type_call_trinary(closureScope, PT_LC("hasvariabletype"), 1, &variableNameZv); + if (UNEXPECTED(hasVariableType < 0)) return zv::Val(); + if (hasVariableType != PT_TRI_YES) { + zval nullTypeZv; + if (UNEXPECTED(!pt_null_type_new(&nullTypeZv))) return zv::Val(); + zv::Val nullType = zv::Val::adopt(nullTypeZv); + zval holderZv; + pt_holder_create(&holderZv, var.raw(), nullType.raw(), PT_TRI_YES); + zv::Val holder = zv::Val::adopt(holderZv); + expressionTypes.set(variableExprString.get(), zv::Val::copyOf(holder.ref())); + nativeExpressionTypes.set(variableExprString.get(), std::move(holder)); + continue; + } + + zv::Val variableType = pt_type_call(closureScope, PT_LC("getvariabletype"), 1, &variableNameZv); + if (UNEXPECTED(variableType.isUndef())) return zv::Val(); + if (prevScope != NULL) { + zv::Val prevVariableType = pt_type_call(Z_OBJ_P(prevScope), PT_LC("getvariabletype"), 1, &variableNameZv); + if (UNEXPECTED(prevVariableType.isUndef())) return zv::Val(); + bool equal; + if (UNEXPECTED(!typeEquals(variableType.raw(), prevVariableType.raw(), equal))) return zv::Val(); + if (!equal) { + zv::Val united = unionTypes(variableType.raw(), prevVariableType.raw()); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + variableType = generalizeType(united.raw(), prevVariableType.raw(), 0); + if (UNEXPECTED(variableType.isUndef())) return zv::Val(); + } + } + + zval holderZv; + pt_holder_create(&holderZv, var.raw(), variableType.raw(), PT_TRI_YES); + zv::Val holder = zv::Val::adopt(holderZv); + expressionTypes.set(variableExprString.get(), zv::Val::copyOf(holder.ref())); + nativeExpressionTypes.set(variableExprString.get(), std::move(holder)); + } + + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + a.setOwned(CreateArgs::EXPRESSION_TYPES, zv::Val(std::move(expressionTypes))); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Val(std::move(nativeExpressionTypes))); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + return scopeFactoryCreate(a); + } + + /* (twin 4839) */ + zv::Val processAlwaysIterableForeachScopeWithoutPollute(zend_object *finalScope) + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + zv::Arr expressionTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw())); + HashTable *finalExpressionTypes = otherTable(finalScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(finalExpressionTypes == NULL)) return zv::Val(); + if (UNEXPECTED(!mergeForeachHolders(expressionTypes, finalExpressionTypes))) return zv::Val(); + + if (UNEXPECTED(!requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes"))) return zv::Val(); + zv::Arr nativeTypes = zv::Arr::copyOfTable(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw())); + HashTable *finalNativeTypes = otherTable(finalScope, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + if (UNEXPECTED(finalNativeTypes == NULL)) return zv::Val(); + if (UNEXPECTED(!mergeForeachHolders(nativeTypes, finalNativeTypes))) return zv::Val(); + + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + a.setOwned(CreateArgs::EXPRESSION_TYPES, zv::Val(std::move(expressionTypes))); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, zv::Val(std::move(nativeTypes))); + HashTable *finalConditional = otherTable(finalScope, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + if (UNEXPECTED(finalConditional == NULL || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) return zv::Val(); + PT_MS_ARG_CREATE(a, CreateArgs::CONDITIONAL_EXPRESSIONS, pt_scope_ops_intersect_conditional_expressions(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), finalConditional)); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + return scopeFactoryCreate(a); + } + + /* one of the two identical loops of + * processAlwaysIterableForeachScopeWithoutPollute(); false = pending + * exception */ + [[nodiscard]] static bool mergeForeachHolders(zv::Arr &ours, HashTable *theirs) + { + for (auto entry : zv::TableRef(theirs)) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong index = entry.indexKey(); + zv::Val expr = holderExpr(entry.value()); + zv::Val type = holderType(entry.value()); + if (UNEXPECTED(expr.isUndef() || type.isUndef())) return false; + zval *existing = pt_ht_find(ours.table(), key, index); + zend_long certainty; + if (existing == NULL || Z_TYPE_P(existing) == IS_NULL) { + certainty = PT_TRI_MAYBE; + } else { + zend_long theirCertainty = holderCertainty(entry.value()); + zend_long ourCertainty = holderCertainty(zv::Ref(existing)); + if (UNEXPECTED(theirCertainty < 0 || ourCertainty < 0)) return false; + /* TrinaryLogic::and(): YES = 3, MAYBE = 1, NO = 0 */ + certainty = theirCertainty & ourCertainty; + } + zval holderZv; + pt_holder_create(&holderZv, expr.raw(), type.raw(), certainty); + ours.separate(); + pt_ht_update(ours.table(), key, index, &holderZv); + } + + return true; + } + + /* (twin 4893) */ + zv::Val generalizeWith(zend_object *otherScope, HashTable *writableVariableNames) + { + zv::Val generalized = generalizeWithVariableState(otherScope, writableVariableNames); + if (UNEXPECTED(generalized.isUndef())) return zv::Val(); + zend_object *generalizedObject = requireObject(generalized, "addTemplateArgumentConstraints"); + if (UNEXPECTED(generalizedObject == NULL)) return zv::Val(); + zv::Val constraints = pt_type_call(otherScope, PT_LC("gettemplateargumentconstraints"), 0, NULL); + if (UNEXPECTED(constraints.isUndef())) return zv::Val(); + return pt_type_call(generalizedObject, PT_LC("addtemplateargumentconstraints"), 1, constraints.raw()); + } + + /* private (twin 4901) */ + zv::Val generalizeWithVariableState(zend_object *otherScope, HashTable *writableVariableNames) + { + HashTable *otherExpressionTypes = otherTable(otherScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(otherExpressionTypes == NULL || !requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + HashTable *ourExpressionTypes = Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()); + + zv::Arr writable; + if (writableVariableNames != NULL) { + /* a reference created before the loop lets the loop write a + * variable it does not name */ + writable = zv::Arr::copyOfTable(writableVariableNames); + HashTable *tables[2] = { ourExpressionTypes, otherExpressionTypes }; + for (uint32_t i = 0; i < 2; i++) { + for (auto entry : zv::TableRef(tables[i])) { + zv::Val intertwinedExpr = holderExpr(entry.value()); + if (UNEXPECTED(intertwinedExpr.isUndef())) return zv::Val(); + bool isIntertwined; + if (UNEXPECTED(!isInstance(intertwinedExpr.ref(), PT_CLASS_INTERTWINED_VAR, isIntertwined))) return zv::Val(); + if (!isIntertwined) continue; + zend_object *intertwined = Z_OBJ_P(intertwinedExpr.raw()); + zv::Val variableName = pt_type_call(intertwined, PT_LC("getvariablename"), 0, NULL); + if (UNEXPECTED(variableName.isUndef())) return zv::Val(); + zend_string *variableNameStr = zval_get_string(variableName.raw()); + writable.set(variableNameStr, zv::Val::boolean(true)); + zend_string_release(variableNameStr); + zv::Val aliasedExprs[2]; + aliasedExprs[0] = pt_type_call(intertwined, PT_LC("getexpr"), 0, NULL); + if (UNEXPECTED(aliasedExprs[0].isUndef())) return zv::Val(); + aliasedExprs[1] = pt_type_call(intertwined, PT_LC("getassignedexpr"), 0, NULL); + if (UNEXPECTED(aliasedExprs[1].isUndef())) return zv::Val(); + for (uint32_t j = 0; j < 2; j++) { + if (UNEXPECTED(Z_TYPE_P(aliasedExprs[j].raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: an intertwined expression is not a node"); + return zv::Val(); + } + zv::Val aliasedVariableName = pt_scope_ops_intertwined_ref_root_variable_name(Z_OBJ_P(aliasedExprs[j].raw())); + if (UNEXPECTED(aliasedVariableName.isUndef())) return zv::Val(); + if (aliasedVariableName.isNull()) continue; + zend_string *aliasedStr = zval_get_string(aliasedVariableName.raw()); + writable.set(aliasedStr, zv::Val::boolean(true)); + zend_string_release(aliasedStr); + } + } + } + } + + HashTable *writableTable = writableVariableNames == NULL ? NULL : writable.table(); + zv::Val variableTypeHolders = generalizeVariableTypeHolders(ourExpressionTypes, otherExpressionTypes, writableTable); + if (UNEXPECTED(variableTypeHolders.isUndef())) return zv::Val(); + HashTable *otherNativeTypes = otherTable(otherScope, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + if (UNEXPECTED(otherNativeTypes == NULL || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes"))) return zv::Val(); + zv::Val nativeTypes = generalizeVariableTypeHolders(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), otherNativeTypes, writableTable); + if (UNEXPECTED(nativeTypes.isUndef())) return zv::Val(); + + CreateArgs a; + if (UNEXPECTED(!fillFromSlots(a))) return zv::Val(); + if (UNEXPECTED(!fillDispatched(a, true, false))) return zv::Val(); + a.setOwned(CreateArgs::EXPRESSION_TYPES, std::move(variableTypeHolders)); + a.setOwned(CreateArgs::NATIVE_EXPRESSION_TYPES, std::move(nativeTypes)); + a.setBool(CreateArgs::IN_FIRST_LEVEL_STATEMENT, slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT)); + a.setEmptyArray(CreateArgs::CURRENTLY_ASSIGNED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS); + a.setEmptyArray(CreateArgs::IN_FUNCTION_CALLS_STACK); + return scopeFactoryCreate(a); + } + + /* private (twin 4961) */ + zv::Val generalizeVariableTypeHolders(HashTable *variableTypeHolders, HashTable *otherVariableTypeHolders, HashTable *writableVariableNames) + { + /* uksort(fn ($a, $b) => strlen($a) <=> strlen($b)) — PHP's sort is + * stable, so equal lengths keep their insertion order */ + struct SortedEntry + { + zend_string *key; + zend_ulong index; + zval *value; + size_t length; + }; + std::vector sorted; + sorted.reserve(zend_hash_num_elements(variableTypeHolders)); + for (auto entry : zv::TableRef(variableTypeHolders)) { + zend_string *key = entry.stringKeyOrNull(); + size_t length; + if (key != NULL) { + length = ZSTR_LEN(key); + } else { + zend_string *rendered = zend_long_to_str((zend_long) entry.indexKey()); + length = ZSTR_LEN(rendered); + zend_string_release(rendered); + } + sorted.push_back({ key, entry.indexKey(), entry.value().deref().raw(), length }); + } + std::stable_sort(sorted.begin(), sorted.end(), [](const SortedEntry &x, const SortedEntry &y) { + return x.length < y.length; + }); + + zv::Arr generalizedExpressions = zv::Arr::create(0); + zv::Arr newVariableTypeHolders = zv::Arr::create(0); + zv::Ref exprPrinter = slot(PT_MS_PROP_EXPR_PRINTER); + if (UNEXPECTED(!exprPrinter.isObject())) return uninitializedProperty("exprPrinter"); + for (const SortedEntry &entry : sorted) { + zv::Str variableExprString = entryKey(entry.key, entry.index); + zv::Val variableExpr = holderExpr(zv::Ref(entry.value)); + if (UNEXPECTED(variableExpr.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(variableExpr.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: an expression type holder has no expression"); + return zv::Val(); + } + bool invalidated = false; + for (auto generalized : zv::TableRef(generalizedExpressions.table())) { + zv::Str generalizedExprString = entryKey(generalized.stringKeyOrNull(), generalized.indexKey()); + bool failed = false; + bool should = pt_scope_ops_should_invalidate_expression( + thisZval(), + exprPrinter.raw(), + generalizedExprString.get(), + generalized.value().deref().raw(), + Z_OBJ_P(variableExpr.raw()), + variableExprString.get(), + false, + NULL, + false, + &failed); + if (UNEXPECTED(failed)) return zv::Val(); + if (!should) continue; + invalidated = true; + break; + } + if (invalidated) continue; + + zval *otherHolder = pt_ht_find(otherVariableTypeHolders, entry.key, entry.index); + if (otherHolder == NULL || Z_TYPE_P(otherHolder) == IS_NULL) { + newVariableTypeHolders.set(variableExprString.get(), zv::Val::copyOf(zv::Ref(entry.value))); + continue; + } + + bool byNarrowingOnly = false; + if (writableVariableNames != NULL) { + bool isVariable; + if (UNEXPECTED(!isInstance(variableExpr.ref(), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (isVariable) { + zv::Ref name = nodeProp(Z_OBJ_P(variableExpr.raw()), PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return zv::Val(); + name = name.deref(); + if (name.isString() && zend_symtable_find(writableVariableNames, name.asString()) == NULL) { + byNarrowingOnly = true; + } + } + } + + zv::Val ourType = holderType(zv::Ref(entry.value)); + zv::Val theirType = holderType(zv::Ref(otherHolder)); + if (UNEXPECTED(ourType.isUndef() || theirType.isUndef())) return zv::Val(); + /* the loop does not write this variable, its types differ between + * passes only by narrowing */ + zv::Val generalizedType = byNarrowingOnly + ? unionTypes(ourType.raw(), theirType.raw()) + : generalizeType(ourType.raw(), theirType.raw(), 0); + if (UNEXPECTED(generalizedType.isUndef())) return zv::Val(); + bool equal; + if (UNEXPECTED(!typeEquals(generalizedType.raw(), ourType.raw(), equal))) return zv::Val(); + if (!equal) { + generalizedExpressions.set(variableExprString.get(), zv::Val::copyOf(variableExpr.ref())); + } + zend_long certainty = holderCertainty(zv::Ref(entry.value)); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + zval holderZv; + pt_holder_create(&holderZv, variableExpr.raw(), generalizedType.raw(), certainty); + newVariableTypeHolders.set(variableExprString.get(), zv::Val::adopt(holderZv)); + } + + return zv::Val(std::move(newVariableTypeHolders)); + } + + /* the 'a' / 'b' pair of type lists generalizeType() sorts its inputs + * into (the twin's `['a' => [], 'b' => []]`) */ + struct GeneralizeBucket + { + zv::Arr list[2]; + + GeneralizeBucket() + { + list[0] = zv::Arr::create(0); + list[1] = zv::Arr::create(0); + } + + HashTable *table(int side) { return list[side].table(); } + uint32_t count(int side) { return zend_hash_num_elements(list[side].table()); } + }; + + /* $type->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($other->getArraySize())->yes(); + * false = pending exception */ + [[nodiscard]] bool arraySizeGreaterOrEqual(zval *type, zval *other, bool &out) + { + zv::Val size = pt_type_call(Z_OBJ_P(type), PT_LC("getarraysize"), 0, NULL); + if (UNEXPECTED(size.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(size.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getGreaterOrEqualType() on %s", zend_zval_value_name(size.raw())); + return false; + } + zv::Ref phpVersion = slot(PT_MS_PROP_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) { + (void) uninitializedProperty("phpVersion"); + return false; + } + zv::Val greaterOrEqual = pt_type_call(Z_OBJ_P(size.raw()), PT_LC("getgreaterorequaltype"), 1, phpVersion.raw()); + if (UNEXPECTED(greaterOrEqual.isUndef())) return false; + zv::Val otherSize = pt_type_call(Z_OBJ_P(other), PT_LC("getarraysize"), 0, NULL); + if (UNEXPECTED(otherSize.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(greaterOrEqual.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function isSuperTypeOf() on %s", zend_zval_value_name(greaterOrEqual.raw())); + return false; + } + return isSuperTypeOfYes(Z_OBJ_P(greaterOrEqual.raw()), otherSize.raw(), out); + } + + /* private flattenUnionForGeneralization(): a union's members, flattened + * recursively, any other type as the only element — the shapes stay + * whole (TypeUtils::flattenTypes() would expand optional keys into every + * variant only for generalizeType() to merge them back); false = pending + * exception */ + [[nodiscard]] static bool flattenUnionForGeneralization(zval *type, zv::Arr &out) + { + if (Z_TYPE_P(type) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(type), pt_ce_union_type)) { + out.push(zv::Ref(type)); + return true; + } + zv::Val innerTypes = pt_type_call(Z_OBJ_P(type), PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(innerTypes.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(innerTypes.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: UnionType::getTypes() did not answer with an array"); + return false; + } + for (auto entry : zv::TableRef(Z_ARRVAL_P(innerTypes.raw()))) { + if (UNEXPECTED(!flattenUnionForGeneralization(entry.value().deref().raw(), out))) return false; + } + return true; + } + + /* private (twin 5011) */ + zv::Val generalizeType(zval *a, zval *b, zend_long depth) + { + bool equal; + if (UNEXPECTED(!typeEquals(a, b, equal))) return zv::Val(); + if (equal) return zv::Val::copyOf(zv::Ref(a)); + + /* Track whether either input carries a BenevolentUnion so the result + * can be re-wrapped at the end (see the twin's comment) */ + bool wrapBenevolent = zv::Ref(a).instanceOf(pt_ce_benevolent_union_type) || zv::Ref(b).instanceOf(pt_ce_benevolent_union_type); + + GeneralizeBucket constantIntegers, constantFloats, constantBooleans, constantStrings, constantArrays, generalArrays, integerRanges; + zv::Arr otherTypes = zv::Arr::create(0); + + for (int side = 0; side < 2; side++) { + zv::Arr flattenedTypes = zv::Arr::create(4); + if (UNEXPECTED(!flattenUnionForGeneralization(side == 0 ? a : b, flattenedTypes))) return zv::Val(); + zv::Val flattened(std::move(flattenedTypes)); + for (auto entry : zv::TableRef(Z_ARRVAL_P(flattened.raw()))) { + zv::Ref type = entry.value().deref(); + if (UNEXPECTED(!type.isObject())) { + zend_throw_error(NULL, "phpstan_turbo: a flattened type is not a Type"); + return zv::Val(); + } + zend_class_entry *ce = type.asObject()->ce; + if (instanceof_function(ce, pt_ce_constant_integer_type)) { + constantIntegers.list[side].push(type); + continue; + } + if (instanceof_function(ce, pt_ce_constant_float_type)) { + constantFloats.list[side].push(type); + continue; + } + if (instanceof_function(ce, pt_ce_constant_boolean_type)) { + constantBooleans.list[side].push(type); + continue; + } + if (instanceof_function(ce, pt_ce_constant_string_type)) { + constantStrings.list[side].push(type); + continue; + } + zend_long isConstantArray = typeOpTrinary(type.asObject(), PT_OP_IS_CONSTANT_ARRAY, 0, NULL); + if (UNEXPECTED(isConstantArray < 0)) return zv::Val(); + if (isConstantArray == PT_TRI_YES) { + constantArrays.list[side].push(type); + continue; + } + zend_long isArray = typeOpTrinary(type.asObject(), PT_OP_IS_ARRAY, 0, NULL); + if (UNEXPECTED(isArray < 0)) return zv::Val(); + if (isArray == PT_TRI_YES) { + generalArrays.list[side].push(type); + continue; + } + if (instanceof_function(ce, pt_ce_integer_range_type)) { + integerRanges.list[side].push(type); + continue; + } + + otherTypes.push(type); + } + } + + zv::Arr resultTypes = zv::Arr::create(0); + GeneralizeBucket *scalarBuckets[3] = { &constantFloats, &constantBooleans, &constantStrings }; + for (uint32_t i = 0; i < 3; i++) { + GeneralizeBucket &bucket = *scalarBuckets[i]; + if (bucket.count(0) == 0) { + if (bucket.count(1) > 0) { + zv::Val united = unionOfList(bucket.table(1)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } + continue; + } else if (bucket.count(1) == 0) { + zv::Val united = unionOfList(bucket.table(0)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + continue; + } + + zv::Val aTypes = unionOfList(bucket.table(0)); + if (UNEXPECTED(aTypes.isUndef())) return zv::Val(); + zv::Val bTypes = unionOfList(bucket.table(1)); + if (UNEXPECTED(bTypes.isUndef())) return zv::Val(); + bool sameTypes; + if (UNEXPECTED(!typeEquals(aTypes.raw(), bTypes.raw(), sameTypes))) return zv::Val(); + if (sameTypes) { + resultTypes.push(std::move(aTypes)); + continue; + } + + zv::Val both = unionOfLists(bucket.table(0), bucket.table(1)); + if (UNEXPECTED(both.isUndef())) return zv::Val(); + zv::Val generalized = generalizeMoreSpecific(both.raw()); + if (UNEXPECTED(generalized.isUndef())) return zv::Val(); + resultTypes.push(std::move(generalized)); + } + + if (constantArrays.count(0) > 0) { + if (constantArrays.count(1) == 0) { + zv::Val united = unionOfList(constantArrays.table(0)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } else { + zv::Val constantArraysA = unionOfList(constantArrays.table(0)); + if (UNEXPECTED(constantArraysA.isUndef())) return zv::Val(); + zv::Val constantArraysB = unionOfList(constantArrays.table(1)); + if (UNEXPECTED(constantArraysB.isUndef())) return zv::Val(); + zv::Val keyTypeA = pt_type_op(Z_OBJ_P(constantArraysA.raw()), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); + if (UNEXPECTED(keyTypeA.isUndef())) return zv::Val(); + zv::Val keyTypeB = pt_type_op(Z_OBJ_P(constantArraysB.raw()), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); + if (UNEXPECTED(keyTypeB.isUndef())) return zv::Val(); + bool sameKeys; + if (UNEXPECTED(!typeEquals(keyTypeA.raw(), keyTypeB.raw(), sameKeys))) return zv::Val(); + bool sizeGreaterOrEqual = false; + if (sameKeys && UNEXPECTED(!arraySizeGreaterOrEqual(constantArraysA.raw(), constantArraysB.raw(), sizeGreaterOrEqual))) return zv::Val(); + if (sameKeys && sizeGreaterOrEqual) { + zv::Val builder = pt_constant_array_type_builder_create_empty(); + if (UNEXPECTED(builder.isUndef())) return zv::Val(); + zv::Val flattenedKeys = pt_type_utils_flatten_types(keyTypeA.raw()); + if (UNEXPECTED(flattenedKeys.isUndef() || Z_TYPE_P(flattenedKeys.raw()) != IS_ARRAY)) return zv::Val(); + for (auto keyEntry : zv::TableRef(Z_ARRVAL_P(flattenedKeys.raw()))) { + zv::Ref keyType = keyEntry.value().deref(); + zv::Val valueA = pt_type_op(Z_OBJ_P(constantArraysA.raw()), PT_OP_GET_OFFSET_VALUE_TYPE, 1, keyType.raw()); + if (UNEXPECTED(valueA.isUndef())) return zv::Val(); + zv::Val valueB = pt_type_op(Z_OBJ_P(constantArraysB.raw()), PT_OP_GET_OFFSET_VALUE_TYPE, 1, keyType.raw()); + if (UNEXPECTED(valueB.isUndef())) return zv::Val(); + zv::Val generalizedValue = generalizeType(valueA.raw(), valueB.raw(), depth + 1); + if (UNEXPECTED(generalizedValue.isUndef())) return zv::Val(); + zend_long hasA = typeOpTrinary(Z_OBJ_P(constantArraysA.raw()), PT_OP_HAS_OFFSET_VALUE_TYPE, 1, keyType.raw()); + if (UNEXPECTED(hasA < 0)) return zv::Val(); + zend_long hasB = typeOpTrinary(Z_OBJ_P(constantArraysB.raw()), PT_OP_HAS_OFFSET_VALUE_TYPE, 1, keyType.raw()); + if (UNEXPECTED(hasB < 0)) return zv::Val(); + /* !$hasA->and($hasB)->negate()->no() — negate()->no() + * holds exactly when the and() is Yes */ + bool optional = (hasA & hasB) != PT_TRI_YES; + if (UNEXPECTED(!pt_constant_array_type_builder_set_offset_value_type(builder.raw(), keyType.raw(), generalizedValue.raw(), optional))) { + return zv::Val(); + } + } + zv::Val resultArray = pt_constant_array_type_builder_get_array(builder.raw()); + if (UNEXPECTED(resultArray.isUndef())) return zv::Val(); + resultTypes.push(std::move(resultArray)); + } else { + /* Both inputs are sealed constant array shapes — see the + * twin's comment: keep the literal union instead of + * widening the keys and values */ + bool bothSealed = true; + for (uint32_t side = 0; side < 2 && bothSealed; side++) { + for (auto checkEntry : zv::TableRef(constantArrays.table((int) side))) { + zv::Val constantArrayInstances = pt_type_op(checkEntry.value().deref().asObject(), PT_OP_GET_CONSTANT_ARRAYS, 0, NULL); + if (UNEXPECTED(constantArrayInstances.isUndef() || Z_TYPE_P(constantArrayInstances.raw()) != IS_ARRAY)) return zv::Val(); + for (auto instance : zv::TableRef(Z_ARRVAL_P(constantArrayInstances.raw()))) { + zend_long isSealed = pt_type_call_trinary(instance.value().deref().asObject(), PT_LC("issealed"), 0, NULL); + if (UNEXPECTED(isSealed < 0)) return zv::Val(); + if (isSealed != PT_TRI_YES) { + bothSealed = false; + break; + } + } + if (!bothSealed) break; + } + } + + zv::Val valueTypeA = pt_type_op(Z_OBJ_P(constantArraysA.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); + if (UNEXPECTED(valueTypeA.isUndef())) return zv::Val(); + zv::Val valueTypeB = pt_type_op(Z_OBJ_P(constantArraysB.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); + if (UNEXPECTED(valueTypeB.isUndef())) return zv::Val(); + zv::Val resultKeyType, resultValueType; + if (bothSealed) { + resultKeyType = unionTypes(keyTypeA.raw(), keyTypeB.raw()); + if (UNEXPECTED(resultKeyType.isUndef())) return zv::Val(); + resultValueType = unionTypes(valueTypeA.raw(), valueTypeB.raw()); + if (UNEXPECTED(resultValueType.isUndef())) return zv::Val(); + zend_long isOversized = pt_type_call_trinary(Z_OBJ_P(resultValueType.raw()), PT_LC("isoversizedarray"), 0, NULL); + if (UNEXPECTED(isOversized < 0)) return zv::Val(); + if (isOversized == PT_TRI_YES) { + /* the literal value union outgrew the shape limit */ + zv::Val generalizedValue = generalizeType(valueTypeA.raw(), valueTypeB.raw(), depth + 1); + if (UNEXPECTED(generalizedValue.isUndef())) return zv::Val(); + resultValueType = pt_type_combinator_union(1, generalizedValue.raw()); + if (UNEXPECTED(resultValueType.isUndef())) return zv::Val(); + } + } else { + zv::Val generalizedKey = generalizeType(keyTypeA.raw(), keyTypeB.raw(), depth + 1); + if (UNEXPECTED(generalizedKey.isUndef())) return zv::Val(); + resultKeyType = pt_type_combinator_union(1, generalizedKey.raw()); + if (UNEXPECTED(resultKeyType.isUndef())) return zv::Val(); + zv::Val generalizedValue = generalizeType(valueTypeA.raw(), valueTypeB.raw(), depth + 1); + if (UNEXPECTED(generalizedValue.isUndef())) return zv::Val(); + resultValueType = pt_type_combinator_union(1, generalizedValue.raw()); + if (UNEXPECTED(resultValueType.isUndef())) return zv::Val(); + } + + zval resultTypeZv; + if (UNEXPECTED(!pt_array_type_new(&resultTypeZv, resultKeyType.raw(), resultValueType.raw()))) return zv::Val(); + zv::Val resultType = zv::Val::adopt(resultTypeZv); + + zv::Arr accessories = zv::Arr::create(2); + zend_long iterableA = typeOpTrinary(Z_OBJ_P(constantArraysA.raw()), PT_OP_IS_ITERABLE_AT_LEAST_ONCE, 0, NULL); + if (UNEXPECTED(iterableA < 0)) return zv::Val(); + bool nonEmpty = iterableA == PT_TRI_YES; + if (nonEmpty) { + zend_long iterableB = typeOpTrinary(Z_OBJ_P(constantArraysB.raw()), PT_OP_IS_ITERABLE_AT_LEAST_ONCE, 0, NULL); + if (UNEXPECTED(iterableB < 0)) return zv::Val(); + nonEmpty = iterableB == PT_TRI_YES; + } + if (nonEmpty) { + bool greaterOrEqual; + if (UNEXPECTED(!arraySizeGreaterOrEqual(constantArraysA.raw(), constantArraysB.raw(), greaterOrEqual))) return zv::Val(); + nonEmpty = greaterOrEqual; + } + if (nonEmpty) { + zval nonEmptyZv; + if (UNEXPECTED(!pt_non_empty_array_type_new(&nonEmptyZv))) return zv::Val(); + accessories.push(zv::Val::adopt(nonEmptyZv)); + } + zend_long listA = typeOpTrinary(Z_OBJ_P(constantArraysA.raw()), PT_OP_IS_LIST, 0, NULL); + if (UNEXPECTED(listA < 0)) return zv::Val(); + if (listA == PT_TRI_YES) { + zend_long listB = typeOpTrinary(Z_OBJ_P(constantArraysB.raw()), PT_OP_IS_LIST, 0, NULL); + if (UNEXPECTED(listB < 0)) return zv::Val(); + if (listB == PT_TRI_YES) { + zval listZv; + if (UNEXPECTED(!pt_accessory_array_list_type_new(&listZv))) return zv::Val(); + accessories.push(zv::Val::adopt(listZv)); + } + } + + if (zend_hash_num_elements(accessories.table()) == 0) { + resultTypes.push(std::move(resultType)); + } else { + zv::Val intersected = intersectWithAccessories(resultType.raw(), accessories.table()); + if (UNEXPECTED(intersected.isUndef())) return zv::Val(); + resultTypes.push(std::move(intersected)); + } + } + } + } else if (constantArrays.count(1) > 0) { + zv::Val united = unionOfList(constantArrays.table(1)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } + + if (generalArrays.count(0) > 0) { + if (generalArrays.count(1) == 0) { + zv::Val united = unionOfList(generalArrays.table(0)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } else { + zv::Val generalArraysA = unionOfList(generalArrays.table(0)); + if (UNEXPECTED(generalArraysA.isUndef())) return zv::Val(); + zv::Val generalArraysB = unionOfList(generalArrays.table(1)); + if (UNEXPECTED(generalArraysB.isUndef())) return zv::Val(); + zv::Val aValueType = pt_type_op(Z_OBJ_P(generalArraysA.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); + if (UNEXPECTED(aValueType.isUndef())) return zv::Val(); + zv::Val bValueType = pt_type_op(Z_OBJ_P(generalArraysB.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); + if (UNEXPECTED(bValueType.isUndef())) return zv::Val(); + bool nestedArrays; + if (UNEXPECTED(!isNonConstantArray(aValueType.raw(), nestedArrays))) return zv::Val(); + if (nestedArrays && UNEXPECTED(!isNonConstantArray(bValueType.raw(), nestedArrays))) return zv::Val(); + if (nestedArrays) { + zend_long aDepth, bDepth; + if (UNEXPECTED(!getArrayDepth(aValueType.raw(), aDepth) || !getArrayDepth(bValueType.raw(), bDepth))) return zv::Val(); + aDepth += depth; + bDepth += depth; + if ((aDepth > 2 || bDepth > 2) && aDepth != bDepth) { + zval aMixed, bMixed; + if (UNEXPECTED(!pt_mixed_type_new(&aMixed))) return zv::Val(); + aValueType = zv::Val::adopt(aMixed); + if (UNEXPECTED(!pt_mixed_type_new(&bMixed))) return zv::Val(); + bValueType = zv::Val::adopt(bMixed); + } + } + + zv::Val keyTypeA = pt_type_op(Z_OBJ_P(generalArraysA.raw()), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); + if (UNEXPECTED(keyTypeA.isUndef())) return zv::Val(); + zv::Val keyTypeB = pt_type_op(Z_OBJ_P(generalArraysB.raw()), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); + if (UNEXPECTED(keyTypeB.isUndef())) return zv::Val(); + zv::Val generalizedKey = generalizeType(keyTypeA.raw(), keyTypeB.raw(), depth + 1); + if (UNEXPECTED(generalizedKey.isUndef())) return zv::Val(); + zv::Val resultKeyType = pt_type_combinator_union(1, generalizedKey.raw()); + if (UNEXPECTED(resultKeyType.isUndef())) return zv::Val(); + zv::Val generalizedValue = generalizeType(aValueType.raw(), bValueType.raw(), depth + 1); + if (UNEXPECTED(generalizedValue.isUndef())) return zv::Val(); + zv::Val resultValueType = pt_type_combinator_union(1, generalizedValue.raw()); + if (UNEXPECTED(resultValueType.isUndef())) return zv::Val(); + zval resultTypeZv; + if (UNEXPECTED(!pt_array_type_new(&resultTypeZv, resultKeyType.raw(), resultValueType.raw()))) return zv::Val(); + zv::Val resultType = zv::Val::adopt(resultTypeZv); + + zv::Arr accessories = zv::Arr::create(3); + bool both; + if (UNEXPECTED(!bothYes(generalArraysA.raw(), generalArraysB.raw(), PT_OP_IS_ITERABLE_AT_LEAST_ONCE, both))) return zv::Val(); + if (both) { + zval nonEmptyZv; + if (UNEXPECTED(!pt_non_empty_array_type_new(&nonEmptyZv))) return zv::Val(); + accessories.push(zv::Val::adopt(nonEmptyZv)); + } + if (UNEXPECTED(!bothYes(generalArraysA.raw(), generalArraysB.raw(), PT_OP_IS_LIST, both))) return zv::Val(); + if (both) { + zval listZv; + if (UNEXPECTED(!pt_accessory_array_list_type_new(&listZv))) return zv::Val(); + accessories.push(zv::Val::adopt(listZv)); + } + zend_long oversizedA = pt_type_call_trinary(Z_OBJ_P(generalArraysA.raw()), PT_LC("isoversizedarray"), 0, NULL); + if (UNEXPECTED(oversizedA < 0)) return zv::Val(); + if (oversizedA == PT_TRI_YES) { + zend_long oversizedB = pt_type_call_trinary(Z_OBJ_P(generalArraysB.raw()), PT_LC("isoversizedarray"), 0, NULL); + if (UNEXPECTED(oversizedB < 0)) return zv::Val(); + if (oversizedB == PT_TRI_YES) { + zval oversizedZv; + if (UNEXPECTED(!pt_oversized_array_type_new(&oversizedZv))) return zv::Val(); + accessories.push(zv::Val::adopt(oversizedZv)); + } + } + + if (zend_hash_num_elements(accessories.table()) == 0) { + resultTypes.push(std::move(resultType)); + } else { + zv::Val intersected = intersectWithAccessories(resultType.raw(), accessories.table()); + if (UNEXPECTED(intersected.isUndef())) return zv::Val(); + resultTypes.push(std::move(intersected)); + } + } + } else if (generalArrays.count(1) > 0) { + zv::Val united = unionOfList(generalArrays.table(1)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } + + if (constantIntegers.count(0) > 0) { + if (constantIntegers.count(1) == 0) { + zv::Val united = unionOfList(constantIntegers.table(0)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } else { + zv::Val constantIntegersA = unionOfList(constantIntegers.table(0)); + if (UNEXPECTED(constantIntegersA.isUndef())) return zv::Val(); + zv::Val constantIntegersB = unionOfList(constantIntegers.table(1)); + if (UNEXPECTED(constantIntegersB.isUndef())) return zv::Val(); + bool same; + if (UNEXPECTED(!typeEquals(constantIntegersA.raw(), constantIntegersB.raw(), same))) return zv::Val(); + if (same) { + resultTypes.push(std::move(constantIntegersA)); + } else { + bool hasMin = false, hasMax = false; + zend_long min = 0, max = 0; + for (auto intEntry : zv::TableRef(constantIntegers.table(0))) { + zend_long value; + if (UNEXPECTED(!pt_constant_integer_get_value(intEntry.value().deref().asObject(), value))) return zv::Val(); + if (!hasMin || value < min) { + min = value; + hasMin = true; + } + if (hasMax && value <= max) continue; + max = value; + hasMax = true; + } + + zend_long newMin = min, newMax = max; + for (auto intEntry : zv::TableRef(constantIntegers.table(1))) { + zend_long value; + if (UNEXPECTED(!pt_constant_integer_get_value(intEntry.value().deref().asObject(), value))) return zv::Val(); + if (value > newMax) { + newMax = value; + } + if (value >= newMin) continue; + newMin = value; + } + + if (newMax > max && newMin < min) { + zv::Val range = pt_integer_range_from_interval(phpstanturbo::NullableLong::of(newMin), phpstanturbo::NullableLong::of(newMax), 0); + if (UNEXPECTED(range.isUndef())) return zv::Val(); + resultTypes.push(std::move(range)); + } else if (newMax > max) { + zv::Val range = pt_integer_range_from_interval(phpstanturbo::NullableLong::of(min), phpstanturbo::NullableLong::null(), 0); + if (UNEXPECTED(range.isUndef())) return zv::Val(); + resultTypes.push(std::move(range)); + } else if (newMin < min) { + zv::Val range = pt_integer_range_from_interval(phpstanturbo::NullableLong::null(), phpstanturbo::NullableLong::of(max), 0); + if (UNEXPECTED(range.isUndef())) return zv::Val(); + resultTypes.push(std::move(range)); + } else { + zv::Val united = unionTypes(constantIntegersA.raw(), constantIntegersB.raw()); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } + } + } + } else if (constantIntegers.count(1) > 0) { + zv::Val united = unionOfList(constantIntegers.table(1)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } + + if (integerRanges.count(0) > 0) { + if (integerRanges.count(1) == 0) { + zv::Val united = unionOfList(integerRanges.table(0)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } else { + zv::Val integerRangesA = unionOfList(integerRanges.table(0)); + if (UNEXPECTED(integerRangesA.isUndef())) return zv::Val(); + zv::Val integerRangesB = unionOfList(integerRanges.table(1)); + if (UNEXPECTED(integerRangesB.isUndef())) return zv::Val(); + bool same; + if (UNEXPECTED(!typeEquals(integerRangesA.raw(), integerRangesB.raw(), same))) return zv::Val(); + if (same) { + resultTypes.push(std::move(integerRangesA)); + } else { + bool hasMin = false, hasMax = false; + zend_long min = 0, max = 0; + for (auto rangeEntry : zv::TableRef(integerRanges.table(0))) { + zend_long rangeMin, rangeMax; + if (UNEXPECTED(!rangeBounds(rangeEntry.value().deref().asObject(), rangeMin, rangeMax))) return zv::Val(); + if (!hasMin || rangeMin < min) { + min = rangeMin; + hasMin = true; + } + if (hasMax && rangeMax <= max) continue; + max = rangeMax; + hasMax = true; + } + + zend_long newMin = min, newMax = max; + for (auto rangeEntry : zv::TableRef(integerRanges.table(1))) { + zend_long rangeMin, rangeMax; + if (UNEXPECTED(!rangeBounds(rangeEntry.value().deref().asObject(), rangeMin, rangeMax))) return zv::Val(); + if (rangeMax > newMax) { + newMax = rangeMax; + } + if (rangeMin >= newMin) continue; + newMin = rangeMin; + } + + bool gotGreater = newMax > max; + bool gotSmaller = newMin < min; + phpstanturbo::NullableLong minValue = min == ZEND_LONG_MIN ? phpstanturbo::NullableLong::null() : phpstanturbo::NullableLong::of(min); + phpstanturbo::NullableLong maxValue = max == ZEND_LONG_MAX ? phpstanturbo::NullableLong::null() : phpstanturbo::NullableLong::of(max); + phpstanturbo::NullableLong newMinValue = newMin == ZEND_LONG_MIN ? phpstanturbo::NullableLong::null() : phpstanturbo::NullableLong::of(newMin); + phpstanturbo::NullableLong newMaxValue = newMax == ZEND_LONG_MAX ? phpstanturbo::NullableLong::null() : phpstanturbo::NullableLong::of(newMax); + + zv::Val result; + if (gotGreater && gotSmaller) { + result = pt_integer_range_from_interval(newMinValue, newMaxValue, 0); + } else if (gotGreater) { + result = pt_integer_range_from_interval(minValue, phpstanturbo::NullableLong::null(), 0); + } else if (gotSmaller) { + result = pt_integer_range_from_interval(phpstanturbo::NullableLong::null(), maxValue, 0); + } else { + result = unionTypes(integerRangesA.raw(), integerRangesB.raw()); + } + if (UNEXPECTED(result.isUndef())) return zv::Val(); + resultTypes.push(std::move(result)); + } + } + } else if (integerRanges.count(1) > 0) { + zv::Val united = unionOfList(integerRanges.table(1)); + if (UNEXPECTED(united.isUndef())) return zv::Val(); + resultTypes.push(std::move(united)); + } + + zv::Val accessoryTypes = pt_type_call_static_ce(pt_ce_type_utils, PT_LC("getaccessorytypes"), 1, a); + if (UNEXPECTED(accessoryTypes.isUndef() || Z_TYPE_P(accessoryTypes.raw()) != IS_ARRAY)) return zv::Val(); + zv::Arr generalizedAccessories = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(accessoryTypes.raw()))); + for (auto accessoryEntry : zv::TableRef(Z_ARRVAL_P(accessoryTypes.raw()))) { + zv::Val generalized = generalizeMoreSpecific(accessoryEntry.value().deref().raw()); + if (UNEXPECTED(generalized.isUndef())) return zv::Val(); + generalizedAccessories.push(std::move(generalized)); + } + + zv::Val combined = unionOfLists(resultTypes.table(), otherTypes.table()); + if (UNEXPECTED(combined.isUndef())) return zv::Val(); + if (wrapBenevolent) { + combined = pt_union_to_benevolent(combined.raw()); + if (UNEXPECTED(combined.isUndef())) return zv::Val(); + } + + zv::Val intersected = intersectWithAccessories(combined.raw(), generalizedAccessories.table()); + if (UNEXPECTED(intersected.isUndef())) return zv::Val(); + zv::Arr head = zv::Arr::create(1); + head.push(intersected.ref()); + return unionOfLists(head.table(), otherTypes.table()); + } + + /* TypeCombinator::intersect($type, ...$accessories) */ + static zv::Val intersectWithAccessories(zval *type, HashTable *accessories) + { + std::vector args; + args.reserve(zend_hash_num_elements(accessories) + 1); + args.push_back(*type); + for (auto entry : zv::TableRef(accessories)) { + args.push_back(*entry.value().deref().raw()); + } + return pt_type_combinator_intersect((uint32_t) args.size(), args.data()); + } + + /* $a->()->yes() && $b->()->yes(); false = pending exception */ + [[nodiscard]] static bool bothYes(zval *a, zval *b, pt_type_op_id op, bool &out) + { + out = false; + zend_long first = typeOpTrinary(Z_OBJ_P(a), op, 0, NULL); + if (UNEXPECTED(first < 0)) return false; + if (first != PT_TRI_YES) return true; + zend_long second = typeOpTrinary(Z_OBJ_P(b), op, 0, NULL); + if (UNEXPECTED(second < 0)) return false; + out = second == PT_TRI_YES; + return true; + } + + /* $type->isArray()->yes() && $type->isConstantArray()->no(); false = + * pending exception */ + static bool isNonConstantArray(zval *type, bool &out) + { + out = false; + zend_long isArray = typeOpTrinary(Z_OBJ_P(type), PT_OP_IS_ARRAY, 0, NULL); + if (UNEXPECTED(isArray < 0)) return false; + if (isArray != PT_TRI_YES) return true; + zend_long isConstantArray = typeOpTrinary(Z_OBJ_P(type), PT_OP_IS_CONSTANT_ARRAY, 0, NULL); + if (UNEXPECTED(isConstantArray < 0)) return false; + out = isConstantArray == PT_TRI_NO; + return true; + } + + /* $range->getMin() / getMax() with PHP_INT_MIN / PHP_INT_MAX for null, + * as the integer-range arm of generalizeType() spells it; false = + * pending exception */ + static bool rangeBounds(zend_object *range, zend_long &min, zend_long &max) + { + phpstanturbo::NullableLong rangeMin, rangeMax; + if (UNEXPECTED(!pt_integer_range_bounds(range, rangeMin, rangeMax))) return false; + min = rangeMin.isNull ? ZEND_LONG_MIN : rangeMin.value; + max = rangeMax.isNull ? ZEND_LONG_MAX : rangeMax.value; + return true; + } + + /* private static (twin 5393) */ + static bool getArrayDepth(zval *type, zend_long &out) + { + zend_long depth = 0; + zv::Val current = zv::Val::copyOf(zv::Ref(type)); + for (;;) { + zv::Val benevolent = pt_union_to_benevolent(current.raw()); + if (UNEXPECTED(benevolent.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(benevolent.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getArrays() on %s", zend_zval_value_name(benevolent.raw())); + return false; + } + zv::Val arrays = pt_type_call(Z_OBJ_P(benevolent.raw()), PT_LC("getarrays"), 0, NULL); + if (UNEXPECTED(arrays.isUndef())) return false; + if (Z_TYPE_P(arrays.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(arrays.raw())) == 0) break; + zv::Val next = pt_type_op(Z_OBJ_P(current.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); + if (UNEXPECTED(next.isUndef())) return false; + current = std::move(next); + depth++; + } + out = depth; + return true; + } + + /* (twin 5407) */ + bool equals(zend_object *otherScope, bool &out) + { + out = false; + zv::Ref context = slot(PT_MS_PROP_CONTEXT); + if (UNEXPECTED(!context.isObject())) { + (void) uninitializedProperty("context"); + return false; + } + zval *otherContext = otherProp(otherScope, PT_MS_PROP_CONTEXT, PT_LC("context")); + if (UNEXPECTED(otherContext == NULL)) return false; + ZVAL_DEREF(otherContext); + zv::Val contextsEqual = pt_type_call(context.asObject(), PT_LC("equals"), 1, otherContext); + if (UNEXPECTED(contextsEqual.isUndef())) return false; + if (!zend_is_true(contextsEqual.raw())) return true; + + HashTable *otherExpressionTypes = otherTable(otherScope, PT_MS_PROP_EXPRESSION_TYPES, PT_LC("expressionTypes")); + if (UNEXPECTED(otherExpressionTypes == NULL || !requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return false; + bool same; + if (UNEXPECTED(!compareVariableTypeHolders(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), otherExpressionTypes, same))) return false; + if (!same) return true; + + HashTable *otherNativeTypes = otherTable(otherScope, PT_MS_PROP_NATIVE_EXPRESSION_TYPES, PT_LC("nativeExpressionTypes")); + if (UNEXPECTED(otherNativeTypes == NULL || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes"))) return false; + if (UNEXPECTED(!compareVariableTypeHolders(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()), otherNativeTypes, same))) return false; + if (!same) return true; + + HashTable *otherConditional = otherTable(otherScope, PT_MS_PROP_CONDITIONAL_EXPRESSIONS, PT_LC("conditionalExpressions")); + if (UNEXPECTED(otherConditional == NULL || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) return false; + return compareConditionalExpressions(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()), otherConditional, out); + } + + /* private (twin 5427) */ + static bool compareConditionalExpressions(HashTable *conditionalExpressions, HashTable *otherConditionalExpressions, bool &out) + { + out = false; + if (zend_hash_num_elements(conditionalExpressions) != zend_hash_num_elements(otherConditionalExpressions)) return true; + for (auto entry : zv::TableRef(conditionalExpressions)) { + zval *otherHolders = pt_ht_find(otherConditionalExpressions, entry.stringKeyOrNull(), entry.indexKey()); + if (otherHolders == NULL || Z_TYPE_P(otherHolders) == IS_NULL) return true; + ZVAL_DEREF(otherHolders); + zv::Ref holders = entry.value().deref(); + if (UNEXPECTED(!holders.isArray() || Z_TYPE_P(otherHolders) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return false; + } + if (zend_hash_num_elements(holders.asArrayTable()) != zend_hash_num_elements(Z_ARRVAL_P(otherHolders))) return true; + for (auto holderEntry : zv::TableRef(holders.asArrayTable())) { + zval *otherHolder = pt_ht_find(Z_ARRVAL_P(otherHolders), holderEntry.stringKeyOrNull(), holderEntry.indexKey()); + if (otherHolder == NULL || Z_TYPE_P(otherHolder) == IS_NULL) return true; + zv::Val typeHolder = conditionalTypeHolder(holderEntry.value()); + zv::Val otherTypeHolder = conditionalTypeHolder(zv::Ref(otherHolder)); + if (UNEXPECTED(typeHolder.isUndef() || otherTypeHolder.isUndef())) return false; + bool equal; + if (UNEXPECTED(!holderEquals(typeHolder.ref(), otherTypeHolder.ref(), equal))) return false; + if (!equal) return true; + zv::Val conditions = conditionalConditions(holderEntry.value()); + zv::Val otherConditions = conditionalConditions(zv::Ref(otherHolder)); + if (UNEXPECTED(conditions.isUndef() || otherConditions.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(conditions.raw()) != IS_ARRAY || Z_TYPE_P(otherConditions.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expression holder has no conditions"); + return false; + } + if (zend_hash_num_elements(Z_ARRVAL_P(conditions.raw())) != zend_hash_num_elements(Z_ARRVAL_P(otherConditions.raw()))) return true; + for (auto conditionEntry : zv::TableRef(Z_ARRVAL_P(conditions.raw()))) { + zval *otherCondition = pt_ht_find(Z_ARRVAL_P(otherConditions.raw()), conditionEntry.stringKeyOrNull(), conditionEntry.indexKey()); + if (otherCondition == NULL || Z_TYPE_P(otherCondition) == IS_NULL) return true; + if (UNEXPECTED(!holderEquals(conditionEntry.value(), zv::Ref(otherCondition), equal))) return false; + if (!equal) return true; + } + } + } + + out = true; + return true; + } + + /* private (twin 5471) */ + static bool compareVariableTypeHolders(HashTable *variableTypeHolders, HashTable *otherVariableTypeHolders, bool &out) + { + out = false; + if (zend_hash_num_elements(variableTypeHolders) != zend_hash_num_elements(otherVariableTypeHolders)) return true; + for (auto entry : zv::TableRef(variableTypeHolders)) { + zval *otherHolder = pt_ht_find(otherVariableTypeHolders, entry.stringKeyOrNull(), entry.indexKey()); + if (otherHolder == NULL || Z_TYPE_P(otherHolder) == IS_NULL) return true; + zend_long certainty = holderCertainty(entry.value()); + zend_long otherCertainty = holderCertainty(zv::Ref(otherHolder)); + if (UNEXPECTED(certainty < 0 || otherCertainty < 0)) return false; + if (certainty != otherCertainty) return true; + bool equalTypes; + if (UNEXPECTED(!holderEqualTypes(entry.value(), zv::Ref(otherHolder), equalTypes))) return false; + if (!equalTypes) return true; + } + + out = true; + return true; + } + + /** + * @api + * @deprecated Use canReadProperty() or canWriteProperty() + * (twin 5497) + */ + bool canAccessProperty(zend_object *propertyReflection, bool &out) { return canAccessClassMember(propertyReflection, out); } + + /** @api (twin 5503) */ + bool canReadProperty(zend_object *propertyReflection, bool &out) { return canAccessClassMember(propertyReflection, out); } + + /** @api (twin 5509) */ + bool canWriteProperty(zend_object *propertyReflection, bool &out) + { + zv::Val isPrivateSet = pt_type_call(propertyReflection, PT_LC("isprivateset"), 0, NULL); + if (UNEXPECTED(isPrivateSet.isUndef())) return false; + if (!zend_is_true(isPrivateSet.raw())) { + zv::Val isProtectedSet = pt_type_call(propertyReflection, PT_LC("isprotectedset"), 0, NULL); + if (UNEXPECTED(isProtectedSet.isUndef())) return false; + if (!zend_is_true(isProtectedSet.raw())) return canAccessClassMember(propertyReflection, out); + } + + zv::Ref phpVersion = slot(PT_MS_PROP_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) { + (void) uninitializedProperty("phpVersion"); + return false; + } + zv::Val supportsAsymmetricVisibility = pt_type_call(phpVersion.asObject(), PT_LC("supportsasymmetricvisibility"), 0, NULL); + if (UNEXPECTED(supportsAsymmetricVisibility.isUndef())) return false; + if (!zend_is_true(supportsAsymmetricVisibility.raw())) return canAccessClassMember(propertyReflection, out); + + return memberAccessibleFromScope(propertyReflection, PT_LC("isprivateset"), out); + } + + /** @api (twin 5555) */ + bool canCallMethod(zend_object *methodReflection, bool &out) + { + if (UNEXPECTED(!canAccessClassMember(methodReflection, out))) return false; + if (out) return true; + zv::Val prototype = pt_type_call(methodReflection, PT_LC("getprototype"), 0, NULL); + if (UNEXPECTED(prototype.isUndef())) return false; + zend_object *prototypeObject = requireObject(prototype, "canAccessClassMember"); + if (UNEXPECTED(prototypeObject == NULL)) return false; + return canAccessClassMember(prototypeObject, out); + } + + /** @api (twin 5565) */ + bool canAccessConstant(zend_object *constantReflection, bool &out) { return canAccessClassMember(constantReflection, out); } + + /* private (twin 5570) */ + bool canAccessClassMember(zend_object *classMemberReflection, bool &out) + { + zv::Val isPublic = pt_type_call(classMemberReflection, PT_LC("ispublic"), 0, NULL); + if (UNEXPECTED(isPublic.isUndef())) return false; + if (zend_is_true(isPublic.raw())) { + out = true; + return true; + } + + return memberAccessibleFromScope(classMemberReflection, PT_LC("isprivate"), out); + } + + /* the `$canAccessClassMember` closure of canAccessClassMember() and + * canWriteProperty() run over the closure-bind classes and the scope's + * own class; $privateLcName picks isPrivate() or isPrivateSet() */ + bool memberAccessibleFromScope(zend_object *memberReflection, const char *privateLcName, size_t privateLen, bool &out) + { + out = false; + zv::Val declaringClass = pt_type_call(memberReflection, PT_LC("getdeclaringclass"), 0, NULL); + if (UNEXPECTED(declaringClass.isUndef())) return false; + zend_object *declaringClassObject = requireObject(declaringClass, "getName"); + if (UNEXPECTED(declaringClassObject == NULL)) return false; + + zv::Ref inClosureBindScopeClasses = slot(PT_MS_PROP_IN_CLOSURE_BIND_SCOPE_CLASSES); + if (UNEXPECTED(!inClosureBindScopeClasses.isArray())) { + (void) uninitializedProperty("inClosureBindScopeClasses"); + return false; + } + zv::Ref reflectionProvider = slot(PT_MS_PROP_REFLECTION_PROVIDER); + if (UNEXPECTED(!reflectionProvider.isObject())) { + (void) uninitializedProperty("reflectionProvider"); + return false; + } + for (auto entry : zv::TableRef(inClosureBindScopeClasses.asArrayTable())) { + zval *className = entry.value().deref().raw(); + bool hasClass; + if (UNEXPECTED(!pt_reflection_provider_has_class(reflectionProvider.asObject(), className, hasClass))) return false; + if (!hasClass) continue; + zv::Val classReflection = pt_reflection_provider_get_class(reflectionProvider.asObject(), className); + if (UNEXPECTED(classReflection.isUndef())) return false; + zend_object *classReflectionObject = requireObject(classReflection, "getName"); + if (UNEXPECTED(classReflectionObject == NULL)) return false; + bool accessible; + if (UNEXPECTED(!memberAccessibleFrom(classReflectionObject, memberReflection, declaringClassObject, privateLcName, privateLen, accessible))) { + return false; + } + if (accessible) { + out = true; + return true; + } + } + + bool isInClass; + if (UNEXPECTED(!thisIsInClass(isInClass))) return false; + if (!isInClass) return true; + zv::Val classReflection = thisGetClassReflection(); + if (UNEXPECTED(classReflection.isUndef())) return false; + zend_object *classReflectionObject = requireObject(classReflection, "getName"); + if (UNEXPECTED(classReflectionObject == NULL)) return false; + return memberAccessibleFrom(classReflectionObject, memberReflection, declaringClassObject, privateLcName, privateLen, out); + } + + /* one evaluation of that closure for one ClassReflection */ + static bool memberAccessibleFrom(zend_object *classReflection, zend_object *memberReflection, zend_object *declaringClass, const char *privateLcName, size_t privateLen, bool &out) + { + out = false; + zv::Val isPrivate = pt_type_call(memberReflection, privateLcName, privateLen, 0, NULL); + if (UNEXPECTED(isPrivate.isUndef())) return false; + zv::Val className = pt_class_reflection_get_name(classReflection); + if (UNEXPECTED(className.isUndef())) return false; + zv::Val declaringClassName = pt_class_reflection_get_name(declaringClass); + if (UNEXPECTED(declaringClassName.isUndef())) return false; + bool sameName = Z_TYPE_P(className.raw()) == IS_STRING + && Z_TYPE_P(declaringClassName.raw()) == IS_STRING + && zend_string_equals(Z_STR_P(className.raw()), Z_STR_P(declaringClassName.raw())); + if (zend_is_true(isPrivate.raw())) { + out = sameName; + return true; + } + + /* protected */ + if (sameName) { + out = true; + return true; + } + zv::Val withoutFinalOverride = pt_type_call(declaringClass, PT_LC("removefinalkeywordoverride"), 0, NULL); + if (UNEXPECTED(withoutFinalOverride.isUndef())) return false; + zv::Val isSubclass = pt_type_call(classReflection, PT_LC("issubclassofclass"), 1, withoutFinalOverride.raw()); + if (UNEXPECTED(isSubclass.isUndef())) return false; + if (zend_is_true(isSubclass.raw())) { + out = true; + return true; + } + + zv::Val memberDeclaringClass = pt_type_call(memberReflection, PT_LC("getdeclaringclass"), 0, NULL); + if (UNEXPECTED(memberDeclaringClass.isUndef())) return false; + zend_object *memberDeclaringClassObject = requireObject(memberDeclaringClass, "isSubclassOfClass"); + if (UNEXPECTED(memberDeclaringClassObject == NULL)) return false; + zval classReflectionZv; + ZVAL_OBJ(&classReflectionZv, classReflection); + zv::Val result = pt_type_call(memberDeclaringClassObject, PT_LC("issubclassofclass"), 1, &classReflectionZv); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* TrinaryLogic::describe() — the twin's final class, its three labels */ + static const char *certaintyLabel(zend_long certainty) + { + return certainty == PT_TRI_YES ? "Yes" : (certainty == PT_TRI_MAYBE ? "Maybe" : "No"); + } + + /* (twin 5614) */ + zv::Val debug() + { + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes") + || !requireSlot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES, "nativeExpressionTypes") + || !requireSlot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS, "currentlyAssignedExpressions") + || !requireSlot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS, "currentlyAllowedUndefinedExpressions") + || !requireSlot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS, "conditionalExpressions"))) { + return zv::Val(); + } + zv::Arr descriptions = zv::Arr::create(0); + for (auto entry : zv::TableRef(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()))) { + if (UNEXPECTED(!describeHolder(descriptions, entry, NULL))) return zv::Val(); + } + for (auto entry : zv::TableRef(Z_ARRVAL_P(slot(PT_MS_PROP_NATIVE_EXPRESSION_TYPES).raw()))) { + if (UNEXPECTED(!describeHolder(descriptions, entry, "native "))) return zv::Val(); + } + for (auto entry : zv::TableRef(Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ASSIGNED_EXPRESSIONS).raw()))) { + zv::Str exprString = entryKey(entry.stringKeyOrNull(), entry.indexKey()); + zv::Str key = zv::Str::adopt(zend_strpprintf(0, "currently assigned %s", ZSTR_VAL(exprString.get()))); + descriptions.set(key.get(), zv::Val::string("true", 4)); + } + for (auto entry : zv::TableRef(Z_ARRVAL_P(slot(PT_MS_PROP_CURRENTLY_ALLOWED_UNDEFINED_EXPRESSIONS).raw()))) { + zv::Str exprString = entryKey(entry.stringKeyOrNull(), entry.indexKey()); + zv::Str key = zv::Str::adopt(zend_strpprintf(0, "currently allowed undefined %s", ZSTR_VAL(exprString.get()))); + descriptions.set(key.get(), zv::Val::string("true", 4)); + } + for (auto entry : zv::TableRef(Z_ARRVAL_P(slot(PT_MS_PROP_CONDITIONAL_EXPRESSIONS).raw()))) { + zv::Str exprString = entryKey(entry.stringKeyOrNull(), entry.indexKey()); + zv::Ref holders = entry.value().deref(); + if (UNEXPECTED(!holders.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expressions entry is not an array"); + return zv::Val(); + } + zend_long index = 0; + for (auto holderEntry : zv::TableRef(holders.asArrayTable())) { + zv::Str key = zv::Str::adopt(zend_strpprintf(0, "condition about %s #" ZEND_LONG_FMT, ZSTR_VAL(exprString.get()), index + 1)); + index++; + zv::Val conditions = conditionalConditions(holderEntry.value()); + if (UNEXPECTED(conditions.isUndef() || Z_TYPE_P(conditions.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: a conditional expression holder has no conditions"); + return zv::Val(); + } + smart_str condition = {}; + bool first = true; + for (auto conditionEntry : zv::TableRef(Z_ARRVAL_P(conditions.raw()))) { + if (!first) { + smart_str_appendl(&condition, " && ", 4); + } + first = false; + zv::Str conditionalExprString = entryKey(conditionEntry.stringKeyOrNull(), conditionEntry.indexKey()); + smart_str_append(&condition, conditionalExprString.get()); + smart_str_appendc(&condition, '='); + zv::Val conditionType = holderType(conditionEntry.value()); + zval described; + if (UNEXPECTED(conditionType.isUndef() || !pt_type_describe_precise(conditionType.raw(), &described))) { + smart_str_free(&condition); + return zv::Val(); + } + zv::Val describedValue = zv::Val::adopt(described); + if (EXPECTED(Z_TYPE_P(describedValue.raw()) == IS_STRING)) { + smart_str_append(&condition, Z_STR_P(describedValue.raw())); + } + } + zv::Str conditionString = zv::Str::adopt(smart_str_extract(&condition)); + + zv::Val typeHolder = conditionalTypeHolder(holderEntry.value()); + if (UNEXPECTED(typeHolder.isUndef())) return zv::Val(); + zv::Val type = holderType(typeHolder.ref()); + zend_long certainty = holderCertainty(typeHolder.ref()); + if (UNEXPECTED(type.isUndef() || certainty < 0)) return zv::Val(); + zval described; + if (UNEXPECTED(!pt_type_describe_precise(type.raw(), &described))) return zv::Val(); + zv::Val describedValue = zv::Val::adopt(described); + zend_string *describedString = zval_get_string(describedValue.raw()); + zv::Str value = zv::Str::adopt(zend_strpprintf( + 0, + "if %s then %s is %s (%s)", + ZSTR_VAL(conditionString.get()), + ZSTR_VAL(exprString.get()), + ZSTR_VAL(describedString), + certaintyLabel(certainty))); + zend_string_release(describedString); + descriptions.set(key.get(), zv::Val::string(value.get())); + } + } + + return zv::Val(std::move(descriptions)); + } + + /* one `$name (certainty) => description` line of debug() */ + static bool describeHolder(zv::Arr &descriptions, zv::ArrayEntry entry, const char *prefix) + { + zv::Str name = entryKey(entry.stringKeyOrNull(), entry.indexKey()); + zend_long certainty = holderCertainty(entry.value()); + if (UNEXPECTED(certainty < 0)) return false; + zv::Str key = zv::Str::adopt(prefix == NULL + ? zend_strpprintf(0, "%s (%s)", ZSTR_VAL(name.get()), certaintyLabel(certainty)) + : zend_strpprintf(0, "%s%s (%s)", prefix, ZSTR_VAL(name.get()), certaintyLabel(certainty))); + zv::Val type = holderType(entry.value()); + if (UNEXPECTED(type.isUndef())) return false; + zval described; + if (UNEXPECTED(!pt_type_describe_precise(type.raw(), &described))) return false; + descriptions.set(key.get(), zv::Val::adopt(described)); + return true; + } + + /* the predicates the filterTypes() sites pass: state0 is the member name + * (null where the predicate takes none), state1 the lowercase method to + * ask of every inner type */ + static void filterPredicate(zval *state0, zval *state1, uint32_t argc, zval *argv, zval *return_value) + { + if (UNEXPECTED(argc < 1 || Z_TYPE_P(argv) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: a type filter was called without a Type"); + return; + } + zend_long value = Z_TYPE_P(state0) == IS_STRING + ? pt_type_call_trinary(Z_OBJ_P(argv), Z_STRVAL_P(state1), Z_STRLEN_P(state1), 1, state0) + : pt_type_call_trinary(Z_OBJ_P(argv), Z_STRVAL_P(state1), Z_STRLEN_P(state1), 0, NULL); + if (UNEXPECTED(value < 0)) return; + ZVAL_BOOL(return_value, value == PT_TRI_YES); + } + + /* $type->filterTypes(static fn (Type $innerType) => $innerType->($name)->yes()) */ + static zv::Val filterUnionTypes(zval *type, const char *lcname, size_t len, zval *memberName) + { + zval method; + ZVAL_STRINGL(&method, lcname, len); + zv::Val methodValue = zv::Val::adopt(method); + zval noName; + ZVAL_NULL(&noName); + zv::Val callback = pt_type_native_callback(filterPredicate, memberName != NULL ? memberName : &noName, methodValue.raw()); + if (UNEXPECTED(callback.isUndef())) return zv::Val(); + return pt_type_call(Z_OBJ_P(type), PT_LC("filtertypes"), 1, callback.raw()); + } + + /* (twin 5655) */ + zv::Val filterTypeWithMethod(zval *typeWithMethod, zend_string *methodName) + { + zval methodNameZv; + ZVAL_STR(&methodNameZv, methodName); + if (zv::Ref(typeWithMethod).instanceOf(pt_ce_union_type)) { + zv::Val filtered = filterUnionTypes(typeWithMethod, PT_LC("hasmethod"), &methodNameZv); + if (UNEXPECTED(filtered.isUndef())) return zv::Val(); + if (filtered.ref().instanceOf(pt_ce_never_type)) return zv::Val::null(); + return filtered; + } + + zend_long hasMethod = typeOpTrinary(Z_OBJ_P(typeWithMethod), PT_OP_HAS_METHOD, 1, &methodNameZv); + if (UNEXPECTED(hasMethod < 0)) return zv::Val(); + if (hasMethod != PT_TRI_YES) return zv::Val::null(); + + return zv::Val::copyOf(zv::Ref(typeWithMethod)); + } + + /** @api (twin 5670) */ + zv::Val getMethodReflection(zval *typeWithMethod, zend_string *methodName) + { + zval methodNameZv; + ZVAL_STR(&methodNameZv, methodName); + zv::Val type = thisFilterTypeWithMethod(typeWithMethod, &methodNameZv); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (type.isNull()) return zv::Val::null(); + zv::Args args{methodName, self}; + return pt_type_call(Z_OBJ_P(type.raw()), PT_LC("getmethod"), 2, args); + } + + /* (twin 5680) */ + zv::Val getNakedMethod(zval *typeWithMethod, zend_string *methodName) + { + zval methodNameZv; + ZVAL_STR(&methodNameZv, methodName); + zv::Val type = thisFilterTypeWithMethod(typeWithMethod, &methodNameZv); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (type.isNull()) return zv::Val::null(); + zv::Args args{methodName, self}; + zv::Val prototype = pt_type_op(Z_OBJ_P(type.raw()), PT_OP_GET_UNRESOLVED_METHOD_PROTOTYPE, 2, args); + if (UNEXPECTED(prototype.isUndef())) return zv::Val(); + zend_object *prototypeObject = requireObject(prototype, "getNakedMethod"); + if (UNEXPECTED(prototypeObject == NULL)) return zv::Val(); + return pt_type_call(prototypeObject, PT_LC("getnakedmethod"), 0, NULL); + } + + /** + * @api + * @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead + * (twin 5694) + */ + zv::Val getPropertyReflection(zval *typeWithProperty, zend_string *propertyName) + { + zval propertyNameZv; + ZVAL_STR(&propertyNameZv, propertyName); + zv::Val filtered; + zval *type = typeWithProperty; + if (zv::Ref(typeWithProperty).instanceOf(pt_ce_union_type)) { + filtered = filterUnionTypes(typeWithProperty, PT_LC("hasproperty"), &propertyNameZv); + if (UNEXPECTED(filtered.isUndef())) return zv::Val(); + if (filtered.ref().instanceOf(pt_ce_never_type)) return zv::Val::null(); + type = filtered.raw(); + } else { + zend_long hasProperty = pt_type_call_trinary(Z_OBJ_P(type), PT_LC("hasproperty"), 1, &propertyNameZv); + if (UNEXPECTED(hasProperty < 0)) return zv::Val(); + if (hasProperty != PT_TRI_YES) return zv::Val::null(); + } + + zv::Args args{propertyName, self}; + return pt_type_call(Z_OBJ_P(type), PT_LC("getproperty"), 2, args); + } + + /** @api (twin 5709) */ + zv::Val getInstancePropertyReflection(zval *typeWithProperty, zend_string *propertyName) + { + return propertyReflectionOf(typeWithProperty, propertyName, PT_LC("hasinstanceproperty"), PT_LC("getinstanceproperty")); + } + + /** @api (twin 5725) */ + zv::Val getStaticPropertyReflection(zval *typeWithProperty, zend_string *propertyName) + { + return propertyReflectionOf(typeWithProperty, propertyName, PT_LC("hasstaticproperty"), PT_LC("getstaticproperty")); + } + + /* the shared body of getInstancePropertyReflection() and + * getStaticPropertyReflection(): the union filter, then the same + * has*()/get*() pair on whatever came out */ + zv::Val propertyReflectionOf(zval *typeWithProperty, zend_string *propertyName, const char *hasLcName, size_t hasLen, const char *getLcName, size_t getLen) + { + zval propertyNameZv; + ZVAL_STR(&propertyNameZv, propertyName); + zv::Val filtered; + zval *type = typeWithProperty; + if (zv::Ref(typeWithProperty).instanceOf(pt_ce_union_type)) { + filtered = filterUnionTypes(typeWithProperty, hasLcName, hasLen, &propertyNameZv); + if (UNEXPECTED(filtered.isUndef())) return zv::Val(); + if (filtered.ref().instanceOf(pt_ce_never_type)) return zv::Val::null(); + type = filtered.raw(); + } + + zend_long hasProperty = pt_type_call_trinary(Z_OBJ_P(type), hasLcName, hasLen, 1, &propertyNameZv); + if (UNEXPECTED(hasProperty < 0)) return zv::Val(); + if (hasProperty != PT_TRI_YES) return zv::Val::null(); + + zv::Args args{propertyName, self}; + return pt_type_call(Z_OBJ_P(type), getLcName, getLen, 2, args); + } + + /* (twin 5740) */ + zv::Val getConstantReflection(zval *typeWithConstant, zend_string *constantName) + { + zval constantNameZv; + ZVAL_STR(&constantNameZv, constantName); + zv::Val filtered; + zval *type = typeWithConstant; + if (zv::Ref(typeWithConstant).instanceOf(pt_ce_union_type)) { + filtered = filterUnionTypes(typeWithConstant, PT_LC("hasconstant"), &constantNameZv); + if (UNEXPECTED(filtered.isUndef())) return zv::Val(); + if (filtered.ref().instanceOf(pt_ce_never_type)) return zv::Val::null(); + type = filtered.raw(); + } else { + zend_long hasConstant = pt_type_call_trinary(Z_OBJ_P(type), PT_LC("hasconstant"), 1, &constantNameZv); + if (UNEXPECTED(hasConstant < 0)) return zv::Val(); + if (hasConstant != PT_TRI_YES) return zv::Val::null(); + } + + return pt_type_call(Z_OBJ_P(type), PT_LC("getconstant"), 1, &constantNameZv); + } + + /* (twin 5755) */ + zv::Val getConstantExplicitTypeFromConfig(zend_string *constantName, zval *constantType) + { + zv::Ref constantResolver = slot(PT_MS_PROP_CONSTANT_RESOLVER); + if (UNEXPECTED(!constantResolver.isObject())) return uninitializedProperty("constantResolver"); + zv::Args args{constantName, constantType}; + return pt_type_call(constantResolver.asObject(), PT_LC("resolveconstanttype"), 2, args); + } + + /* (twin 5811 / 5823) */ + zv::Val getIterableKeyType(zval *iteratee) { return iterableTypeOf(iteratee, PT_OP_GET_ITERABLE_KEY_TYPE); } + zv::Val getIterableValueType(zval *iteratee) { return iterableTypeOf(iteratee, PT_OP_GET_ITERABLE_VALUE_TYPE); } + + zv::Val iterableTypeOf(zval *iteratee, pt_type_op_id op) + { + zv::Val filtered; + zval *type = iteratee; + if (zv::Ref(iteratee).instanceOf(pt_ce_union_type)) { + filtered = filterUnionTypes(iteratee, PT_LC("isiterable"), NULL); + if (UNEXPECTED(filtered.isUndef())) return zv::Val(); + if (!filtered.ref().instanceOf(pt_ce_never_type)) { + type = filtered.raw(); + } + } + + return pt_type_op(Z_OBJ_P(type), op, 0, NULL); + } + + /* (twin 5858) */ + bool invokeNodeCallback(zend_object *node) + { + zv::Ref nodeCallback = slot(PT_MS_PROP_NODE_CALLBACK); + if (UNEXPECTED(nodeCallback.isUndef())) { + (void) uninitializedProperty("nodeCallback"); + return false; + } + if (nodeCallback.isNull()) { + throwNodeCallbackMissing(); + return false; + } + zv::Args args{node, self}; + zv::Val result = pt_type_call_callable(nodeCallback.raw(), 2, args); + return !result.isUndef(); + } + + /* (twin 5874) */ + bool emitCollectedData(zend_string *collectorType, zval *data) + { + zv::Ref nodeCallback = slot(PT_MS_PROP_NODE_CALLBACK); + if (UNEXPECTED(nodeCallback.isUndef())) { + (void) uninitializedProperty("nodeCallback"); + return false; + } + if (nodeCallback.isNull()) { + throwNodeCallbackMissing(); + return false; + } + zv::Args nodeArgs{collectorType, data}; + zv::Val node = pt_type_new(PT_CLASS_EMIT_COLLECTED_DATA_NODE, 2, nodeArgs); + if (UNEXPECTED(node.isUndef())) return false; + zv::Args args{node.raw(), self}; + zv::Val result = pt_type_call_callable(nodeCallback.raw(), 2, args); + return !result.isUndef(); + } + + static void throwNodeCallbackMissing() + { + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (ce == NULL) return; /* error already thrown */ + zend_throw_exception(ce, "Node callback is not present in this scope", 0); + } + + /* }}} */ + + /* {{{ out of the twin's file order: getNodeKey() 1093, getExprPrinter() 1099, + * hasExpressionType() 1814, getTrackedExpressionType() 1827, + * isInFirstLevelStatement() 4383, getGlobalConstantType() 5776 */ + + zv::Val getNodeKey(zend_object *node) + { + zv::Ref exprPrinter = slot(PT_MS_PROP_EXPR_PRINTER); + if (UNEXPECTED(!exprPrinter.isObject())) return uninitializedProperty("exprPrinter"); + zend_string *key = pt_node_key(node, exprPrinter.raw()); + if (UNEXPECTED(key == NULL)) return zv::Val(); + return zv::Val::adoptString(key); + } + + /** @internal */ + zv::Val getExprPrinter() const { return copyOfSlot(PT_MS_PROP_EXPR_PRINTER); } + + zv::Val hasExpressionType(zend_object *node) + { + zv::Ref exprPrinter = slot(PT_MS_PROP_EXPR_PRINTER); + if (UNEXPECTED(!exprPrinter.isObject())) return uninitializedProperty("exprPrinter"); + return pt_scope_ops_has_expression_type(thisZval(), node, exprPrinter.raw()); + } + + /** @internal */ + zv::Val getTrackedExpressionType(zend_object *node) + { + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + zv::Val key = thisGetNodeKey(&nodeZv); + if (UNEXPECTED(key.isUndef())) return zv::Val(); + zend_string *keyStr = keyString(key); + if (UNEXPECTED(keyStr == NULL)) return zv::Val(); + zval *holder = zend_symtable_find(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), keyStr); + if (holder == NULL) { + /* the twin's undefined-offset read: the warning, then the Error + * of the method call on null */ + zend_error(E_WARNING, "Undefined array key \"%s\"", ZSTR_VAL(keyStr)); + if (UNEXPECTED(EG(exception))) return zv::Val(); + zend_throw_error(NULL, "Call to a member function getType() on null"); + return zv::Val(); + } + return holderType(zv::Ref(holder)); + } + + /* {{{ out of the twin's file order: getScopeStateType() 3473 and + * resolveScopeStateType() 3490 (private; getKeepVoidType() and + * getCurrentTypesOfSpecifiedExpr() call them) */ + + zv::Val getScopeStateType(zend_object *expr) { return resolveScopeStateType(expr, false); } + + /* Reads a narrowable expression's current type from the scope's + * tracked state (recursing into its operands), instead of routing + * through the stored ExpressionResult callbacks. */ + zv::Val resolveScopeStateType(zend_object *expr, bool native) + { + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + bool isVariable; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_VARIABLE, isVariable))) return zv::Val(); + if (!isVariable) { + zv::Val has = thisHasExpressionType(&exprZv); + if (UNEXPECTED(has.isUndef())) return zv::Val(); + zend_long certainty = pt_type_trinary_value(has.raw()); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_YES) { + /* mirror resolveType()'s tracked-holder lookup without + * pricing the node - the tracked type IS scope state */ + zv::Val askScope = native ? thisDoNotTreatPhpDocTypesAsCertain() : self_(); + if (UNEXPECTED(askScope.isUndef())) return zv::Val(); + zend_object *askObject = requireObject(askScope, "getNodeKey"); + if (UNEXPECTED(askObject == NULL)) return zv::Val(); + zv::Val key = MutatingScope(askObject).thisGetNodeKey(&exprZv); + if (UNEXPECTED(key.isUndef())) return zv::Val(); + zend_string *keyStr = keyString(key); + if (UNEXPECTED(keyStr == NULL)) return zv::Val(); + zv::Val trackedType = pt_scope_ops_expression_type_by_key(askScope.raw(), expr, keyStr); + if (UNEXPECTED(trackedType.isUndef())) return zv::Val(); + if (!trackedType.isNull()) return pt_type_utils_resolve_late_resolvable_types(trackedType.raw()); + + return native ? thisGetNativeType(&exprZv) : thisGetType(&exprZv); + } + } + + if (isVariable) { + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return zv::Val(); + name = name.deref(); + if (name.isString()) { + zv::Val scope = native ? thisDoNotTreatPhpDocTypesAsCertain() : self_(); + if (UNEXPECTED(scope.isUndef())) return zv::Val(); + zend_object *scopeObject = requireObject(scope, "hasVariableType"); + if (UNEXPECTED(scopeObject == NULL)) return zv::Val(); + MutatingScope asked(scopeObject); + zv::Val has = asked.thisHasVariableType(name.raw()); + if (UNEXPECTED(has.isUndef())) return zv::Val(); + zend_long certainty = pt_type_trinary_value(has.raw()); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_NO) return pt_type_new_error_type(); + return asked.thisGetVariableType(name.raw()); + } + } + + bool isArrayDimFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_ARRAY_DIM_FETCH, isArrayDimFetch))) return zv::Val(); + if (isArrayDimFetch) { + zv::Ref dim = nodeProp(expr, PT_LC("dim")); + if (UNEXPECTED(dim.raw() == NULL)) return zv::Val(); + dim = dim.deref(); + if (!dim.isNull()) { + zv::Ref var = nodeProp(expr, PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + zv::Val varStateType = resolveScopeStateType(Z_OBJ_P(var.deref().raw()), native); + if (UNEXPECTED(varStateType.isUndef())) return zv::Val(); + bool isNever; + pt_type_instanceof_ce(varStateType.raw(), pt_ce_never_type, isNever); + if (isNever) { + /* real pricing of an offset read on never yields ErrorType + * (a benevolent mixed), never NeverType - mirror it */ + return pt_type_new_error_type(); + } + zv::Val dimStateType = resolveScopeStateType(Z_OBJ_P(dim.raw()), native); + if (UNEXPECTED(dimStateType.isUndef())) return zv::Val(); + zend_object *varTypeObject = requireObject(varStateType, "getOffsetValueType"); + if (UNEXPECTED(varTypeObject == NULL)) return zv::Val(); + return pt_type_op(varTypeObject, PT_OP_GET_OFFSET_VALUE_TYPE, 1, dimStateType.raw()); + } + } + + bool isPropertyFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_PROPERTY_FETCH, isPropertyFetch))) return zv::Val(); + if (isPropertyFetch) { + bool nameIsIdentifier; + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_IDENTIFIER, nameIsIdentifier))) return zv::Val(); + if (nameIsIdentifier) { + zv::Val propertyReflection = memberReflectionOfFetch(expr, native, PT_LC("getinstancepropertyreflection")); + if (UNEXPECTED(propertyReflection.isUndef())) return zv::Val(); + return propertyStateType(propertyReflection, native); + } + } + + bool isStaticPropertyFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_STATIC_PROPERTY_FETCH, isStaticPropertyFetch))) return zv::Val(); + if (isStaticPropertyFetch) { + bool nameIsVarLike; + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_VAR_LIKE_IDENTIFIER, nameIsVarLike))) return zv::Val(); + if (nameIsVarLike) { + zv::Ref classNode = nodeProp(expr, PT_LC("class")); + if (UNEXPECTED(classNode.raw() == NULL)) return zv::Val(); + classNode = classNode.deref(); + bool classIsName; + if (UNEXPECTED(!isInstance(classNode, PT_CLASS_NAME, classIsName))) return zv::Val(); + zv::Val fetchedOnType; + if (classIsName) { + fetchedOnType = thisResolveTypeByName(classNode.raw()); + } else { + if (UNEXPECTED(!classNode.isObject())) { + zend_type_error("PHPStan\\Analyser\\MutatingScope::resolveScopeStateType(): Argument #1 ($expr) must be of type PhpParser\\Node\\Expr, %s given", zend_zval_value_name(classNode.raw())); + return zv::Val(); + } + zv::Val classStateType = resolveScopeStateType(classNode.asObject(), native); + if (UNEXPECTED(classStateType.isUndef())) return zv::Val(); + zv::Val withoutNull = pt_type_combinator_remove_null(classStateType.raw()); + if (UNEXPECTED(withoutNull.isUndef())) return zv::Val(); + zend_object *withoutNullObject = requireObject(withoutNull, "getObjectTypeOrClassStringObjectType"); + if (UNEXPECTED(withoutNullObject == NULL)) return zv::Val(); + fetchedOnType = pt_type_call(withoutNullObject, PT_LC("getobjecttypeorclassstringobjecttype"), 0, NULL); + } + if (UNEXPECTED(fetchedOnType.isUndef())) return zv::Val(); + zv::Val nameString = memberNameString(expr); + if (UNEXPECTED(nameString.isUndef())) return zv::Val(); + zv::Args args{fetchedOnType.raw(), nameString.raw()}; + zv::Val propertyReflection = thisCallByName(PT_LC("getstaticpropertyreflection"), 2, args); + if (UNEXPECTED(propertyReflection.isUndef())) return zv::Val(); + return propertyStateType(propertyReflection, native); + } + } + + /* a nullsafe link of a chain being ensured non-null ahead of its + * walk: the plain link's state on the receiver's state, plus the + * short-circuit null */ + bool isNullsafePropertyFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_NULLSAFE_PROPERTY_FETCH, isNullsafePropertyFetch))) return zv::Val(); + if (isNullsafePropertyFetch) { + bool nameIsIdentifier; + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_IDENTIFIER, nameIsIdentifier))) return zv::Val(); + if (nameIsIdentifier) { + zv::Ref var = nodeProp(expr, PT_LC("var")); + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(var.raw() == NULL || name.raw() == NULL)) return zv::Val(); + zv::Args args{var.deref().raw(), name.deref().raw()}; + zv::Val plainFetch = pt_type_new(PT_CLASS_PROPERTY_FETCH, 2, args); + if (UNEXPECTED(plainFetch.isUndef())) return zv::Val(); + zv::Val plainType = resolveScopeStateType(Z_OBJ_P(plainFetch.raw()), native); + if (UNEXPECTED(plainType.isUndef())) return zv::Val(); + return pt_type_combinator_add_null(plainType.raw()); + } + } + bool isNullsafeMethodCall; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_NULLSAFE_METHOD_CALL, isNullsafeMethodCall))) return zv::Val(); + if (isNullsafeMethodCall) { + bool nameIsIdentifier; + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_IDENTIFIER, nameIsIdentifier))) return zv::Val(); + if (nameIsIdentifier) { + bool argumentLess; + if (UNEXPECTED(!isArgumentLessPlainCall(expr, argumentLess))) return zv::Val(); + if (argumentLess) { + /* new Expr\MethodCall($expr->var, $expr->name, attributes: $expr->getAttributes()) */ + zv::Ref var = nodeProp(expr, PT_LC("var")); + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(var.raw() == NULL || name.raw() == NULL)) return zv::Val(); + zv::Val attributes = pt_type_call(expr, PT_LC("getattributes"), 0, NULL); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + zval args[4]; + ZVAL_COPY_VALUE(&args[0], var.deref().raw()); + ZVAL_COPY_VALUE(&args[1], name.deref().raw()); + ZVAL_EMPTY_ARRAY(&args[2]); + ZVAL_COPY_VALUE(&args[3], attributes.raw()); + zv::Val plainCall = pt_type_new(PT_CLASS_METHOD_CALL, 4, args); + if (UNEXPECTED(plainCall.isUndef())) return zv::Val(); + zv::Val plainType = resolveScopeStateType(Z_OBJ_P(plainCall.raw()), native); + if (UNEXPECTED(plainType.isUndef())) return zv::Val(); + return pt_type_combinator_add_null(plainType.raw()); + } + } + } + + /* an argument-less instance call - the shape @phpstan-assert + * subjects take: its declared return type on the receiver's state + * is the narrowing base, derived from reflection instead of walking + * the synthetic node. A call the walk did store answers from that + * result. */ + bool isMethodCall; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_METHOD_CALL, isMethodCall))) return zv::Val(); + if (isMethodCall) { + bool nameIsIdentifier; + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_IDENTIFIER, nameIsIdentifier))) return zv::Val(); + if (nameIsIdentifier) { + bool argumentLess; + if (UNEXPECTED(!isArgumentLessPlainCall(expr, argumentLess))) return zv::Val(); + if (argumentLess) { + zv::Val storage = currentStorage(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + if (!storage.isNull()) { + zv::Val stored = storageFind(storage, expr); + if (UNEXPECTED(stored.isUndef())) return zv::Val(); + if (!stored.isNull()) return native ? thisGetNativeType(&exprZv) : thisGetType(&exprZv); + } + + zv::Val methodReflection = memberReflectionOfFetch(expr, native, PT_LC("getmethodreflection")); + if (UNEXPECTED(methodReflection.isUndef())) return zv::Val(); + if (methodReflection.isNull()) return pt_type_new_error_type(); + zend_object *methodObject = requireObject(methodReflection, "getVariants"); + if (UNEXPECTED(methodObject == NULL)) return zv::Val(); + + /* resolved against the (empty) argument list so a template + * inferred from an omitted parameter's default resolves the + * way a walk resolves it */ + zv::Val variants = pt_type_call(methodObject, PT_LC("getvariants"), 0, NULL); + if (UNEXPECTED(variants.isUndef())) return zv::Val(); + zv::Val namedArgumentsVariants = pt_type_call(methodObject, PT_LC("getnamedargumentsvariants"), 0, NULL); + if (UNEXPECTED(namedArgumentsVariants.isUndef())) return zv::Val(); + zval selectArgs[4]; + ZVAL_OBJ(&selectArgs[0], self); + ZVAL_EMPTY_ARRAY(&selectArgs[1]); + ZVAL_COPY_VALUE(&selectArgs[2], variants.raw()); + ZVAL_COPY_VALUE(&selectArgs[3], namedArgumentsVariants.raw()); + zv::Val variant = pt_type_call_static(PT_CLASS_PARAMETERS_ACCEPTOR_SELECTOR, PT_LC("selectfromargs"), 4, selectArgs); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + + if (native) { + bool isExtended; + if (UNEXPECTED(!isInstance(variant.ref(), PT_CLASS_EXTENDED_PARAMETERS_ACCEPTOR, isExtended))) return zv::Val(); + if (isExtended) return pt_type_call(Z_OBJ_P(variant.raw()), PT_LC("getnativereturntype"), 0, NULL); + } + zv::Args callArgs{variant.raw(), self, expr, true}; + return pt_type_call_static(PT_CLASS_TEMPLATE_ARGUMENT_FRAME, PT_LC("returntypeofcall"), 4, callArgs); + } + } + } + + /* position-independent constant expressions are priced without + * walking the node */ + bool isConstantExpr = false; + static const int constantClasses[] = { PT_CLASS_SCALAR_STRING, PT_CLASS_SCALAR_INT, PT_CLASS_SCALAR_FLOAT }; + for (int classIdx : constantClasses) { + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), classIdx, isConstantExpr))) return zv::Val(); + if (isConstantExpr) break; + } + if (!isConstantExpr) { + bool isClassConstFetch; + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_CLASS_CONST_FETCH, isClassConstFetch))) return zv::Val(); + if (isClassConstFetch) { + zv::Ref classNode = nodeProp(expr, PT_LC("class")); + if (UNEXPECTED(classNode.raw() == NULL)) return zv::Val(); + bool classIsName; + if (UNEXPECTED(!isInstance(classNode.deref(), PT_CLASS_NAME, classIsName))) return zv::Val(); + if (classIsName) { + if (UNEXPECTED(!nodeNameIs(expr, PT_CLASS_IDENTIFIER, isConstantExpr))) return zv::Val(); + } + } + } + if (!isConstantExpr) { + if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_CONST_FETCH, isConstantExpr))) return zv::Val(); + } + if (isConstantExpr) { + zv::Ref resolver = slot(PT_MS_PROP_INITIALIZER_EXPR_TYPE_RESOLVER); + if (UNEXPECTED(!resolver.isObject())) return uninitializedProperty("initializerExprTypeResolver"); + zval selfZv; + ZVAL_OBJ(&selfZv, self); + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromscope"), 1, &selfZv); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Args args{expr, context.raw()}; + return pt_type_call(resolver.asObject(), PT_LC("gettype"), 2, args); + } + + /* genuinely non-narrowed expressions (calls, ...) have no + * variable-callback hazard, so read them normally */ + return native ? thisGetNativeType(&exprZv) : thisGetType(&exprZv); + } + + /* $this->($this->resolveScopeStateType($expr->var, $native), $expr->name->toString()) */ + zv::Val memberReflectionOfFetch(zend_object *expr, bool native, const char *lcname, size_t len) + { + zv::Ref var = nodeProp(expr, PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL)) return zv::Val(); + var = var.deref(); + if (UNEXPECTED(!var.isObject())) { + zend_type_error("PHPStan\\Analyser\\MutatingScope::resolveScopeStateType(): Argument #1 ($expr) must be of type PhpParser\\Node\\Expr, %s given", zend_zval_value_name(var.raw())); + return zv::Val(); + } + zv::Val varStateType = resolveScopeStateType(var.asObject(), native); + if (UNEXPECTED(varStateType.isUndef())) return zv::Val(); + zv::Val nameString = memberNameString(expr); + if (UNEXPECTED(nameString.isUndef())) return zv::Val(); + zv::Args args{varStateType.raw(), nameString.raw()}; + return thisCallByName(lcname, len, 2, args); + } + + /* $expr->name->toString() */ + static zv::Val memberNameString(zend_object *expr) + { + zv::Ref name = nodeProp(expr, PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) return zv::Val(); + name = name.deref(); + if (UNEXPECTED(!name.isObject())) { + zend_throw_error(NULL, "Call to a member function toString() on %s", zend_zval_value_name(name.raw())); + return zv::Val(); + } + return pt_type_call(name.asObject(), PT_LC("tostring"), 0, NULL); + } + + /* the (readable / native) type of a property reflection lookup, an + * ErrorType for none */ + static zv::Val propertyStateType(zv::Val &propertyReflection, bool native) + { + if (propertyReflection.isNull()) return pt_type_new_error_type(); + zend_object *reflection = requireObject(propertyReflection, native ? "hasNativeType" : "getReadableType"); + if (UNEXPECTED(reflection == NULL)) return zv::Val(); + if (native) { + zv::Val hasNativeType = pt_type_call(reflection, PT_LC("hasnativetype"), 0, NULL); + if (UNEXPECTED(hasNativeType.isUndef())) return zv::Val(); + if (zend_is_true(hasNativeType.raw())) return pt_type_call(reflection, PT_LC("getnativetype"), 0, NULL); + return pt_type_new_mixed_type(); + } + return pt_type_call(reflection, PT_LC("getreadabletype"), 0, NULL); + } + + /* }}} */ + + bool isInFirstLevelStatement() const { return slotBool(PT_MS_PROP_IN_FIRST_LEVEL_STATEMENT); } + + /* private; null when no fetch is tracked, UNDEF = pending exception */ + zv::Val getGlobalConstantType(zend_object *name) + { + /* the namespace only takes part for a name that is not already fully qualified */ + zv::Val isFullyQualified = pt_type_call(name, PT_LC("isfullyqualified"), 0, NULL); + if (UNEXPECTED(isFullyQualified.isUndef())) return zv::Val(); + zv::Val ns = zv::Val::null(); + if (!zend_is_true(isFullyQualified.raw())) { + ns = thisGetNamespace(); + if (UNEXPECTED(ns.isUndef())) return zv::Val(); + } + + zv::Val nameString = pt_type_call(name, PT_LC("tostring"), 0, NULL); + if (UNEXPECTED(nameString.isUndef())) return zv::Val(); + if (UNEXPECTED(!nameString.ref().isString())) { + zend_throw_error(NULL, "phpstan_turbo: Name::toString() did not return a string"); + return zv::Val(); + } + + zv::Str cacheKey = globalConstantFetchCacheKey(name->ce->name, nameString.ref().asString(), ns.ref()); + zval *keysTable = globalConstantFetchKeysTable(); + if (UNEXPECTED(keysTable == NULL)) return zv::Val(); + + zv::Val exprStrings; + zval *memo = zend_hash_find(Z_ARRVAL_P(keysTable), cacheKey.get()); + if (memo != NULL) { + zv::Ref memoRef = zv::Ref(memo).deref(); + if (UNEXPECTED(!memoRef.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: globalConstantFetchKeys entry is not an array"); + return zv::Val(); + } + exprStrings = zv::Val::copyOf(memoRef); + } else { + zv::Val fetches[3]; + uint32_t count = 0; + if (UNEXPECTED(!createGlobalConstantFetches(name, nameString.ref().asString(), ns.ref(), fetches, count))) return zv::Val(); + + zv::Arr keys = zv::Arr::create(count); + for (uint32_t i = 0; i < count; i++) { + zv::Val key = thisGetNodeKey(fetches[i].raw()); + if (UNEXPECTED(key.isUndef())) return zv::Val(); + keys.push(std::move(key)); + } + + if (zend_hash_num_elements(Z_ARRVAL_P(keysTable)) < PT_MS_GLOBAL_CONSTANT_FETCH_KEYS_LIMIT) { + SEPARATE_ARRAY(keysTable); + zval stored; + ZVAL_COPY(&stored, keys.raw()); + zend_hash_update(Z_ARRVAL_P(keysTable), cacheKey.get(), &stored); + } + exprStrings = std::move(keys); + } + + zv::Ref expressionTypes = slot(PT_MS_PROP_EXPRESSION_TYPES); + if (UNEXPECTED(!expressionTypes.isArray())) return uninitializedProperty("expressionTypes"); + + uint32_t i = 0; + for (auto entry : zv::ArrRef(exprStrings.raw())) { + /* hasExpressionType() looks at nothing but the key for a node that is + * not a Variable, and the key is exactly what is memoized above */ + zv::Ref exprString = entry.value().deref(); + if (UNEXPECTED(!exprString.isString())) { + zend_throw_error(NULL, "phpstan_turbo: globalConstantFetchKeys entry is not a string"); + return zv::Val(); + } + zval *holder = zend_symtable_find(Z_ARRVAL_P(expressionTypes.raw()), exprString.asString()); + if (holder != NULL) { + zend_long certainty = holderCertainty(zv::Ref(holder).deref()); + if (UNEXPECTED(certainty < 0)) return zv::Val(); + if (certainty == PT_TRI_YES) { + zv::Val fetches[3]; + uint32_t count = 0; + if (UNEXPECTED(!createGlobalConstantFetches(name, nameString.ref().asString(), ns.ref(), fetches, count))) return zv::Val(); + return thisGetType(fetches[i].raw()); + } + } + i++; + } + + return zv::Val::null(); + } + + /* + * The nodes a global constant name is looked up as, in priority order: the + * current namespace's constant, the global one, then the name as written. + * + * Fresh nodes on every call by design - everything that keys on node identity + * (ExpressionResultStorage, NodeScopeResolver's processed-node guards) must + * keep seeing a node of its own; only the keys they print as are memoized. + * + * false = pending exception + */ + [[nodiscard]] static bool createGlobalConstantFetches(zend_object *name, zend_string *nameString, zv::Ref ns, zv::Val (&fetches)[3], uint32_t &count) + { + count = 0; + + if (!ns.isNull()) { + zv::Arr parts = zv::Arr::create(2); + parts.push(ns); + parts.push(zv::Val::string(nameString)); + zv::Val fetch = newConstFetchOfFullyQualified(parts.raw()); + if (UNEXPECTED(fetch.isUndef())) return false; + fetches[count++] = std::move(fetch); + } + + zval nameStringZv; + ZVAL_STR(&nameStringZv, nameString); + zv::Val fetch = newConstFetchOfFullyQualified(&nameStringZv); + if (UNEXPECTED(fetch.isUndef())) return false; + fetches[count++] = std::move(fetch); + + zval nameZv; + ZVAL_OBJ(&nameZv, name); + fetch = pt_type_new(PT_CLASS_CONST_FETCH, 1, &nameZv); + if (UNEXPECTED(fetch.isUndef())) return false; + fetches[count++] = std::move(fetch); + + return true; + } + + /* the twin's get_class($name) . "\0" . $name->toString() . "\0" . ($namespace ?? "\0") */ + static zv::Str globalConstantFetchCacheKey(zend_string *nameClass, zend_string *nameString, zv::Ref ns) + { + smart_str key = {}; + smart_str_append(&key, nameClass); + smart_str_appendc(&key, '\0'); + smart_str_append(&key, nameString); + smart_str_appendc(&key, '\0'); + if (ns.isNull()) { + smart_str_appendc(&key, '\0'); + } else { + smart_str_append(&key, ns.asString()); + } + return zv::Str::adopt(smart_str_extract(&key)); + } + + /* MutatingScope::$globalConstantFetchKeys - the same static property the twin + * memoizes into; NULL = pending exception */ + [[nodiscard]] static zval *globalConstantFetchKeysTable() + { + if (UNEXPECTED(pt_ce_mutating_scope == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: MutatingScope class entry is not available"); + return NULL; + } + zval *table = zend_read_static_property(pt_ce_mutating_scope, PT_LC("globalConstantFetchKeys"), 0); + if (UNEXPECTED(table == NULL)) return NULL; + ZVAL_DEREF(table); + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: MutatingScope::$globalConstantFetchKeys is not an array"); + return NULL; + } + return table; + } + + /* new ConstFetch(new FullyQualified($nameOrParts)) */ + static zv::Val newConstFetchOfFullyQualified(zval *nameOrParts) + { + zv::Val fullyQualified = pt_type_new(PT_CLASS_FULLY_QUALIFIED, 1, nameOrParts); + if (UNEXPECTED(fullyQualified.isUndef())) return zv::Val(); + return pt_type_new(PT_CLASS_CONST_FETCH, 1, fullyQualified.raw()); + } + + /* }}} */ + +private: + zend_object *self; + zval selfZval; +}; + +} // namespace phpstanturbo + +using phpstanturbo::MutatingScope; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#define PT_THIS MutatingScope(Z_OBJ_P(ZEND_THIS)) + +/* a `bool method(bool &out)` body into return_value */ +#define PT_MS_RETURN_BOOL(expr) \ + do { \ + bool out_; \ + if (UNEXPECTED(!(expr))) { \ + RETURN_THROWS(); \ + } \ + RETURN_BOOL(out_); \ + } while (0) + +namespace pt_ms { +/* the twin's parameter and return class names (persistent literals) */ +inline constexpr const char *self = "PHPStan\\Analyser\\MutatingScope"; +inline constexpr const char *container = "PHPStan\\DependencyInjection\\Container"; +inline constexpr const char *internalScopeFactory = "PHPStan\\Analyser\\InternalScopeFactory"; +inline constexpr const char *reflectionProvider = "PHPStan\\Reflection\\ReflectionProvider"; +inline constexpr const char *initializerExprTypeResolver = "PHPStan\\Reflection\\InitializerExprTypeResolver"; +inline constexpr const char *extensionsCollection = "PHPStan\\DependencyInjection\\ExtensionsCollection"; +inline constexpr const char *exprPrinter = "PHPStan\\Node\\Printer\\ExprPrinter"; +inline constexpr const char *typeSpecifier = "PHPStan\\Analyser\\TypeSpecifier"; +inline constexpr const char *propertyReflectionFinder = "PHPStan\\Rules\\Properties\\PropertyReflectionFinder"; +inline constexpr const char *parser = "PHPStan\\Parser\\Parser"; +inline constexpr const char *constantResolver = "PHPStan\\Analyser\\ConstantResolver"; +inline constexpr const char *expressionResultStorageStack = "PHPStan\\Analyser\\ExpressionResultStorageStack"; +inline constexpr const char *scopeContext = "PHPStan\\Analyser\\ScopeContext"; +inline constexpr const char *phpVersion = "PHPStan\\Php\\PhpVersion"; +inline constexpr const char *attributeReflectionFactory = "PHPStan\\Reflection\\AttributeReflectionFactory"; +inline constexpr const char *configuredPhpVersionRangeHelper = "PHPStan\\Php\\ConfiguredPhpVersionRangeHelper"; +inline constexpr const char *phpFunctionFromParserNodeReflection = "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection"; +inline constexpr const char *closureType = "PHPStan\\Type\\ClosureType"; +inline constexpr const char *templateArgumentFrame = "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame"; +inline constexpr const char *templateArgumentConstraints = "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints"; +inline constexpr const char *classReflection = "PHPStan\\Reflection\\ClassReflection"; +inline constexpr const char *expr = "PhpParser\\Node\\Expr"; +inline constexpr const char *name = "PhpParser\\Node\\Name"; +inline constexpr const char *trinaryLogic = "PHPStan\\TrinaryLogic"; +inline constexpr const char *type = "PHPStan\\Type\\Type"; +inline constexpr const char *specifiedTypes = "PHPStan\\Analyser\\SpecifiedTypes"; +inline constexpr const char *parameterReflection = "PHPStan\\Reflection\\ParameterReflection"; +inline constexpr const char *phpVersions = "PHPStan\\Php\\PhpVersions"; +inline constexpr const char *param = "PhpParser\\Node\\Param"; +inline constexpr const char *classMethodNode = "PhpParser\\Node\\Stmt\\ClassMethod"; +inline constexpr const char *functionNode = "PhpParser\\Node\\Stmt\\Function_"; +inline constexpr const char *propertyHook = "PhpParser\\Node\\PropertyHook"; +inline constexpr const char *identifierOrNameOrComplexType = "PhpParser\\Node\\Identifier|PhpParser\\Node\\Name|PhpParser\\Node\\ComplexType"; +inline constexpr const char *templateTypeMap = "PHPStan\\Type\\Generic\\TemplateTypeMap"; +inline constexpr const char *assertions = "PHPStan\\Reflection\\Assertions"; +inline constexpr const char *resolvedPhpDocBlock = "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"; +inline constexpr const char *closureNode = "PhpParser\\Node\\Expr\\Closure"; +inline constexpr const char *arrowFunctionNode = "PhpParser\\Node\\Expr\\ArrowFunction"; +inline constexpr const char *node = "PhpParser\\Node"; +inline constexpr const char *propertyReflection = "PHPStan\\Reflection\\PropertyReflection"; +inline constexpr const char *methodReflection = "PHPStan\\Reflection\\MethodReflection"; + +/* `?bool $x` — reg::boolArg() is never nullable */ +constexpr reg::Arg nullableBool(const char *name) +{ + return { name, MAY_BE_BOOL | MAY_BE_NULL | reg::detail::flagBits(false, false), nullptr }; +} + +inline constexpr reg::Arg returnsSelf = reg::obj("", self); +} // namespace pt_ms + +/* the handlers the $this-dispatch fast paths identify */ +static void ZEND_FASTCALL msGetFile(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getFile()); +} + +static void ZEND_FASTCALL msIsDeclareStrictTypes(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(PT_THIS.isDeclareStrictTypes()); +} + +static void ZEND_FASTCALL msIsReadonlyPropertyFetch(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *expr; + bool allowOnlyOnThis; + zend_class_entry *propertyFetchCe = pt_class(PT_CLASS_PROPERTY_FETCH); + if (UNEXPECTED(propertyFetchCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(expr, propertyFetchCe) + Z_PARAM_BOOL(allowOnlyOnThis) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_RETURN_BOOL(PT_THIS.isReadonlyPropertyFetch(Z_OBJ_P(expr), allowOnlyOnThis, out_)); +} + +static void ZEND_FASTCALL msIsInClass(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_MS_RETURN_BOOL(PT_THIS.isInClass(out_)); +} + +static void ZEND_FASTCALL msGetClassReflection(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getClassReflection()); +} + +static void ZEND_FASTCALL msGetFunction(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getFunction()); +} + +static void ZEND_FASTCALL msGetNamespace(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getNamespace()); +} + +static void ZEND_FASTCALL msCanAnyVariableExist(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_MS_RETURN_BOOL(PT_THIS.canAnyVariableExist(out_)); +} + +static void ZEND_FASTCALL msHasVariableType(INTERNAL_FUNCTION_PARAMETERS) +{ + zend_string *variableName; + if (!zp::parse(execute_data, variableName)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.hasVariableType(variableName)); +} + +static void ZEND_FASTCALL msIsInAnonymousFunction(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(PT_THIS.isInAnonymousFunction()); +} + +static void ZEND_FASTCALL msGetNodeKey(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *node; + if (!zp::parse(execute_data, node)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getNodeKey(Z_OBJ_P(node))); +} + +static void ZEND_FASTCALL msHasExpressionType(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *node; + if (!zp::parse(execute_data, node)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.hasExpressionType(Z_OBJ_P(node))); +} + +static void ZEND_FASTCALL msIsInFirstLevelStatement(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(PT_THIS.isInFirstLevelStatement()); +} + +static void ZEND_FASTCALL msToWalkScope(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.toWalkScope()); +} + +static void ZEND_FASTCALL msGetVariableType(INTERNAL_FUNCTION_PARAMETERS) +{ + zend_string *variableName; + if (!zp::parse(execute_data, variableName)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getVariableType(variableName)); +} + +/* the one Expr argument of getType() & co. */ +/* a nullable object / string / array parameter as a zval the handle class takes */ +#define PT_MS_OBJ_ZVAL(name) \ + zval name##Zv; \ + if ((name) == NULL) { \ + ZVAL_NULL(&name##Zv); \ + (name) = &name##Zv; \ + } + +#define PT_MS_STR_ZVAL(name) \ + zval name##Zv; \ + if ((name) == NULL) { \ + ZVAL_NULL(&name##Zv); \ + } else { \ + ZVAL_STR(&name##Zv, (name)); \ + } + +#define PT_MS_ARRAY_ZVAL(name) \ + zval name##Zv; \ + if ((name) == NULL) { \ + ZVAL_EMPTY_ARRAY(&name##Zv); \ + (name) = &name##Zv; \ + } + +/* The parameter is declared as PHPStan\Reflection\ClassReflection, so the + * check resolves that NAME: in production it is the shadowing class itself, + * under the prefixed differential activation the PHP twin (the native class + * lives beside it as PHPStanTurbo\ClassReflection). Resolved once and + * cached; the class is loaded by the time a scope is entered. */ +static zend_class_entry *msClassReflectionCe() +{ + static zend_class_entry *cached = NULL; + if (EXPECTED(cached != NULL)) return cached; + zend_string *name = zend_string_init("PHPStan\\Reflection\\ClassReflection", sizeof("PHPStan\\Reflection\\ClassReflection") - 1, 0); + cached = zend_lookup_class_ex(name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_string_release(name); + return cached; +} + +#define PT_MS_PARSE_CLASS_REFLECTION(var) \ + zval *var; \ + do { \ + zend_class_entry *classReflectionCe_ = msClassReflectionCe(); \ + if (UNEXPECTED(classReflectionCe_ == NULL)) { \ + zend_throw_error(NULL, "phpstan_turbo: PHPStan\\Reflection\\ClassReflection is not loaded"); \ + RETURN_THROWS(); \ + } \ + ZEND_PARSE_PARAMETERS_START(1, 1) \ + Z_PARAM_OBJECT_OF_CLASS(var, classReflectionCe_) \ + ZEND_PARSE_PARAMETERS_END(); \ + } while (0) + +#define PT_MS_PARSE_EXPR(var) \ + zval *var; \ + do { \ + zend_class_entry *exprCe_ = pt_class(PT_CLASS_EXPR); \ + if (UNEXPECTED(exprCe_ == NULL)) { \ + RETURN_THROWS(); \ + } \ + ZEND_PARSE_PARAMETERS_START(1, 1) \ + Z_PARAM_OBJECT_OF_CLASS(var, exprCe_) \ + ZEND_PARSE_PARAMETERS_END(); \ + } while (0) + +static void ZEND_FASTCALL msGetType(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_EXPR(node); + PT_RETURN_VAL(PT_THIS.getType(Z_OBJ_P(node))); +} + +static void ZEND_FASTCALL msDuplicateWith(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *expressionTypes, *nativeExpressionTypes, *conditionalExpressions, *currentlyAssignedExpressions, *currentlyAllowedUndefinedExpressions, *inFunctionCallsStack; + bool inFirstLevelStatement, afterExtractCall; + ZEND_PARSE_PARAMETERS_START(8, 8) + Z_PARAM_ARRAY(expressionTypes) + Z_PARAM_ARRAY(nativeExpressionTypes) + Z_PARAM_ARRAY(conditionalExpressions) + Z_PARAM_ARRAY(currentlyAssignedExpressions) + Z_PARAM_ARRAY(currentlyAllowedUndefinedExpressions) + Z_PARAM_ARRAY(inFunctionCallsStack) + Z_PARAM_BOOL(inFirstLevelStatement) + Z_PARAM_BOOL(afterExtractCall) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.duplicateWith(expressionTypes, nativeExpressionTypes, conditionalExpressions, currentlyAssignedExpressions, currentlyAllowedUndefinedExpressions, inFunctionCallsStack, inFirstLevelStatement, afterExtractCall)); +} + +static void ZEND_FASTCALL msObtainResultForNode(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_EXPR(node); + PT_RETURN_VAL(PT_THIS.obtainResultForNode(Z_OBJ_P(node))); +} + +static void ZEND_FASTCALL msWithTemplateArgumentConstraints(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *constraints; + if (!zp::parse(execute_data, constraints)) RETURN_THROWS(); + zval nullZv; + if (constraints == NULL) { + ZVAL_NULL(&nullZv); + constraints = &nullZv; + } + PT_RETURN_VAL(PT_THIS.withTemplateArgumentConstraints(constraints)); +} + +static void ZEND_FASTCALL msWithoutMemoizedTypes(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.withoutMemoizedTypes()); +} + +static void ZEND_FASTCALL msGetNativeType(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.getNativeType(expr)); +} + +static void ZEND_FASTCALL msDoNotTreatPhpDocTypesAsCertain(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.doNotTreatPhpDocTypesAsCertain()); +} + +/* the one Name argument of resolveName() / resolveTypeByName() */ +#define PT_MS_PARSE_NAME(var) \ + zval *var; \ + do { \ + zend_class_entry *nameCe_ = pt_class(PT_CLASS_NAME); \ + if (UNEXPECTED(nameCe_ == NULL)) { \ + RETURN_THROWS(); \ + } \ + ZEND_PARSE_PARAMETERS_START(1, 1) \ + Z_PARAM_OBJECT_OF_CLASS(var, nameCe_) \ + ZEND_PARSE_PARAMETERS_END(); \ + } while (0) + +static void ZEND_FASTCALL msResolveName(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_NAME(name); + PT_RETURN_VAL(PT_THIS.resolveName(Z_OBJ_P(name))); +} + +static void ZEND_FASTCALL msResolveTypeByName(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_NAME(name); + PT_RETURN_VAL(PT_THIS.resolveTypeByName(Z_OBJ_P(name))); +} + +static void ZEND_FASTCALL msGetParentScope(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getParentScope()); +} + +static void ZEND_FASTCALL msPushInFunctionCall(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *reflection, *parameter; + bool rememberTypes; + if (!zp::parse(execute_data, reflection, parameter, rememberTypes)) RETURN_THROWS(); + ZVAL_DEREF(reflection); + zval nullZv; + if (parameter == NULL) { + ZVAL_NULL(&nullZv); + parameter = &nullZv; + } + PT_RETURN_VAL(PT_THIS.pushInFunctionCall(reflection, parameter, rememberTypes)); +} + +static void ZEND_FASTCALL msPopInFunctionCall(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.popInFunctionCall()); +} + +static void ZEND_FASTCALL msGetPhpVersion(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getPhpVersion()); +} + +static void ZEND_FASTCALL msIsParameterValueNullable(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *parameter; + zend_class_entry *paramCe = pt_class(PT_CLASS_PARAM); + if (UNEXPECTED(paramCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(parameter, paramCe) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_RETURN_BOOL(PT_THIS.isParameterValueNullable(Z_OBJ_P(parameter), out_)); +} + +static void ZEND_FASTCALL msGetFunctionType(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *type; + bool isNullable, isVariadic; + if (!zp::parse(execute_data, type, isNullable, isVariadic)) RETURN_THROWS(); + ZVAL_DEREF(type); + PT_RETURN_VAL(PT_THIS.getFunctionType(type, isNullable, isVariadic)); +} + +static void ZEND_FASTCALL msGetCurrentExpressionResultStorage(INTERNAL_FUNCTION_PARAMETERS) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PT_RETURN_VAL(PT_THIS.getCurrentExpressionResultStorage()); +} + +/* the ($functionLike, ?array, ?array) argument list of the two + * *WithoutReflection() entries */ +#define PT_MS_PARSE_FUNCTION_ENTRY(var, classIdx) \ + zval *var, *callableParameters = NULL, *nativeCallableParameters = NULL; \ + do { \ + zend_class_entry *ce_ = pt_class(classIdx); \ + if (UNEXPECTED(ce_ == NULL)) { \ + RETURN_THROWS(); \ + } \ + ZEND_PARSE_PARAMETERS_START(3, 3) \ + Z_PARAM_OBJECT_OF_CLASS(var, ce_) \ + Z_PARAM_ARRAY_OR_NULL(callableParameters) \ + Z_PARAM_ARRAY_OR_NULL(nativeCallableParameters) \ + ZEND_PARSE_PARAMETERS_END(); \ + } while (0) + +static void ZEND_FASTCALL msEnterAnonymousFunctionWithoutReflection(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_FUNCTION_ENTRY(closure, PT_CLASS_CLOSURE_EXPR); + PT_RETURN_VAL(PT_THIS.enterAnonymousFunctionWithoutReflection(Z_OBJ_P(closure), callableParameters, nativeCallableParameters)); +} + +static void ZEND_FASTCALL msEnterArrowFunctionWithoutReflection(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_FUNCTION_ENTRY(arrowFunction, PT_CLASS_ARROW_FUNCTION); + PT_RETURN_VAL(PT_THIS.enterArrowFunctionWithoutReflection(Z_OBJ_P(arrowFunction), callableParameters, nativeCallableParameters)); +} + +static void ZEND_FASTCALL msAssignVariable(INTERNAL_FUNCTION_PARAMETERS) +{ + zend_string *variableName; + zval *type, *nativeType, *certainty, *intertwinedPropagatedFrom = NULL; + if (!zp::parse>(execute_data, variableName, type, nativeType, certainty, intertwinedPropagatedFrom)) RETURN_THROWS(); + PT_MS_ARRAY_ZVAL(intertwinedPropagatedFrom); + PT_RETURN_VAL(PT_THIS.assignVariable(variableName, type, nativeType, certainty, intertwinedPropagatedFrom)); +} + +static void ZEND_FASTCALL msAssignExpression(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *expr, *type, *nativeType; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + Z_PARAM_OBJECT(type) + Z_PARAM_OBJECT(nativeType) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.assignExpression(Z_OBJ_P(expr), type, nativeType)); +} + +static void ZEND_FASTCALL msSpecifyExpressionType(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *expr, *type, *nativeType, *certainty; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(4, 4) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + Z_PARAM_OBJECT(type) + Z_PARAM_OBJECT(nativeType) + Z_PARAM_OBJECT(certainty) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.specifyExpressionType(Z_OBJ_P(expr), type, nativeType, certainty)); +} + +static void ZEND_FASTCALL msInvalidateExpression(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *expressionToInvalidate, *invalidatingClass = NULL; + bool requireMoreCharacters = false, keepPropertyFetches = false; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 4) + Z_PARAM_OBJECT_OF_CLASS(expressionToInvalidate, exprCe) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(requireMoreCharacters) + Z_PARAM_OBJECT_OR_NULL(invalidatingClass) + Z_PARAM_BOOL(keepPropertyFetches) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.invalidateExpression(expressionToInvalidate, requireMoreCharacters, invalidatingClass, keepPropertyFetches)); +} + +static void ZEND_FASTCALL msFilterByTruthyValue(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.filterByValue(Z_OBJ_P(expr), true)); +} + +static void ZEND_FASTCALL msFilterByFalseyValue(INTERNAL_FUNCTION_PARAMETERS) +{ + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.filterByValue(Z_OBJ_P(expr), false)); +} + +static void ZEND_FASTCALL msApplySpecifiedTypes(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *specifiedTypes; + if (!zp::parse(execute_data, specifiedTypes)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.applySpecifiedTypes(specifiedTypes)); +} + +static void ZEND_FASTCALL msFilterTypeWithMethod(INTERNAL_FUNCTION_PARAMETERS) +{ + zval *typeWithMethod; + zend_string *methodName; + if (!zp::parse(execute_data, typeWithMethod, methodName)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.filterTypeWithMethod(typeWithMethod, methodName)); +} + +void pt_register_mutating_scope() +{ + using namespace pt_ms; + + reg::Class cls("PHPStan\\Analyser\\MutatingScope"); + /* not final: NodeCallbackScope extends it in PHP (and a third party + * may too), so every method stays dispatched through the object's + * class entry */ + ptdecl::MutatingScope::declareClass(cls); + + /* {{{ the slots, in the twin's declaration order (the PT_MS_PROP_* + * enum): the class-body properties with their defaults, then the + * promoted constructor properties, uninitialized until the + * constructor writes them */ + cls.property("resolvedTypes", ZEND_ACC_PUBLIC, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("nodeCallbackScope", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self"); + cls.property("namespace", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING | MAY_BE_NULL); + cls.property("scopeOutOfFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self"); + cls.property("scopeWithPromotedNativeTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self"); + cls.property("container", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, container); + cls.property("scopeFactory", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, internalScopeFactory); + cls.property("reflectionProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, reflectionProvider); + cls.property("initializerExprTypeResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, initializerExprTypeResolver); + cls.property("expressionTypeResolverExtensions", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, extensionsCollection); + cls.property("exprPrinter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, exprPrinter); + cls.property("typeSpecifier", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, typeSpecifier); + cls.property("propertyReflectionFinder", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, propertyReflectionFinder); + cls.property("parser", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, parser); + cls.property("constantResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, constantResolver); + cls.property("expressionResultStorageStack", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, expressionResultStorageStack); + cls.property("context", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, scopeContext); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, phpVersion); + cls.property("attributeReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, attributeReflectionFactory); + cls.property("configuredPhpVersionRangeHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, configuredPhpVersionRangeHelper); + cls.property("nodeCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Null, 0); + cls.property("declareStrictTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("function", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, phpFunctionFromParserNodeReflection); + cls.property("expressionTypes", ZEND_ACC_PUBLIC, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("nativeExpressionTypes", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("conditionalExpressions", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("inClosureBindScopeClasses", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("anonymousFunctionReflection", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, closureType); + cls.property("inFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("currentlyAssignedExpressions", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("currentlyAllowedUndefinedExpressions", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("inFunctionCallsStack", ZEND_ACC_PUBLIC, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("afterExtractCall", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("parentScope", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "self"); + cls.property("nativeTypesPromoted", ZEND_ACC_PUBLIC, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("templateArgumentFrame", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_NULL, templateArgumentFrame); + cls.property("templateArgumentConstraints", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_NULL, templateArgumentConstraints); + /* the twin's `private static array $globalConstantFetchKeys = []`, which + * getGlobalConstantType() memoizes into on both sides. Declared after the + * instance properties even though the twin declares it before them: a + * static takes no OBJ_PROP_NUM slot, and keeping it out of the run above + * keeps that run a literal transcript of the PT_MS_PROP_* enum */ + cls.privateStaticTypedArrayPropertyDefaultEmpty("globalConstantFetchKeys"); + /* }}} */ + + cls.method("__construct", reg::Public, 15, { + reg::obj("container", container), + reg::obj("scopeFactory", internalScopeFactory), + reg::obj("reflectionProvider", reflectionProvider), + reg::obj("initializerExprTypeResolver", initializerExprTypeResolver), + reg::obj("expressionTypeResolverExtensions", extensionsCollection), + reg::obj("exprPrinter", exprPrinter), + reg::obj("typeSpecifier", typeSpecifier), + reg::obj("propertyReflectionFinder", propertyReflectionFinder), + reg::obj("parser", parser), + reg::obj("constantResolver", constantResolver), + reg::obj("expressionResultStorageStack", expressionResultStorageStack), + reg::obj("context", scopeContext), + reg::obj("phpVersion", phpVersion), + reg::obj("attributeReflectionFactory", attributeReflectionFactory), + reg::obj("configuredPhpVersionRangeHelper", configuredPhpVersionRangeHelper), + reg::withDefault(reg::any("nodeCallback"), "null"), + reg::withDefault(reg::boolArg("declareStrictTypes"), "false"), + reg::withDefault(reg::obj("function", phpFunctionFromParserNodeReflection, true), "null"), + reg::withDefault(reg::stringArg("namespace", true), "null"), + reg::withDefault(reg::arrayArg("expressionTypes"), "[]"), + reg::withDefault(reg::arrayArg("nativeExpressionTypes"), "[]"), + reg::withDefault(reg::arrayArg("conditionalExpressions"), "[]"), + reg::withDefault(reg::arrayArg("inClosureBindScopeClasses"), "[]"), + reg::withDefault(reg::obj("anonymousFunctionReflection", closureType, true), "null"), + reg::withDefault(reg::boolArg("inFirstLevelStatement"), "true"), + reg::withDefault(reg::arrayArg("currentlyAssignedExpressions"), "[]"), + reg::withDefault(reg::arrayArg("currentlyAllowedUndefinedExpressions"), "[]"), + reg::withDefault(reg::arrayArg("inFunctionCallsStack"), "[]"), + reg::withDefault(reg::boolArg("afterExtractCall"), "false"), + reg::withDefault(reg::obj("parentScope", self, true), "null"), + reg::withDefault(reg::boolArg("nativeTypesPromoted"), "false"), + reg::withDefault(reg::obj("templateArgumentFrame", templateArgumentFrame, true), "null"), + reg::withDefault(reg::obj("templateArgumentConstraints", templateArgumentConstraints, true), "null"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + MutatingScope::ConstructArgs a = {}; + a.inFirstLevelStatement = true; + ZEND_PARSE_PARAMETERS_START(15, 33) + Z_PARAM_OBJECT(a.container) + Z_PARAM_OBJECT(a.scopeFactory) + Z_PARAM_OBJECT(a.reflectionProvider) + Z_PARAM_OBJECT(a.initializerExprTypeResolver) + Z_PARAM_OBJECT(a.expressionTypeResolverExtensions) + Z_PARAM_OBJECT(a.exprPrinter) + Z_PARAM_OBJECT(a.typeSpecifier) + Z_PARAM_OBJECT(a.propertyReflectionFinder) + Z_PARAM_OBJECT(a.parser) + Z_PARAM_OBJECT(a.constantResolver) + Z_PARAM_OBJECT(a.expressionResultStorageStack) + Z_PARAM_OBJECT(a.context) + Z_PARAM_OBJECT(a.phpVersion) + Z_PARAM_OBJECT(a.attributeReflectionFactory) + Z_PARAM_OBJECT(a.configuredPhpVersionRangeHelper) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(a.nodeCallback) + Z_PARAM_BOOL(a.declareStrictTypes) + Z_PARAM_OBJECT_OR_NULL(a.function) + Z_PARAM_STR_OR_NULL(a.ns) + Z_PARAM_ARRAY(a.expressionTypes) + Z_PARAM_ARRAY(a.nativeExpressionTypes) + Z_PARAM_ARRAY(a.conditionalExpressions) + Z_PARAM_ARRAY(a.inClosureBindScopeClasses) + Z_PARAM_OBJECT_OR_NULL(a.anonymousFunctionReflection) + Z_PARAM_BOOL(a.inFirstLevelStatement) + Z_PARAM_ARRAY(a.currentlyAssignedExpressions) + Z_PARAM_ARRAY(a.currentlyAllowedUndefinedExpressions) + Z_PARAM_ARRAY(a.inFunctionCallsStack) + Z_PARAM_BOOL(a.afterExtractCall) + Z_PARAM_OBJECT_OR_NULL(a.parentScope) + Z_PARAM_BOOL(a.nativeTypesPromoted) + Z_PARAM_OBJECT_OR_NULL(a.templateArgumentFrame) + Z_PARAM_OBJECT_OR_NULL(a.templateArgumentConstraints) + ZEND_PARSE_PARAMETERS_END(); + if (a.nodeCallback != NULL) { + ZVAL_DEREF(a.nodeCallback); + } + PT_THIS.construct(a); + }); + + cls.method<&MutatingScope::toNodeCallbackScope>(sigs::toNodeCallbackScope); + + cls.method(sigs::toWalkScope, msToWalkScope); + + cls.method<&MutatingScope::toMutatingScope>(sigs::toMutatingScope); + + cls.method(sigs::getFile, msGetFile); + + cls.method<&MutatingScope::getFileDescription>(sigs::getFileDescription); + + cls.method(sigs::isDeclareStrictTypes, msIsDeclareStrictTypes); + + cls.method<&MutatingScope::enterDeclareStrictTypes>(sigs::enterDeclareStrictTypes); + + cls.method<&MutatingScope::rememberConstructorScope>(sigs::rememberConstructorScope); + + cls.method(sigs::isReadonlyPropertyFetch, msIsReadonlyPropertyFetch); + cls.method(sigs::isInClass, msIsInClass); + + cls.method(sigs::isInTrait, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_MS_RETURN_BOOL(PT_THIS.isInTrait(out_)); + }); + + cls.method(sigs::getClassReflection, msGetClassReflection); + + cls.method<&MutatingScope::getTraitReflection>(sigs::getTraitReflection); + + cls.method(sigs::getFunction, msGetFunction); + + cls.method<&MutatingScope::getFunctionName>(sigs::getFunctionName); + + cls.method(sigs::getNamespace, msGetNamespace); + + /* a named handler: NodeCallbackScope overrides it (as it does + * pushInFunctionCall / popInFunctionCall / filterByTruthyValue / + * filterByFalseyValue), so a $this-dispatch must be able to identify + * the native body */ + cls.method(sigs::getParentScope, msGetParentScope); + + cls.method(sigs::canAnyVariableExist, msCanAnyVariableExist); + + cls.method<&MutatingScope::afterExtractCall>(sigs::afterExtractCall); + + cls.method<&MutatingScope::afterClearstatcacheCall>(sigs::afterClearstatcacheCall); + + cls.method<&MutatingScope::afterOpenSslCall, zp::Str>(sigs::afterOpenSslCall); + + cls.method<&MutatingScope::invalidateVolatileExpressions>(sigs::invalidateVolatileExpressions); + + cls.method(sigs::invalidateExistenceCheckExpressions, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *functionNames; + zend_string *declaredSymbolName; + if (!zp::parse(execute_data, functionNames, declaredSymbolName)) RETURN_THROWS(); + zval declaredSymbolNameZv = {}; + if (declaredSymbolName == NULL) { + ZVAL_NULL(&declaredSymbolNameZv); + } else { + ZVAL_STR(&declaredSymbolNameZv, declaredSymbolName); + } + PT_RETURN_VAL(PT_THIS.invalidateExistenceCheckExpressions(functionNames, &declaredSymbolNameZv)); + }); + + cls.method(sigs::hasVariableType, msHasVariableType); + + cls.method(sigs::getVariableType, msGetVariableType); + + cls.method<&MutatingScope::getDefinedVariables>(sigs::getDefinedVariables); + + cls.method<&MutatingScope::getMaybeDefinedVariables>(sigs::getMaybeDefinedVariables); + + cls.method(sigs::findPossiblyImpureCallDescriptions, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *exprArg; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(exprArg, exprCe) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.findPossiblyImpureCallDescriptions(Z_OBJ_P(exprArg))); + }); + + cls.method(sigs::hasConstant, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *nameArg; + zend_class_entry *nameCe = pt_class(PT_CLASS_NAME); + if (UNEXPECTED(nameCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(nameArg, nameCe) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_RETURN_BOOL(PT_THIS.hasConstant(Z_OBJ_P(nameArg), out_)); + }); + + cls.method(sigs::isInAnonymousFunction, msIsInAnonymousFunction); + + cls.method<&MutatingScope::getAnonymousFunctionReflection>(sigs::getAnonymousFunctionReflection); + + cls.method<&MutatingScope::getAnonymousFunctionReturnType>(sigs::getAnonymousFunctionReturnType); + + cls.method(sigs::withAnonymousFunctionReflection, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *anonymousFunctionReflection; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(anonymousFunctionReflection, pt_ce_closure_type) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.withAnonymousFunctionReflection(anonymousFunctionReflection)); + }); + + /* {{{ the type resolution core (twin 1054–1808) */ + + cls.method(sigs::getType, msGetType); + + cls.method(sigs::getScopeType, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(exprArg); + PT_RETURN_VAL(PT_THIS.getScopeType(exprArg)); + }); + + cls.method(sigs::getScopeNativeType, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(exprArg); + PT_RETURN_VAL(PT_THIS.getScopeNativeType(exprArg)); + }); + + /* getNodeKey() / getExprPrinter(): registered below */ + + cls.method("duplicateWith", reg::Public, 8, { + reg::arrayArg("expressionTypes"), + reg::arrayArg("nativeExpressionTypes"), + reg::arrayArg("conditionalExpressions"), + reg::arrayArg("currentlyAssignedExpressions"), + reg::arrayArg("currentlyAllowedUndefinedExpressions"), + reg::arrayArg("inFunctionCallsStack"), + reg::boolArg("inFirstLevelStatement"), + reg::boolArg("afterExtractCall"), + }, msDuplicateWith, &returnsSelf); + + cls.method(sigs::getClosureScopeCacheKey, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *relevantRoots = NULL; + if (!zp::parse>(execute_data, relevantRoots)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getClosureScopeCacheKey(relevantRoots)); + }); + + cls.method(sigs::specifyTypesOfNewWorldHandlerNode, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *node, *context; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(node, exprCe) + Z_PARAM_OBJECT(context) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.specifyTypesOfNewWorldHandlerNode(Z_OBJ_P(node), context)); + }); + + cls.method(sigs::obtainResultForNode, msObtainResultForNode); + + cls.method(sigs::pushExpressionResultStorage, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *storage; + if (!zp::parse(execute_data, storage)) RETURN_THROWS(); + if (UNEXPECTED(!PT_THIS.pushExpressionResultStorage(storage))) RETURN_THROWS(); + }); + + cls.method(sigs::popExpressionResultStorage, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + if (UNEXPECTED(!PT_THIS.popExpressionResultStorage())) RETURN_THROWS(); + }); + + cls.method(sigs::findSettledStoredResult, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(node); + PT_RETURN_VAL(PT_THIS.findSettledStoredResult(Z_OBJ_P(node))); + }); + + cls.method<&MutatingScope::getCurrentExpressionResultStorage>(sigs::getCurrentExpressionResultStorage); + + cls.method(sigs::withTemplateArgumentFrame, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *frame; + zend_class_entry *frameCe = pt_class(PT_CLASS_TEMPLATE_ARGUMENT_FRAME); + if (UNEXPECTED(frameCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(frame, frameCe) + ZEND_PARSE_PARAMETERS_END(); + zval nullZv; + if (frame == NULL) { + ZVAL_NULL(&nullZv); + frame = &nullZv; + } + PT_RETURN_VAL(PT_THIS.withTemplateArgumentFrame(frame)); + }); + + cls.method<&MutatingScope::getCurrentTemplateArgumentFrame>(sigs::getCurrentTemplateArgumentFrame); + + cls.method<&MutatingScope::getTemplateArgumentConstraints>(sigs::getTemplateArgumentConstraints); + + cls.method(sigs::withTemplateArgumentConstraints, msWithTemplateArgumentConstraints); + + cls.method(sigs::addTemplateArgumentConstraints, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *constraints; + if (!zp::parse(execute_data, constraints)) RETURN_THROWS(); + zval nullZv; + if (constraints == NULL) { + ZVAL_NULL(&nullZv); + constraints = &nullZv; + } + PT_RETURN_VAL(PT_THIS.addTemplateArgumentConstraints(constraints)); + }); + + cls.method(sigs::withoutMemoizedTypes, msWithoutMemoizedTypes); + + cls.method(sigs::getDifferingVariableRoots, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.getDifferingVariableRoots(Z_OBJ_P(other))); + }); + + cls.method(sigs::withRecordedStatementDelta, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *recordedEntry, *recordedExit; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(recordedEntry, pt_ce_mutating_scope) + Z_PARAM_OBJECT_OF_CLASS(recordedExit, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.withRecordedStatementDelta(Z_OBJ_P(recordedEntry), Z_OBJ_P(recordedExit))); + }); + + cls.method(sigs::getNativeType, msGetNativeType); + + cls.method(sigs::getKeepVoidType, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(node); + PT_RETURN_VAL(PT_THIS.getKeepVoidType(Z_OBJ_P(node))); + }); + + cls.method(sigs::doNotTreatPhpDocTypesAsCertain, msDoNotTreatPhpDocTypesAsCertain); + cls.method(sigs::resolveName, msResolveName); + cls.method(sigs::resolveTypeByName, msResolveTypeByName); + + cls.method<&MutatingScope::getTypeFromValue, zp::Zval>(sigs::getTypeFromValue); + + /* }}} */ + + /* {{{ twin 1835-2011 */ + + cls.method("pushInFunctionCall", reg::Public, 3, { + reg::any("reflection"), + reg::obj("parameter", parameterReflection, true), + reg::boolArg("rememberTypes"), + }, msPushInFunctionCall, &returnsSelf); + + cls.method(sigs::popInFunctionCall, msPopInFunctionCall); + + cls.method(sigs::isInClassExists, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *className; + if (!zp::parse(execute_data, className)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.isInClassExists(className, out_)); + }); + + cls.method<&MutatingScope::getFunctionCallStack>(sigs::getFunctionCallStack); + + cls.method<&MutatingScope::getFunctionCallStackWithParameters>(sigs::getFunctionCallStackWithParameters); + + cls.method(sigs::isInFunctionExists, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *functionName; + if (!zp::parse(execute_data, functionName)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.isInFunctionExists(functionName, out_)); + }); + + cls.method(sigs::enterClass, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_CLASS_REFLECTION(classReflectionArg); + PT_RETURN_VAL(PT_THIS.enterClass(classReflectionArg)); + }); + + cls.method(sigs::enterTrait, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_CLASS_REFLECTION(traitReflection); + PT_RETURN_VAL(PT_THIS.enterTrait(traitReflection)); + }); + + /* out of the twin's file order, for the function-like family */ + cls.method(sigs::getPhpVersion, msGetPhpVersion); + + cls.method(sigs::isParameterValueNullable, msIsParameterValueNullable); + + cls.method(sigs::getFunctionType, msGetFunctionType); + + /* {{{ twin 2012-2369: the function-like family */ + + cls.method("enterClassMethod", reg::Public, 9, { + reg::obj("classMethod", classMethodNode), + reg::obj("templateTypeMap", templateTypeMap), + reg::arrayArg("phpDocParameterTypes"), + reg::obj("phpDocReturnType", type, true), + reg::obj("throwType", type, true), + reg::stringArg("deprecatedDescription", true), + reg::boolArg("isDeprecated"), + reg::boolArg("isInternal"), + reg::boolArg("isFinal"), + reg::withDefault(nullableBool("isPure"), "null"), + reg::withDefault(reg::boolArg("acceptsNamedArguments"), "true"), + reg::withDefault(reg::obj("asserts", assertions, true), "null"), + reg::withDefault(reg::obj("selfOutType", type, true), "null"), + reg::withDefault(reg::stringArg("phpDocComment", true), "null"), + reg::withDefault(reg::arrayArg("parameterOutTypes"), "[]"), + reg::withDefault(reg::arrayArg("immediatelyInvokedCallableParameters"), "[]"), + reg::withDefault(reg::arrayArg("phpDocClosureThisTypeParameters"), "[]"), + reg::withDefault(reg::boolArg("isConstructor"), "false"), + reg::withDefault(reg::obj("resolvedPhpDocBlock", resolvedPhpDocBlock, true), "null"), + reg::withDefault(reg::arrayArg("phpDocPureUnlessCallableIsImpureParameters"), "[]"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classMethod, *templateTypeMapArg, *phpDocParameterTypes, *phpDocReturnType = NULL, *throwType = NULL, *asserts = NULL, *selfOutType = NULL, *resolvedPhpDocBlock = NULL; + zval *parameterOutTypes = NULL, *immediatelyInvokedCallableParameters = NULL, *phpDocClosureThisTypeParameters = NULL, *phpDocPureUnlessCallableIsImpureParameters = NULL; + zend_string *deprecatedDescription = NULL, *phpDocComment = NULL; + bool isDeprecated, isInternal, isFinal, isPure = false, isPureIsNull = true, acceptsNamedArguments = true, isConstructor = false; + ZEND_PARSE_PARAMETERS_START(9, 20) + Z_PARAM_OBJECT(classMethod) + Z_PARAM_OBJECT(templateTypeMapArg) + Z_PARAM_ARRAY(phpDocParameterTypes) + Z_PARAM_OBJECT_OR_NULL(phpDocReturnType) + Z_PARAM_OBJECT_OR_NULL(throwType) + Z_PARAM_STR_OR_NULL(deprecatedDescription) + Z_PARAM_BOOL(isDeprecated) + Z_PARAM_BOOL(isInternal) + Z_PARAM_BOOL(isFinal) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL_OR_NULL(isPure, isPureIsNull) + Z_PARAM_BOOL(acceptsNamedArguments) + Z_PARAM_OBJECT_OR_NULL(asserts) + Z_PARAM_OBJECT_OR_NULL(selfOutType) + Z_PARAM_STR_OR_NULL(phpDocComment) + Z_PARAM_ARRAY(parameterOutTypes) + Z_PARAM_ARRAY(immediatelyInvokedCallableParameters) + Z_PARAM_ARRAY(phpDocClosureThisTypeParameters) + Z_PARAM_BOOL(isConstructor) + Z_PARAM_OBJECT_OR_NULL(resolvedPhpDocBlock) + Z_PARAM_ARRAY(phpDocPureUnlessCallableIsImpureParameters) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_OBJ_ZVAL(phpDocReturnType); + PT_MS_OBJ_ZVAL(throwType); + PT_MS_OBJ_ZVAL(asserts); + PT_MS_OBJ_ZVAL(selfOutType); + PT_MS_OBJ_ZVAL(resolvedPhpDocBlock); + PT_MS_STR_ZVAL(deprecatedDescription); + PT_MS_STR_ZVAL(phpDocComment); + PT_MS_ARRAY_ZVAL(parameterOutTypes); + PT_MS_ARRAY_ZVAL(immediatelyInvokedCallableParameters); + PT_MS_ARRAY_ZVAL(phpDocClosureThisTypeParameters); + PT_MS_ARRAY_ZVAL(phpDocPureUnlessCallableIsImpureParameters); + zval isPureZv; + if (isPureIsNull) { + ZVAL_NULL(&isPureZv); + } else { + ZVAL_BOOL(&isPureZv, isPure); + } + PT_RETURN_VAL(PT_THIS.enterClassMethod(classMethod, templateTypeMapArg, phpDocParameterTypes, phpDocReturnType, throwType, &deprecatedDescriptionZv, isDeprecated, isInternal, isFinal, &isPureZv, acceptsNamedArguments, asserts, selfOutType, &phpDocCommentZv, parameterOutTypes, immediatelyInvokedCallableParameters, phpDocClosureThisTypeParameters, isConstructor, resolvedPhpDocBlock, phpDocPureUnlessCallableIsImpureParameters)); + }, &returnsSelf); + + cls.method("enterPropertyHook", reg::Public, 10, { + reg::obj("hook", propertyHook), + reg::stringArg("propertyName"), + reg::obj("nativePropertyTypeNode", identifierOrNameOrComplexType, true), + reg::obj("phpDocPropertyType", type, true), + reg::arrayArg("phpDocParameterTypes"), + reg::obj("throwType", type, true), + reg::stringArg("deprecatedDescription", true), + reg::boolArg("isDeprecated"), + nullableBool("isPure"), + reg::stringArg("phpDocComment", true), + reg::withDefault(reg::obj("resolvedPhpDocBlock", resolvedPhpDocBlock, true), "null"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *hook, *nativePropertyTypeNode = NULL, *phpDocPropertyType = NULL, *phpDocParameterTypes, *throwType = NULL, *resolvedPhpDocBlock = NULL; + zend_string *propertyName, *deprecatedDescription = NULL, *phpDocComment = NULL; + bool isDeprecated, isPure = false, isPureIsNull = true; + ZEND_PARSE_PARAMETERS_START(10, 11) + Z_PARAM_OBJECT(hook) + Z_PARAM_STR(propertyName) + Z_PARAM_OBJECT_OR_NULL(nativePropertyTypeNode) + Z_PARAM_OBJECT_OR_NULL(phpDocPropertyType) + Z_PARAM_ARRAY(phpDocParameterTypes) + Z_PARAM_OBJECT_OR_NULL(throwType) + Z_PARAM_STR_OR_NULL(deprecatedDescription) + Z_PARAM_BOOL(isDeprecated) + Z_PARAM_BOOL_OR_NULL(isPure, isPureIsNull) + Z_PARAM_STR_OR_NULL(phpDocComment) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OR_NULL(resolvedPhpDocBlock) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_OBJ_ZVAL(nativePropertyTypeNode); + PT_MS_OBJ_ZVAL(phpDocPropertyType); + PT_MS_OBJ_ZVAL(throwType); + PT_MS_OBJ_ZVAL(resolvedPhpDocBlock); + PT_MS_STR_ZVAL(deprecatedDescription); + PT_MS_STR_ZVAL(phpDocComment); + zval isPureZv; + if (isPureIsNull) { + ZVAL_NULL(&isPureZv); + } else { + ZVAL_BOOL(&isPureZv, isPure); + } + PT_RETURN_VAL(PT_THIS.enterPropertyHook(hook, propertyName, nativePropertyTypeNode, phpDocPropertyType, phpDocParameterTypes, throwType, &deprecatedDescriptionZv, isDeprecated, &isPureZv, &phpDocCommentZv, resolvedPhpDocBlock)); + }, &returnsSelf); + + cls.method("enterFunction", reg::Public, 8, { + reg::obj("function", functionNode), + reg::obj("templateTypeMap", templateTypeMap), + reg::arrayArg("phpDocParameterTypes"), + reg::obj("phpDocReturnType", type, true), + reg::obj("throwType", type, true), + reg::stringArg("deprecatedDescription", true), + reg::boolArg("isDeprecated"), + reg::boolArg("isInternal"), + reg::withDefault(nullableBool("isPure"), "null"), + reg::withDefault(reg::boolArg("acceptsNamedArguments"), "true"), + reg::withDefault(reg::obj("asserts", assertions, true), "null"), + reg::withDefault(reg::stringArg("phpDocComment", true), "null"), + reg::withDefault(reg::arrayArg("parameterOutTypes"), "[]"), + reg::withDefault(reg::arrayArg("immediatelyInvokedCallableParameters"), "[]"), + reg::withDefault(reg::arrayArg("phpDocClosureThisTypeParameters"), "[]"), + reg::withDefault(reg::arrayArg("pureUnlessCallableIsImpureParameters"), "[]"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *function = NULL, *templateTypeMapArg = NULL, *phpDocParameterTypes = NULL, *phpDocReturnType = NULL, *throwType = NULL, *asserts = NULL; + zval *parameterOutTypes = NULL, *immediatelyInvokedCallableParameters = NULL, *phpDocClosureThisTypeParameters = NULL, *pureUnlessCallableIsImpureParameters = NULL; + zend_string *deprecatedDescription = NULL, *phpDocComment = NULL; + bool isDeprecated = false, isInternal = false, isPure = false, isPureIsNull = true, acceptsNamedArguments = true; + ZEND_PARSE_PARAMETERS_START(8, 16) + Z_PARAM_OBJECT(function) + Z_PARAM_OBJECT(templateTypeMapArg) + Z_PARAM_ARRAY(phpDocParameterTypes) + Z_PARAM_OBJECT_OR_NULL(phpDocReturnType) + Z_PARAM_OBJECT_OR_NULL(throwType) + Z_PARAM_STR_OR_NULL(deprecatedDescription) + Z_PARAM_BOOL(isDeprecated) + Z_PARAM_BOOL(isInternal) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL_OR_NULL(isPure, isPureIsNull) + Z_PARAM_BOOL(acceptsNamedArguments) + Z_PARAM_OBJECT_OR_NULL(asserts) + Z_PARAM_STR_OR_NULL(phpDocComment) + Z_PARAM_ARRAY(parameterOutTypes) + Z_PARAM_ARRAY(immediatelyInvokedCallableParameters) + Z_PARAM_ARRAY(phpDocClosureThisTypeParameters) + Z_PARAM_ARRAY(pureUnlessCallableIsImpureParameters) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_OBJ_ZVAL(phpDocReturnType); + PT_MS_OBJ_ZVAL(throwType); + PT_MS_OBJ_ZVAL(asserts); + PT_MS_STR_ZVAL(deprecatedDescription); + PT_MS_STR_ZVAL(phpDocComment); + PT_MS_ARRAY_ZVAL(parameterOutTypes); + PT_MS_ARRAY_ZVAL(immediatelyInvokedCallableParameters); + PT_MS_ARRAY_ZVAL(phpDocClosureThisTypeParameters); + PT_MS_ARRAY_ZVAL(pureUnlessCallableIsImpureParameters); + zval isPureZv; + if (isPureIsNull) { + ZVAL_NULL(&isPureZv); + } else { + ZVAL_BOOL(&isPureZv, isPure); + } + PT_RETURN_VAL(PT_THIS.enterFunction(function, templateTypeMapArg, phpDocParameterTypes, phpDocReturnType, throwType, &deprecatedDescriptionZv, isDeprecated, isInternal, &isPureZv, acceptsNamedArguments, asserts, &phpDocCommentZv, parameterOutTypes, immediatelyInvokedCallableParameters, phpDocClosureThisTypeParameters, pureUnlessCallableIsImpureParameters)); + }, &returnsSelf); + + cls.method<&MutatingScope::enterNamespace, zp::Str>(sigs::enterNamespace); + + /* }}} */ + + /* {{{ twin 2385-2558: the closure-bind family */ + + cls.method(sigs::enterClosureBind, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *thisType = NULL, *nativeThisType = NULL, *scopeClasses; + if (!zp::parse(execute_data, thisType, nativeThisType, scopeClasses)) RETURN_THROWS(); + PT_MS_OBJ_ZVAL(thisType); + PT_MS_OBJ_ZVAL(nativeThisType); + PT_RETURN_VAL(PT_THIS.enterClosureBind(thisType, nativeThisType, scopeClasses)); + }); + + cls.method(sigs::restoreOriginalScopeAfterClosureBind, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *originalScope; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(originalScope, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.restoreOriginalScopeAfterClosureBind(Z_OBJ_P(originalScope))); + }); + + cls.method(sigs::restoreThis, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *restoreThisScope; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(restoreThisScope, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.restoreThis(Z_OBJ_P(restoreThisScope))); + }); + + cls.method<&MutatingScope::enterClosureCall, zp::Obj, zp::Obj>(sigs::enterClosureCall); + + cls.method(sigs::isInClosureBind, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + PT_MS_RETURN_BOOL(PT_THIS.isInClosureBind(out_)); + }); + + cls.method<&MutatingScope::withClosureBindScopeClasses, zp::Arr>(sigs::withClosureBindScopeClasses); + + /* }}} */ + + /* }}} */ + + /* {{{ twin 2560-3990: the anonymous- and arrow-function + * entries, the assignment / invalidation family and the specification + * machinery */ + + cls.method("enterAnonymousFunction", reg::Public, 2, { + reg::obj("closure", closureNode), + reg::arrayArg("callableParameters", true), + reg::withDefault(reg::arrayArg("nativeCallableParameters", true), "null"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *closure, *callableParameters = NULL, *nativeCallableParameters = NULL; + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + if (UNEXPECTED(closureCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(closure, closureCe) + Z_PARAM_ARRAY_OR_NULL(callableParameters) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(nativeCallableParameters) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.enterAnonymousFunction(Z_OBJ_P(closure), callableParameters, nativeCallableParameters)); + }, &returnsSelf); + + cls.method("enterAnonymousFunctionWithoutReflection", reg::Public, 3, { + reg::obj("closure", closureNode), + reg::arrayArg("callableParameters", true), + reg::arrayArg("nativeCallableParameters", true), + }, msEnterAnonymousFunctionWithoutReflection, &returnsSelf); + + cls.method("enterArrowFunction", reg::Public, 2, { + reg::obj("arrowFunction", arrowFunctionNode), + reg::arrayArg("callableParameters", true), + reg::withDefault(reg::arrayArg("nativeCallableParameters", true), "null"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *arrowFunction, *callableParameters = NULL, *nativeCallableParameters = NULL; + zend_class_entry *arrowFunctionCe = pt_class(PT_CLASS_ARROW_FUNCTION); + if (UNEXPECTED(arrowFunctionCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(arrowFunction, arrowFunctionCe) + Z_PARAM_ARRAY_OR_NULL(callableParameters) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(nativeCallableParameters) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.enterArrowFunction(Z_OBJ_P(arrowFunction), callableParameters, nativeCallableParameters)); + }, &returnsSelf); + + cls.method("enterArrowFunctionWithoutReflection", reg::Public, 3, { + reg::obj("arrowFunction", arrowFunctionNode), + reg::arrayArg("callableParameters", true), + reg::arrayArg("nativeCallableParameters", true), + }, msEnterArrowFunctionWithoutReflection, &returnsSelf); + + cls.method(sigs::intersectButNotNever, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *nativeType, *inferredType; + if (!zp::parse(execute_data, nativeType, inferredType)) RETURN_THROWS(); + zv::Val result = MutatingScope::intersectButNotNever(nativeType, inferredType); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.method(sigs::enterMatch, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *condType, *condNativeType; + zend_class_entry *matchCe = pt_class(PT_CLASS_MATCH); + if (UNEXPECTED(matchCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT_OF_CLASS(expr, matchCe) + Z_PARAM_OBJECT(condType) + Z_PARAM_OBJECT(condNativeType) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.enterMatch(Z_OBJ_P(expr), condType, condNativeType)); + }); + + cls.method("enterForeach", reg::Public, 7, { + reg::obj("originalScope", self), + reg::obj("iteratee", expr), + reg::obj("iterateeType", type), + reg::obj("nativeIterateeType", type), + reg::stringArg("valueName"), + reg::stringArg("keyName", true), + reg::boolArg("valueByRef"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *originalScope, *iteratee, *iterateeType, *nativeIterateeType; + zend_string *valueName, *keyName = NULL; + bool valueByRef; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(7, 7) + Z_PARAM_OBJECT_OF_CLASS(originalScope, pt_ce_mutating_scope) + Z_PARAM_OBJECT_OF_CLASS(iteratee, exprCe) + Z_PARAM_OBJECT(iterateeType) + Z_PARAM_OBJECT(nativeIterateeType) + Z_PARAM_STR(valueName) + Z_PARAM_STR_OR_NULL(keyName) + Z_PARAM_BOOL(valueByRef) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.enterForeach(Z_OBJ_P(originalScope), iteratee, iterateeType, nativeIterateeType, valueName, keyName, valueByRef)); + }, &returnsSelf); + + cls.method("enterForeachKey", reg::Public, 5, { + reg::obj("originalScope", self), + reg::obj("iteratee", expr), + reg::obj("iterateeType", type), + reg::obj("nativeIterateeType", type), + reg::stringArg("keyName"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *originalScope, *iteratee, *iterateeType, *nativeIterateeType; + zend_string *keyName; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(5, 5) + Z_PARAM_OBJECT_OF_CLASS(originalScope, pt_ce_mutating_scope) + Z_PARAM_OBJECT_OF_CLASS(iteratee, exprCe) + Z_PARAM_OBJECT(iterateeType) + Z_PARAM_OBJECT(nativeIterateeType) + Z_PARAM_STR(keyName) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.enterForeachKey(Z_OBJ_P(originalScope), iteratee, iterateeType, nativeIterateeType, keyName)); + }, &returnsSelf); + + cls.method(sigs::enterCatchType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *catchType; + zend_string *variableName = NULL; + if (!zp::parse(execute_data, catchType, variableName)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.enterCatchType(catchType, variableName)); + }); + + cls.method(sigs::enterExpressionAssign, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr; + bool isPlainWrite = true; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(isPlainWrite) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.enterExpressionAssign(Z_OBJ_P(expr), isPlainWrite)); + }); + + cls.method(sigs::exitExpressionAssign, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.exitExpressionAssign(Z_OBJ_P(expr))); + }); + + cls.method(sigs::isInExpressionAssign, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_MS_RETURN_BOOL(PT_THIS.isInExpressionAssign(Z_OBJ_P(expr), out_)); + }); + + cls.method(sigs::isInWriteExpressionAssign, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_MS_RETURN_BOOL(PT_THIS.isInWriteExpressionAssign(Z_OBJ_P(expr), out_)); + }); + + cls.method(sigs::setAllowedUndefinedExpression, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.setAllowedUndefinedExpression(Z_OBJ_P(expr))); + }); + + cls.method(sigs::unsetAllowedUndefinedExpression, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.unsetAllowedUndefinedExpression(Z_OBJ_P(expr))); + }); + + cls.method(sigs::isUndefinedExpressionAllowed, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_MS_RETURN_BOOL(PT_THIS.isUndefinedExpressionAllowed(Z_OBJ_P(expr), out_)); + }); + + cls.method("assignVariable", reg::Public, 4, { + reg::stringArg("variableName"), + reg::obj("type", type), + reg::obj("nativeType", type), + reg::obj("certainty", trinaryLogic), + reg::withDefault(reg::arrayArg("intertwinedPropagatedFrom"), "[]"), + }, msAssignVariable, &returnsSelf); + + cls.method(sigs::getStateType, [](INTERNAL_FUNCTION_PARAMETERS) { + PT_MS_PARSE_EXPR(expr); + PT_RETURN_VAL(PT_THIS.getStateType(Z_OBJ_P(expr))); + }); + + cls.method("specifyExpressionType", reg::Public, 4, { + reg::obj("expr", expr), + reg::obj("type", type), + reg::obj("nativeType", type), + reg::obj("certainty", trinaryLogic), + }, msSpecifyExpressionType, &returnsSelf); + + cls.method(sigs::assignExpression, msAssignExpression); + + cls.method<&MutatingScope::assignInitializedProperty, zp::Obj, zp::Str>(sigs::assignInitializedProperty); + + cls.method("invalidateExpression", reg::Public, 1, { + reg::obj("expressionToInvalidate", expr), + reg::withDefault(reg::boolArg("requireMoreCharacters"), "false"), + reg::withDefault(reg::obj("invalidatingClass", classReflection, true), "null"), + reg::withDefault(reg::boolArg("keepPropertyFetches"), "false"), + }, msInvalidateExpression, &returnsSelf); + + cls.method(sigs::isPrivatePropertyOfDifferentClass, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *invalidatingClass; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + zend_class_entry *classReflectionCe = msClassReflectionCe(); + if (UNEXPECTED(exprCe == NULL || classReflectionCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + Z_PARAM_OBJECT_OF_CLASS(invalidatingClass, classReflectionCe) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_RETURN_BOOL(PT_THIS.isPrivatePropertyOfDifferentClass(Z_OBJ_P(expr), invalidatingClass, out_)); + }); + + cls.method(sigs::addTypeToExpression, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *type; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.addTypeToExpression(Z_OBJ_P(expr), type)); + }); + + cls.method(sigs::removeTypeFromExpression, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *typeToRemove; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + Z_PARAM_OBJECT(typeToRemove) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.removeTypeFromExpression(Z_OBJ_P(expr), typeToRemove)); + }); + + /* }}} */ + + /* {{{ twin 3993-4773: the narrowing application, the + * conditional-expression bookkeeping and the scope merges */ + + cls.method(sigs::filterByTruthyValue, msFilterByTruthyValue); + cls.method(sigs::filterByFalseyValue, msFilterByFalseyValue); + cls.method(sigs::applySpecifiedTypes, msApplySpecifiedTypes); + + cls.method<&MutatingScope::getConditionalExpressions>(sigs::getConditionalExpressions); + + cls.method<&MutatingScope::addConditionalExpressions, zp::Str, zp::Ht>(sigs::addConditionalExpressions); + + cls.method<&MutatingScope::exitFirstLevelStatements>(sigs::exitFirstLevelStatements); + + cls.method(sigs::mergeWith, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *otherScope; + bool preserveVacuousConditionals = false; + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(otherScope, pt_ce_mutating_scope) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(preserveVacuousConditionals) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.mergeWith(otherScope, preserveVacuousConditionals)); + }); + + cls.method(sigs::mergeInitializedProperties, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *calledMethodScope; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(calledMethodScope, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.mergeInitializedProperties(Z_OBJ_P(calledMethodScope))); + }); + + cls.method(sigs::processFinallyScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *finallyScope, *originalFinallyScope; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(finallyScope, pt_ce_mutating_scope) + Z_PARAM_OBJECT_OF_CLASS(originalFinallyScope, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.processFinallyScope(Z_OBJ_P(finallyScope), Z_OBJ_P(originalFinallyScope))); + }); + + /* }}} */ + + /* {{{ twin 4775-5884: the closure and loop scopes, the + * generalization, the scope comparison, the member-access queries and + * the remaining readers */ + + cls.method(sigs::processClosureScope, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *closureScope, *prevScope; + HashTable *byRefUses; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT_OF_CLASS(closureScope, pt_ce_mutating_scope) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(prevScope, pt_ce_mutating_scope) + Z_PARAM_ARRAY_HT(byRefUses) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.processClosureScope(Z_OBJ_P(closureScope), prevScope, byRefUses)); + }); + + cls.method(sigs::processAlwaysIterableForeachScopeWithoutPollute, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *finalScope; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(finalScope, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.processAlwaysIterableForeachScopeWithoutPollute(Z_OBJ_P(finalScope))); + }); + + cls.method(sigs::generalizeWith, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *otherScope; + HashTable *writableVariableNames = NULL; + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(otherScope, pt_ce_mutating_scope) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT_OR_NULL(writableVariableNames) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(PT_THIS.generalizeWith(Z_OBJ_P(otherScope), writableVariableNames)); + }); + + cls.method(sigs::equals, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *otherScope; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(otherScope, pt_ce_mutating_scope) + ZEND_PARSE_PARAMETERS_END(); + PT_MS_RETURN_BOOL(PT_THIS.equals(Z_OBJ_P(otherScope), out_)); + }); + + cls.method(sigs::canAccessProperty, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *reflection; + if (!zp::parse(execute_data, reflection)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.canAccessProperty(Z_OBJ_P(reflection), out_)); + }); + + cls.method(sigs::canReadProperty, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *reflection; + if (!zp::parse(execute_data, reflection)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.canReadProperty(Z_OBJ_P(reflection), out_)); + }); + + cls.method(sigs::canWriteProperty, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *reflection; + if (!zp::parse(execute_data, reflection)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.canWriteProperty(Z_OBJ_P(reflection), out_)); + }); + + cls.method(sigs::canCallMethod, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *reflection; + if (!zp::parse(execute_data, reflection)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.canCallMethod(Z_OBJ_P(reflection), out_)); + }); + + cls.method(sigs::canAccessConstant, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *reflection; + if (!zp::parse(execute_data, reflection)) RETURN_THROWS(); + PT_MS_RETURN_BOOL(PT_THIS.canAccessConstant(Z_OBJ_P(reflection), out_)); + }); + + cls.method<&MutatingScope::debug>(sigs::debug); + + cls.method(sigs::filterTypeWithMethod, msFilterTypeWithMethod); + + cls.method<&MutatingScope::getMethodReflection, zp::Obj, zp::Str>(sigs::getMethodReflection); + + cls.method<&MutatingScope::getNakedMethod, zp::Obj, zp::Str>(sigs::getNakedMethod); + + cls.method<&MutatingScope::getPropertyReflection, zp::Obj, zp::Str>(sigs::getPropertyReflection); + + cls.method<&MutatingScope::getInstancePropertyReflection, zp::Obj, zp::Str>(sigs::getInstancePropertyReflection); + + cls.method<&MutatingScope::getStaticPropertyReflection, zp::Obj, zp::Str>(sigs::getStaticPropertyReflection); + + cls.method<&MutatingScope::getConstantReflection, zp::Obj, zp::Str>(sigs::getConstantReflection); + + cls.method<&MutatingScope::getConstantExplicitTypeFromConfig, zp::Str, zp::Obj>(sigs::getConstantExplicitTypeFromConfig); + + cls.method<&MutatingScope::getIterableKeyType, zp::Obj>(sigs::getIterableKeyType); + + cls.method<&MutatingScope::getIterableValueType, zp::Obj>(sigs::getIterableValueType); + + cls.method(sigs::invokeNodeCallback, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *nodeArg; + if (!zp::parse(execute_data, nodeArg)) RETURN_THROWS(); + if (UNEXPECTED(!PT_THIS.invokeNodeCallback(Z_OBJ_P(nodeArg)))) RETURN_THROWS(); + }); + + cls.method(sigs::emitCollectedData, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *collectorType; + zval *data; + if (!zp::parse(execute_data, collectorType, data)) RETURN_THROWS(); + if (UNEXPECTED(!PT_THIS.emitCollectedData(collectorType, data))) RETURN_THROWS(); + }); + + /* }}} */ + + /* out of the twin's file order (see the handle class) */ + cls.method(sigs::getNodeKey, msGetNodeKey); + + cls.method<&MutatingScope::getExprPrinter>(sigs::getExprPrinter); + + cls.method(sigs::hasExpressionType, msHasExpressionType); + + cls.method(sigs::getTrackedExpressionType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *node; + if (!zp::parse(execute_data, node)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.getTrackedExpressionType(Z_OBJ_P(node))); + }); + + cls.method(sigs::isInFirstLevelStatement, msIsInFirstLevelStatement); + + cls.shadow(&pt_ce_mutating_scope); +} + +/* }}} */ diff --git a/turbo-ext/src/PhpClassReflectionExtension.cpp b/turbo-ext/src/PhpClassReflectionExtension.cpp new file mode 100644 index 00000000000..b61d32d6703 --- /dev/null +++ b/turbo-ext/src/PhpClassReflectionExtension.cpp @@ -0,0 +1,3108 @@ +/* + * PHPStanTurbo\PhpClassReflectionExtension — native implementation of + * PHPStan\Reflection\Php\PhpClassReflectionExtension, declared under the + * twin's real name at activation (final, like the twin). + * + * Why. ClassReflection asks this service for every member it is asked for: + * hasMethod() 807K, hasNativeMethod() 746K, touchMemberCacheKey() 399K, + * getNativeMethod() 292K, hasProperty() 156K and getNativeProperty() 78K + * times in a self-analysis of src/Analyser, src/Rules and src/Type. Those + * bodies are three lines each around a memo array, and they call back into + * ClassReflection::getCacheKey() 1.24M and ::getNativeReflection() 1.01M + * times — trivial memo getters that the pt_class_reflection_*() direct + * calls answer without a frame — and into the already + * native LruCache 0.40M times. The member construction below them + * (createProperty 10K, createMethod 22K, createUserlandMethodReflection + * 17K) is memoized and cold, but a shadowed class has no PHP body left to + * fall back to, so it is ported too. + * + * Design + * ------ + * Class shape. The twin is final: no PHP subclass exists, so every + * `$this->method()` is a direct C++ call — no Z_OBJCE dispatch. The class + * is an #[AutowiredService]: Nette reflects __construct while it compiles + * the container and pairs #[AutowiredParameter] by name, so the arginfo + * declares the twin's exact parameter names AND class names (README rule + * 6). $memberCacheKeysMax is the one non-promoted parameter. + * + * Layout. The twin's properties are typed property slots in the twin's + * declaration order: the seven class-body properties first (the LruCache + * and the six memo arrays), then the seventeen promoted constructor + * properties in parameter order. The std object handlers do GC/clone/free. + * The names are load-bearing — the differential harness + * (tests/php-class-reflection-family.php) reads the constructor arguments + * and the memo state of both sides by reflection. + * + * Collaborators. The fifteen injected services and the BetterReflection + * adapters ($classReflection->getNativeReflection(), the property and + * method reflections, their tags and ResolvedPhpDocBlocks) are PHP objects + * called by name (pt_type_call). ClassReflection's two hot getters go + * through the direct calls ClassReflection.cpp exports + * (pt_class_reflection_get_cache_key / _get_native_reflection); every + * other ClassReflection method is a by-name call, as is + * ClassMemberAccessAnswerer's isInClass()/getClassReflection() (through + * pt_scope_is_in_class / pt_scope_get_class_reflection, which fast-path a + * MutatingScope and call the method on anything else). The shared member + * LRU is the native LruCache through its exported helpers. The Type kernel + * is reached natively: TypeCombinator::union()/intersect(), + * TemplateTypeHelper::resolveTemplateTypes(), + * TypehintHelper::decideTypeFromReflection()/decideType(), + * ConstantArrayTypeBuilder, TemplateTypeMap::createEmpty(), the + * TemplateTypeVariance singletons and `new` of the shadowing + * ConstantStringType / StringType / MixedType / ArrayType / + * AccessoryNonFalsyStringType / AccessoryDecimalIntegerStringType / + * EnumCaseObjectType; `instanceof` against MixedType / TemplateMixedType / + * ErrorType / NeverType / UnionType uses their class entries directly. + * Everything else (PhpPropertyReflection, NativeMethodReflection, + * ExtendedNativeParameterReflection, EnumCasesMethodReflection, + * ExtendedFunctionVariant, Assertions, InitializerExprContext, + * OutOfClassScope, ShouldNotHappenException, the PrivateProperty / + * ProtectedProperty attribute classes, the BetterReflection adapter's + * ReflectionMethod and the parser nodes read by the constructor-inference + * pass) goes through pt_type_new / pt_type_call_static / pt_type_instanceof + * on class-map keys. + * + * getCacheKey() is called once per method where the twin calls it three or + * four times: it memoizes into $cacheKey and is a pure read afterwards, so + * the repeated calls are the same string by construction. + * + * The logic lives in the PhpClassReflectionExtension handle class below, + * structured to mirror src/Reflection/Php/PhpClassReflectionExtension.php + * method for method and in the same order; the registration at the bottom + * is only the engine ABI glue (parameter parsing + delegation). + */ + +#include "support.h" +#include "generated/PhpClassReflectionExtension.h" +#include "zv.h" +#include "TypeTraits.h" +#include "TypeOps.h" + +#include +#include + +zend_class_entry *pt_ce_php_class_reflection_extension = nullptr; + +/* OBJ_PROP_NUM slots, in the twin's declaration order: the class-body + * properties first, the promoted constructor properties after them */ +enum : uint32_t +{ + PT_PCRE_PROP_MEMBER_CACHE_ORDER = 0, + PT_PCRE_PROP_PROPERTIES_INCLUDING_ANNOTATIONS, + PT_PCRE_PROP_NATIVE_PROPERTIES, + PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS, + PT_PCRE_PROP_NATIVE_METHODS, + PT_PCRE_PROP_PROPERTY_TYPES_CACHE, + PT_PCRE_PROP_INFER_IN_PROCESS, + PT_PCRE_PROP_SCOPE_FACTORY, + PT_PCRE_PROP_PHP_DOCS_RESOLVER, + PT_PCRE_PROP_NODE_SCOPE_RESOLVER, + PT_PCRE_PROP_METHOD_REFLECTION_FACTORY, + PT_PCRE_PROP_PHP_DOC_INHERITANCE_RESOLVER, + PT_PCRE_PROP_DEPRECATION_PROVIDER, + PT_PCRE_PROP_ANNOTATIONS_METHODS_EXTENSION, + PT_PCRE_PROP_ANNOTATIONS_PROPERTIES_EXTENSION, + PT_PCRE_PROP_SIGNATURE_MAP_PROVIDER, + PT_PCRE_PROP_PARSER, + PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER, + PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER, + PT_PCRE_PROP_FILE_TYPE_MAPPER, + PT_PCRE_PROP_ATTRIBUTE_REFLECTION_FACTORY, + PT_PCRE_PROP_ALLOWED_CONSTANTS_MAP_PROVIDER, + PT_PCRE_PROP_INFER_PRIVATE_PROPERTY_TYPE, + PT_PCRE_PROP_PHP_VERSION, + PT_PCRE_PROP_COUNT, +}; + +namespace { + +/* {{{ small engine helpers */ + +/* $object->method(...$args); UNDEF = pending exception */ +inline zv::Val call(zval *object, const char *lcname, size_t len, uint32_t argc = 0, zval *argv = NULL) +{ + return pt_type_call(Z_OBJ_P(object), lcname, len, argc, argv); +} + +/* $object->method(...$args) coerced to bool; false with `ok` cleared on a + * pending exception */ +inline bool callBool(zval *object, const char *lcname, size_t len, uint32_t argc, zval *argv, bool &ok) +{ + zv::Val result = pt_type_call(Z_OBJ_P(object), lcname, len, argc, argv); + if (UNEXPECTED(result.isUndef())) { + ok = false; + return false; + } + ok = true; + return zend_is_true(result.raw()); +} + +/* new ShouldNotHappenException() — the twin's default message ("Internal + * error.") comes from its own constructor; thrown, never returned */ +void throwShouldNotHappen() +{ + zv::Val exception = pt_type_new(PT_CLASS_SHOULD_NOT_HAPPEN, 0, NULL); + if (exception.isUndef()) return; + zval thrown = exception.take(); + zend_throw_exception_object(&thrown); +} + +void throwShouldNotHappenStr(zend_string *message) +{ + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (ce == NULL) return; + zend_throw_exception(ce, ZSTR_VAL(message), 0); +} + +/* the twin's two "Internal error: Expected to find an ancestor …" messages */ +zv::Str ancestorMessage(zend_string *declaringClassName, zend_string *className) +{ + return zv::Str::adopt(zend_strpprintf(0, "Internal error: Expected to find an ancestor with class name %s on %s, but none was found.", ZSTR_VAL(declaringClassName), ZSTR_VAL(className))); +} + +/* isset($array[$key]) over a symtable-keyed array: the value, or NULL when + * the key is absent or holds null */ +zval *issetIn(zval *array, zend_string *key) +{ + if (Z_TYPE_P(array) != IS_ARRAY) return NULL; + zval *found = zend_symtable_find(Z_ARRVAL_P(array), key); + if (found == NULL) return NULL; + ZVAL_DEREF(found); + return Z_TYPE_P(found) == IS_NULL ? NULL : found; +} + +/* $array[$key] ?? null over an array keyed by strings (array_key_exists + * semantics: a stored null is returned as the null zval) */ +zval *keyIn(zval *array, zend_string *key) +{ + if (Z_TYPE_P(array) != IS_ARRAY) return NULL; + zval *found = zend_symtable_find(Z_ARRVAL_P(array), key); + if (found != NULL) { + ZVAL_DEREF(found); + } + return found; +} + +/* $array[$key] = $value on a property slot holding an array */ +void setIn(zval *array, zend_string *key, zv::Val value) +{ + SEPARATE_ARRAY(array); + zval v = value.take(); + zend_symtable_update(Z_ARRVAL_P(array), key, &v); +} + +/* $array[$key][$subKey] = $value, creating the inner array when missing */ +void setNested(zval *array, zend_string *key, zend_string *subKey, zv::Val value) +{ + SEPARATE_ARRAY(array); + zval *inner = zend_symtable_find(Z_ARRVAL_P(array), key); + if (inner != NULL) { + ZVAL_DEREF(inner); + } + if (inner == NULL || Z_TYPE_P(inner) != IS_ARRAY) { + zval fresh; + array_init(&fresh); + inner = zend_symtable_update(Z_ARRVAL_P(array), key, &fresh); + } else { + SEPARATE_ARRAY(inner); + } + zval v = value.take(); + zend_symtable_update(Z_ARRVAL_P(inner), subKey, &v); +} + +/* isset($array[$key][$subKey]) — the value or NULL */ +zval *issetNested(zval *array, zend_string *key, zend_string *subKey) +{ + zval *inner = issetIn(array, key); + if (inner == NULL) return NULL; + return issetIn(inner, subKey); +} + +/* unset($array[$key]) */ +void unsetIn(zval *array, zend_string *key) +{ + if (Z_TYPE_P(array) != IS_ARRAY) return; + SEPARATE_ARRAY(array); + zend_symtable_del(Z_ARRVAL_P(array), key); +} + +/* a string-returning call ($x->getName(), ->toString(), …); the Str is + * null with an exception pending */ +zv::Str callString(zval *object, const char *lcname, size_t len, uint32_t argc = 0, zval *argv = NULL) +{ + zv::Val result = pt_type_call(Z_OBJ_P(object), lcname, len, argc, argv); + if (UNEXPECTED(result.isUndef()) || Z_TYPE_P(result.raw()) != IS_STRING) return zv::Str::adopt(NULL); + return zv::Str::adopt(zend_string_copy(Z_STR_P(result.raw()))); +} + +/* }}} */ + +/* {{{ the BetterReflection adapter's member memos + * + * hasMethod() and hasProperty() are what this service is asked for most + * (0.96M calls in a self-analysis of src/Analyser, src/Rules and src/Type) + * and their whole body is + * $classReflection->getNativeReflection()->has*($name) — three PHP frames + * around one array lookup: + * + * Adapter\ReflectionClass::hasMethod() '' => false, else delegate + * ReflectionClass::hasMethod() getMethod() !== null + * ReflectionClass::getMethod() ($this->cachedMethods ?? compute())[strtolower($name)] ?? null + * + * (hasProperty() is the same shape over $cachedProperties, keyed by the + * exact name, and getMethod() adds `new Adapter\ReflectionMethod($m)`.) + * + * The memos are private properties of + * PHPStan\BetterReflection\Reflection\ReflectionClass filled lazily, so + * these readers answer from them once they are filled and leave every + * other case — an unfilled memo, an adapter of some other class, the + * empty name, the exception getMethod() raises for a missing method — to + * the adapter's own methods. The offsets are resolved on the declaring + * class entry, which makes them right for ReflectionEnum too (it extends + * ReflectionClass, and inherited slots keep their offsets); they are + * per-request, like every other class-entry cache here. */ + +struct AdapterSlots +{ + bool usable; /* the class adapter and the two memo offsets are resolved */ + bool unusable; /* a library whose shape these readers do not know */ + zend_class_entry *classAdapterCe; + uint32_t classAdapterOffset; + bool enumResolved; + zend_class_entry *enumAdapterCe; + uint32_t enumAdapterOffset; + zend_class_entry *reflectionClassCe; + uint32_t cachedMethodsOffset; + uint32_t cachedPropertiesOffset; +}; + +AdapterSlots pt_pcre_adapter_slots = { false, false, NULL, 0, false, NULL, 0, NULL, 0, 0 }; + +/* + * The classes are looked up without autoloading, so a class that is simply + * not declared yet is retried on the next call — only a library that + * declares the classes without the memo properties latches the readers off + * for the request. + */ +const AdapterSlots *adapterSlots() +{ + if (EXPECTED(pt_pcre_adapter_slots.usable)) return &pt_pcre_adapter_slots; + if (pt_pcre_adapter_slots.unusable) return NULL; + zend_class_entry *classAdapter = pt_class_loaded(PT_CLASS_ADAPTER_REFLECTION_CLASS); + zend_class_entry *reflectionClass = pt_class_loaded(PT_CLASS_BETTER_REFLECTION_CLASS); + if (EG(exception) != NULL || classAdapter == NULL || reflectionClass == NULL) return NULL; + int32_t classAdapterOffset = pt_instance_prop_offset(classAdapter, PT_LC("betterReflectionClass")); + int32_t cachedMethods = pt_instance_prop_offset(reflectionClass, PT_LC("cachedMethods")); + int32_t cachedProperties = pt_instance_prop_offset(reflectionClass, PT_LC("cachedProperties")); + if (classAdapterOffset < 0 || cachedMethods < 0 || cachedProperties < 0) { + /* not the library these readers know: every call goes through the + * adapter's methods */ + pt_pcre_adapter_slots.unusable = true; + return NULL; + } + pt_pcre_adapter_slots.classAdapterCe = classAdapter; + pt_pcre_adapter_slots.classAdapterOffset = (uint32_t) classAdapterOffset; + pt_pcre_adapter_slots.reflectionClassCe = reflectionClass; + pt_pcre_adapter_slots.cachedMethodsOffset = (uint32_t) cachedMethods; + pt_pcre_adapter_slots.cachedPropertiesOffset = (uint32_t) cachedProperties; + pt_pcre_adapter_slots.usable = true; + return &pt_pcre_adapter_slots; +} + +/* the enum adapter's class entry, resolved on the first enum reflection + * (the class is usually declared later than the class adapter) */ +zend_class_entry *enumAdapterCe() +{ + if (EXPECTED(pt_pcre_adapter_slots.enumResolved)) return pt_pcre_adapter_slots.enumAdapterCe; + zend_class_entry *ce = pt_class_loaded(PT_CLASS_REFLECTION_ENUM); + if (EG(exception) != NULL || ce == NULL) return NULL; + int32_t offset = pt_instance_prop_offset(ce, PT_LC("betterReflectionEnum")); + pt_pcre_adapter_slots.enumResolved = true; + pt_pcre_adapter_slots.enumAdapterCe = offset < 0 ? NULL : ce; + pt_pcre_adapter_slots.enumAdapterOffset = offset < 0 ? 0 : (uint32_t) offset; + return pt_pcre_adapter_slots.enumAdapterCe; +} + +/* the better-reflection class behind an adapter, or NULL when the adapter + * is not one of the two this knows */ +zend_object *betterReflectionOf(zval *adapter) +{ + const AdapterSlots *slots = adapterSlots(); + if (UNEXPECTED(slots == NULL) || Z_TYPE_P(adapter) != IS_OBJECT) return NULL; + zend_class_entry *ce = Z_OBJCE_P(adapter); + uint32_t offset; + if (EXPECTED(ce == slots->classAdapterCe)) { + offset = slots->classAdapterOffset; + } else if (ce == enumAdapterCe()) { + offset = pt_pcre_adapter_slots.enumAdapterOffset; + } else { + return NULL; + } + zval *inner = OBJ_PROP(Z_OBJ_P(adapter), offset); + ZVAL_DEINDIRECT(inner); + if (UNEXPECTED(Z_TYPE_P(inner) != IS_OBJECT) || !instanceof_function(Z_OBJCE_P(inner), slots->reflectionClassCe)) return NULL; + return Z_OBJ_P(inner); +} + +/* $betterReflection->cachedMethods / ->cachedProperties once the library + * filled it, NULL while it is still null */ +zval *adapterMemo(zval *adapter, bool methods) +{ + zend_object *betterReflection = betterReflectionOf(adapter); + if (betterReflection == NULL) return NULL; + const AdapterSlots *slots = &pt_pcre_adapter_slots; + uint32_t offset = methods ? slots->cachedMethodsOffset : slots->cachedPropertiesOffset; + zval *memo = OBJ_PROP(betterReflection, offset); + ZVAL_DEINDIRECT(memo); + return Z_TYPE_P(memo) == IS_ARRAY ? memo : NULL; +} + +/* $methods[strtolower($name)] ?? null over the lowercased-name memo */ +zval *memoFindLowercased(zval *memo, zend_string *name) +{ + size_t len = ZSTR_LEN(name); + char buffer[128]; + if (EXPECTED(len < sizeof(buffer))) { + zend_str_tolower_copy(buffer, ZSTR_VAL(name), len); + zval *found = zend_symtable_str_find(Z_ARRVAL_P(memo), buffer, len); + return found != NULL && Z_TYPE_P(found) != IS_NULL ? found : NULL; + } + zend_string *lower = zend_string_tolower(name); + zval *found = zend_symtable_find(Z_ARRVAL_P(memo), lower); + zend_string_release(lower); + return found != NULL && Z_TYPE_P(found) != IS_NULL ? found : NULL; +} + +/* }}} */ + +} // namespace + +namespace { + +/* the constructor's values, borrowed; $memberCacheKeysMax is the one + * parameter the twin does not promote */ +struct ConstructorArgs +{ + zval *scopeFactory; + zval *phpDocsResolver; + zval *nodeScopeResolver; + zval *methodReflectionFactory; + zval *phpDocInheritanceResolver; + zval *deprecationProvider; + zval *annotationsMethodsClassReflectionExtension; + zval *annotationsPropertiesClassReflectionExtension; + zval *signatureMapProvider; + zval *parser; + zval *stubPhpDocProvider; + zval *reflectionProviderProvider; + zval *fileTypeMapper; + zval *attributeReflectionFactory; + zval *allowedConstantsMapProvider; + bool inferPrivatePropertyTypeFromConstructor; + zval *phpVersion; + zend_long memberCacheKeysMax; +}; + +} // namespace + +namespace phpstanturbo { + +/* + * Mirrors PHPStan\Reflection\Php\PhpClassReflectionExtension. State lives + * in the PHP object's property slots. Methods returning zv::Val use UNDEF + * to signal a pending exception, a legitimate PHP null is zv::Val::null(); + * methods returning bool with an `out` parameter return false on a pending + * exception. + */ +class PhpClassReflectionExtension +{ +public: + explicit PhpClassReflectionExtension(zend_object *self) : self(self) {} + + /* {{{ the slots */ + + zval *slot(uint32_t index) const { return OBJ_PROP_NUM(self, index); } + + /* }}} */ + + /* Mirrors __construct(): the promoted properties, then + * $this->memberCacheOrder = new LruCache($memberCacheKeysMax). + * false = pending exception */ + [[nodiscard]] static bool construct(zend_object *object, const ConstructorArgs &a) + { + static const struct { uint32_t slot; size_t offset; } promoted[] = { + { PT_PCRE_PROP_SCOPE_FACTORY, offsetof(ConstructorArgs, scopeFactory) }, + { PT_PCRE_PROP_PHP_DOCS_RESOLVER, offsetof(ConstructorArgs, phpDocsResolver) }, + { PT_PCRE_PROP_NODE_SCOPE_RESOLVER, offsetof(ConstructorArgs, nodeScopeResolver) }, + { PT_PCRE_PROP_METHOD_REFLECTION_FACTORY, offsetof(ConstructorArgs, methodReflectionFactory) }, + { PT_PCRE_PROP_PHP_DOC_INHERITANCE_RESOLVER, offsetof(ConstructorArgs, phpDocInheritanceResolver) }, + { PT_PCRE_PROP_DEPRECATION_PROVIDER, offsetof(ConstructorArgs, deprecationProvider) }, + { PT_PCRE_PROP_ANNOTATIONS_METHODS_EXTENSION, offsetof(ConstructorArgs, annotationsMethodsClassReflectionExtension) }, + { PT_PCRE_PROP_ANNOTATIONS_PROPERTIES_EXTENSION, offsetof(ConstructorArgs, annotationsPropertiesClassReflectionExtension) }, + { PT_PCRE_PROP_SIGNATURE_MAP_PROVIDER, offsetof(ConstructorArgs, signatureMapProvider) }, + { PT_PCRE_PROP_PARSER, offsetof(ConstructorArgs, parser) }, + { PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER, offsetof(ConstructorArgs, stubPhpDocProvider) }, + { PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER, offsetof(ConstructorArgs, reflectionProviderProvider) }, + { PT_PCRE_PROP_FILE_TYPE_MAPPER, offsetof(ConstructorArgs, fileTypeMapper) }, + { PT_PCRE_PROP_ATTRIBUTE_REFLECTION_FACTORY, offsetof(ConstructorArgs, attributeReflectionFactory) }, + { PT_PCRE_PROP_ALLOWED_CONSTANTS_MAP_PROVIDER, offsetof(ConstructorArgs, allowedConstantsMapProvider) }, + { PT_PCRE_PROP_PHP_VERSION, offsetof(ConstructorArgs, phpVersion) }, + }; + for (const auto &entry : promoted) { + zval *value = *(zval *const *) ((const char *) &a + entry.offset); + ZVAL_COPY(OBJ_PROP_NUM(object, entry.slot), value); + } + ZVAL_BOOL(OBJ_PROP_NUM(object, PT_PCRE_PROP_INFER_PRIVATE_PROPERTY_TYPE), a.inferPrivatePropertyTypeFromConstructor); + + zval lru; + if (UNEXPECTED(!pt_lru_cache_new(&lru, a.memberCacheKeysMax))) return false; + ZVAL_COPY_VALUE(OBJ_PROP_NUM(object, PT_PCRE_PROP_MEMBER_CACHE_ORDER), &lru); + return true; + } + + /* + * Mirrors touchMemberCacheKey(): moves the key to the most recently + * used position of the shared LRU and drops the evicted keys' entries + * from all four member caches. false = pending exception + */ + [[nodiscard]] bool touchMemberCacheKey(zend_string *cacheKey) + { + zval *order = slot(PT_PCRE_PROP_MEMBER_CACHE_ORDER); + zv::Val current = pt_lru_cache_get(order, cacheKey); + if (UNEXPECTED(current.isUndef())) return false; + if (Z_TYPE_P(current.raw()) != IS_NULL) return true; + + zval trueValue; + ZVAL_TRUE(&trueValue); + zv::Val evicted = pt_lru_cache_set(order, cacheKey, &trueValue, 0); + if (UNEXPECTED(evicted.isUndef())) return false; + if (Z_TYPE_P(evicted.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(evicted.raw())) == 0) return true; + for (zv::ArrayEntry entry : zv::ArrRef(evicted.raw())) { + zval *evictKey = entry.value().raw(); + if (UNEXPECTED(Z_TYPE_P(evictKey) != IS_STRING)) continue; + /* the key lives in the evicted list, which outlives the loop */ + zend_string *key = Z_STR_P(evictKey); + unsetIn(slot(PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS), key); + unsetIn(slot(PT_PCRE_PROP_NATIVE_METHODS), key); + unsetIn(slot(PT_PCRE_PROP_PROPERTIES_INCLUDING_ANNOTATIONS), key); + unsetIn(slot(PT_PCRE_PROP_NATIVE_PROPERTIES), key); + } + return true; + } + + /* Mirrors hasProperty(). false with `ok` cleared = pending exception */ + [[nodiscard]] bool hasProperty(zval *classReflection, zend_string *propertyName, bool &ok) + { + zv::Val nativeReflection = pt_class_reflection_get_native_reflection(Z_OBJ_P(classReflection)); + if (UNEXPECTED(nativeReflection.isUndef())) { + ok = false; + return false; + } + zval *memo = adapterMemo(nativeReflection.raw(), false); + if (EXPECTED(memo != NULL)) { + ok = true; + if (ZSTR_LEN(propertyName) == 0) return false; + zval *found = zend_symtable_find(Z_ARRVAL_P(memo), propertyName); + return found != NULL && Z_TYPE_P(found) != IS_NULL; + } + zval name; + ZVAL_STR(&name, propertyName); + return callBool(nativeReflection.raw(), PT_LC("hasproperty"), 1, &name, ok); + } + + /* Mirrors getProperty(). */ + zv::Val getProperty(zval *classReflection, zend_string *propertyName, zval *scope) + { + zv::Val classCacheKey = pt_class_reflection_get_cache_key(Z_OBJ_P(classReflection)); + if (UNEXPECTED(classCacheKey.isUndef())) return zv::Val(); + zv::Str cacheKey = zv::Str::copyOf(Z_STR_P(classCacheKey.raw())); + + bool isInClass; + if (UNEXPECTED(!pt_scope_is_in_class(Z_OBJ_P(scope), isInClass))) return zv::Val(); + if (isInClass) { + zv::Val scopeClassReflection = pt_scope_get_class_reflection(Z_OBJ_P(scope)); + if (UNEXPECTED(scopeClassReflection.isUndef())) return zv::Val(); + zv::Val scopeCacheKey = pt_class_reflection_get_cache_key(Z_OBJ_P(scopeClassReflection.raw())); + if (UNEXPECTED(scopeCacheKey.isUndef())) return zv::Val(); + cacheKey = zv::Str::adopt(zend_strpprintf(0, "%s-%s", ZSTR_VAL(cacheKey.get()), Z_STRVAL_P(scopeCacheKey.raw()))); + } + + if (UNEXPECTED(!touchMemberCacheKey(cacheKey.get()))) return zv::Val(); + zval *cache = slot(PT_PCRE_PROP_PROPERTIES_INCLUDING_ANNOTATIONS); + zval *cached = issetNested(cache, cacheKey.get(), propertyName); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val property = createProperty(classReflection, propertyName, scope, true); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + zv::Val result = zv::Val::copyOf(zv::Ref(property.raw())); + setNested(slot(PT_PCRE_PROP_PROPERTIES_INCLUDING_ANNOTATIONS), cacheKey.get(), propertyName, std::move(property)); + return result; + } + + /* Mirrors getNativeProperty(). */ + zv::Val getNativeProperty(zval *classReflection, zend_string *propertyName) + { + zv::Val classCacheKey = pt_class_reflection_get_cache_key(Z_OBJ_P(classReflection)); + if (UNEXPECTED(classCacheKey.isUndef())) return zv::Val(); + zv::Str cacheKey = zv::Str::copyOf(Z_STR_P(classCacheKey.raw())); + if (UNEXPECTED(!touchMemberCacheKey(cacheKey.get()))) return zv::Val(); + + zval *cached = issetNested(slot(PT_PCRE_PROP_NATIVE_PROPERTIES), cacheKey.get(), propertyName); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val outOfClassScope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL); + if (UNEXPECTED(outOfClassScope.isUndef())) return zv::Val(); + zv::Val property = createProperty(classReflection, propertyName, outOfClassScope.raw(), false); + if (UNEXPECTED(property.isUndef())) return zv::Val(); + zv::Val result = zv::Val::copyOf(zv::Ref(property.raw())); + setNested(slot(PT_PCRE_PROP_NATIVE_PROPERTIES), cacheKey.get(), propertyName, std::move(property)); + return result; + } + + /* + * `$value instanceof ` — the native class entry + * first, and under the prefixed activation of the differential tests + * also the PHP twin that still carries the real name (the container's + * Type objects are the twins there). In a production run the native + * class carries the real name and the second lookup never runs. + */ + static bool instanceOfShadowed(zval *value, zend_class_entry *ce, const char *realName, size_t len) + { + if (Z_TYPE_P(value) != IS_OBJECT) return false; + if (ce != NULL && instanceof_function(Z_OBJCE_P(value), ce)) return true; + if (ce != NULL && zend_string_equals_cstr(ce->name, realName, len)) return false; + zend_string *name = zend_string_init(realName, len, 0); + zend_class_entry *twin = zend_lookup_class_ex(name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_string_release(name); + return twin != NULL && instanceof_function(Z_OBJCE_P(value), twin); + } + + /* + * Class::method(...$args) by the class's REAL name: the native class in + * a production run, the PHP twin's under the prefixed activation of the + * differential tests. The singletons below all end up in typed + * parameters of PHP collaborators (PhpMethodReflection's TrinaryLogic + * maps, the factory's TemplateTypeMap, StaticType's positionVariance), + * which would refuse a PHPStanTurbo\* instance there. UNDEF = pending + * exception. + */ + static zv::Val kernelStatic(const char *className, size_t classLen, const char *lcmethod, size_t methodLen, uint32_t argc = 0, zval *argv = NULL) + { + zend_string *name = zend_string_init(className, classLen, 0); + zend_class_entry *ce = zend_lookup_class(name); + zend_string_release(name); + if (UNEXPECTED(ce == NULL)) { + if (!EG(exception)) { + zend_throw_error(NULL, "phpstan_turbo: class %s not found", className); + } + return zv::Val(); + } + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcmethod, methodLen); + if (UNEXPECTED(fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", className, lcmethod); + return zv::Val(); + } + zval result; + zend_call_known_function(fn, NULL, ce, &result, argc, argv, NULL); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&result); + return zv::Val(); + } + return zv::Val::adopt(result); + } + + /* + * new (...$args) by the class's REAL name, for the same reason as + * kernelStatic(): the native class in a production run, the PHP twin + * under the prefixed activation — the Type instances built here end up + * in PHP collaborators that test them with `instanceof` + * (PhpPropertyReflection::hasNativeType() against MixedType), which a + * PHPStanTurbo\* instance would answer differently there. Every site is + * cold (member construction is memoized). UNDEF = pending exception. + */ + static zv::Val kernelNew(const char *className, size_t classLen, uint32_t argc = 0, zval *argv = NULL) + { + zend_string *name = zend_string_init(className, classLen, 0); + zend_class_entry *ce = zend_lookup_class(name); + zend_string_release(name); + if (UNEXPECTED(ce == NULL)) { + if (!EG(exception)) { + zend_throw_error(NULL, "phpstan_turbo: class %s not found", className); + } + return zv::Val(); + } + zval object; + if (UNEXPECTED(object_init_ex(&object, ce) != SUCCESS)) return zv::Val(); + if (ce->constructor != NULL) { + zend_call_known_instance_method(ce->constructor, Z_OBJ(object), NULL, argc, argv); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&object); + return zv::Val(); + } + } + return zv::Val::adopt(object); + } + + /* Class::method(...$args) by the real name, arguments spread from a PHP + * list (TypeCombinator::union(...$types)) */ + static zv::Val kernelStaticSpread(const char *className, size_t classLen, const char *lcmethod, size_t methodLen, HashTable *args) + { + uint32_t argc = zend_hash_num_elements(args); + std::vector argv; + argv.reserve(argc); + for (zv::ArrayEntry entry : zv::TableRef(args)) { + argv.push_back(*entry.value().raw()); + } + return kernelStatic(className, classLen, lcmethod, methodLen, argc, argv.empty() ? NULL : argv.data()); + } + + /* TypehintHelper::decideTypeFromReflection($reflectionType, null, + * $selfClass) / ::decideType($type, $phpDocType) by the real name: the + * native helper builds its unions out of the native Type family, which + * the PHP twins next to it cannot describe under the prefixed + * activation (a native VerbosityLevel reaches a PHP Type::describe()). + * In a production run the real name IS the native class. */ + static zv::Val decideTypeFromReflection(zval *reflectionType, zval *selfClass) + { + zv::Args args{reflectionType, zv::null, selfClass}; + return kernelStatic(PT_LC("PHPStan\\Type\\TypehintHelper"), PT_LC("decidetypefromreflection"), 3, args); + } + + static zv::Val mixedType() { return kernelNew(PT_LC("PHPStan\\Type\\MixedType")); } + + static zv::Val explicitMixedType() + { + zval isExplicit; + ZVAL_TRUE(&isExplicit); + return kernelNew(PT_LC("PHPStan\\Type\\MixedType"), 1, &isExplicit); + } + + static zv::Val stringType() { return kernelNew(PT_LC("PHPStan\\Type\\StringType")); } + + static zv::Val constantStringType(zend_string *value) + { + zval arg; + ZVAL_STR(&arg, value); + return kernelNew(PT_LC("PHPStan\\Type\\Constant\\ConstantStringType"), 1, &arg); + } + + static zv::Val enumCaseObjectType(zend_string *className, zend_string *caseName) + { + zv::Args args{className, caseName}; + return kernelNew(PT_LC("PHPStan\\Type\\Enum\\EnumCaseObjectType"), 2, args); + } + + static zv::Val arrayType(zval *keyType, zval *itemType) + { + zv::Args args{keyType, itemType}; + return kernelNew(PT_LC("PHPStan\\Type\\ArrayType"), 2, args); + } + + static zv::Val trinaryMaybe() { return kernelStatic(PT_LC("PHPStan\\TrinaryLogic"), PT_LC("createmaybe")); } + static zv::Val trinaryNo() { return kernelStatic(PT_LC("PHPStan\\TrinaryLogic"), PT_LC("createno")); } + static zv::Val trinaryFromBoolean(bool value) + { + return value + ? kernelStatic(PT_LC("PHPStan\\TrinaryLogic"), PT_LC("createyes")) + : kernelStatic(PT_LC("PHPStan\\TrinaryLogic"), PT_LC("createno")); + } + static zv::Val varianceInvariant() { return kernelStatic(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeVariance"), PT_LC("createinvariant")); } + static zv::Val varianceCovariant() { return kernelStatic(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeVariance"), PT_LC("createcovariant")); } + static zv::Val varianceContravariant() { return kernelStatic(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeVariance"), PT_LC("createcontravariant")); } + static zv::Val templateTypeMapEmpty() { return kernelStatic(PT_LC("PHPStan\\Type\\Generic\\TemplateTypeMap"), PT_LC("createempty")); } + + static bool isMixedType(zval *value) { return instanceOfShadowed(value, pt_ce_mixed_type, PT_LC("PHPStan\\Type\\MixedType")); } + static bool isTemplateMixedType(zval *value) { return instanceOfShadowed(value, pt_ce_template_mixed_type, PT_LC("PHPStan\\Type\\Generic\\TemplateMixedType")); } + static bool isErrorType(zval *value) { return instanceOfShadowed(value, pt_ce_error_type, PT_LC("PHPStan\\Type\\ErrorType")); } + static bool isNeverType(zval *value) { return instanceOfShadowed(value, pt_ce_never_type, PT_LC("PHPStan\\Type\\NeverType")); } + static bool isUnionType(zval *value) { return instanceOfShadowed(value, pt_ce_union_type, PT_LC("PHPStan\\Type\\UnionType")); } + + /* $type->isSuperTypeOf($other)->yes(); false with `ok` cleared on a + * pending exception */ + static bool isSuperTypeOfYes(zval *type, zval *other, bool &ok) + { + zv::Val result = pt_type_op(Z_OBJ_P(type), PT_OP_IS_SUPER_TYPE_OF, 1, other); + if (UNEXPECTED(result.isUndef())) { + ok = false; + return false; + } + zend_long value = pt_type_result_trinary(result.raw()); + if (UNEXPECTED(value < 0)) { + ok = false; + return false; + } + ok = true; + return value == PT_TRI_YES; + } + + /* $type->isVoid()->yes() / ->isConstantArray()->yes() */ + static bool typeOpYes(zval *type, pt_type_op_id op, bool &ok) + { + zend_long value = pt_type_op_trinary(Z_OBJ_P(type), op, 0, NULL); + if (UNEXPECTED(value < 0)) { + ok = false; + return false; + } + ok = true; + return value == PT_TRI_YES; + } + + /* the ?string a getDocComment() returns as string|false */ + static zv::Val docCommentOf(zval *reflection) + { + zv::Val docComment = call(reflection, PT_LC("getdoccomment")); + if (UNEXPECTED(docComment.isUndef())) return zv::Val(); + if (Z_TYPE_P(docComment.raw()) != IS_STRING) return zv::Val::null(); + return docComment; + } + + /* $this->fileTypeMapper->getResolvedPhpDoc($fileName, $className, $traitName, $functionName, $docComment) */ + zv::Val getResolvedPhpDoc(zval *fileName, zval *className, zval *traitName, zval *functionName, zval *docComment) + { + zv::Args args{fileName, className, traitName, functionName, docComment}; + return call(slot(PT_PCRE_PROP_FILE_TYPE_MAPPER), PT_LC("getresolvedphpdoc"), 5, args); + } + + /* InitializerExprContext::fromClass($className, $fileName) */ + static zv::Val initializerExprContextFromClass(zval *className, zval *fileName) + { + zv::Args args{className, fileName}; + return pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclass"), 2, args); + } + + /* $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), $context) */ + zv::Val attributesOf(zval *reflection, zval *context) + { + zv::Val attributes = call(reflection, PT_LC("getattributes")); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + zv::Args args{attributes.raw(), context}; + return call(slot(PT_PCRE_PROP_ATTRIBUTE_REFLECTION_FACTORY), PT_LC("fromnativereflection"), 2, args); + } + + /* the first of a @var tag list the twin picks: $varTags[0] when it is + * the only one, else $varTags[$propertyName]; NULL for neither */ + static zval *varTagFor(zval *varTags, zend_string *propertyName) + { + if (Z_TYPE_P(varTags) != IS_ARRAY) return NULL; + zval *first = zend_hash_index_find(Z_ARRVAL_P(varTags), 0); + if (first != NULL) { + ZVAL_DEREF(first); + if (Z_TYPE_P(first) != IS_NULL && zend_hash_num_elements(Z_ARRVAL_P(varTags)) == 1) return first; + } + return issetIn(varTags, propertyName); + } + + /* Mirrors createProperty(). */ + zv::Val createProperty(zval *classReflection, zend_string *requestedPropertyName, zval *scope, bool includingAnnotations) + { + bool ok; + zv::Val nativeReflection = pt_class_reflection_get_native_reflection(Z_OBJ_P(classReflection)); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + zval requestedNameArg; + ZVAL_STR(&requestedNameArg, requestedPropertyName); + zv::Val propertyReflection = call(nativeReflection.raw(), PT_LC("getproperty"), 1, &requestedNameArg); + if (UNEXPECTED(propertyReflection.isUndef())) return zv::Val(); + + zv::Str propertyNameStr = callString(propertyReflection.raw(), PT_LC("getname")); + if (UNEXPECTED(propertyNameStr.isNull())) return zv::Val(); + zend_string *propertyName = propertyNameStr.get(); + zval propertyNameArg; + ZVAL_STR(&propertyNameArg, propertyName); + + zv::Val propertyDeclaringClass = call(propertyReflection.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(propertyDeclaringClass.isUndef())) return zv::Val(); + zv::Str declaringClassNameStr = callString(propertyDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(declaringClassNameStr.isNull())) return zv::Val(); + zval declaringClassNameArg; + ZVAL_STR(&declaringClassNameArg, declaringClassNameStr.get()); + + zv::Val declaringClassReflection = call(classReflection, PT_LC("getancestorwithclassname"), 1, &declaringClassNameArg); + if (UNEXPECTED(declaringClassReflection.isUndef())) return zv::Val(); + if (Z_TYPE_P(declaringClassReflection.raw()) != IS_OBJECT) { + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(classReflection)); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Str message = ancestorMessage(declaringClassNameStr.get(), Z_STR_P(className.raw())); + throwShouldNotHappenStr(message.get()); + return zv::Val(); + } + zval *declaringClass = declaringClassReflection.raw(); + + bool supportsEnums = callBool(slot(PT_PCRE_PROP_PHP_VERSION), PT_LC("supportsenums"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool isNameProperty = zend_string_equals_literal(propertyName, "name"); + bool isUnitEnumInterfaceNameProperty = supportsEnums + && isNameProperty + && zend_string_equals_literal(declaringClassNameStr.get(), "UnitEnum"); + + bool declaringIsEnum; + if (UNEXPECTED(!pt_class_reflection_is_enum(Z_OBJ_P(declaringClass), declaringIsEnum))) return zv::Val(); + if (declaringIsEnum || isUnitEnumInterfaceNameProperty) { + bool enumMemberProperty = isNameProperty; + if (!enumMemberProperty) { + bool isBackedEnum = callBool(declaringClass, PT_LC("isbackedenum"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + enumMemberProperty = isBackedEnum && zend_string_equals_literal(propertyName, "value"); + } + if (enumMemberProperty) { + zv::Val phpDocType; + zv::Val nativeType; + if (declaringIsEnum) { + zv::Val enumCases = call(classReflection, PT_LC("getenumcases")); + if (UNEXPECTED(enumCases.isUndef()) || Z_TYPE_P(enumCases.raw()) != IS_ARRAY) return zv::Val(); + zv::Arr types = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(enumCases.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(enumCases.raw())) { + if (isNameProperty) { + zend_string *caseName = entry.stringKeyOrNull(); + zv::Str owned; + if (caseName == NULL) { + owned = zv::Str::adopt(zend_long_to_str((zend_long) entry.indexKey())); + caseName = owned.get(); + } + zv::Val constantString = constantStringType(caseName); + if (UNEXPECTED(constantString.isUndef())) return zv::Val(); + types.push(std::move(constantString)); + continue; + } + zv::Val backingValue = call(entry.value().raw(), PT_LC("getbackingvaluetype")); + if (UNEXPECTED(backingValue.isUndef())) return zv::Val(); + if (Z_TYPE_P(backingValue.raw()) == IS_NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + types.push(zv::Ref(backingValue.raw())); + } + phpDocType = kernelStaticSpread(PT_LC("PHPStan\\Type\\TypeCombinator"), PT_LC("union"), types.table()); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + nativeType = mixedType(); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + } else { + zv::Val ownedString = stringType(); + if (UNEXPECTED(ownedString.isUndef())) return zv::Val(); + zv::Val ownedNonFalsy = kernelNew(PT_LC("PHPStan\\Type\\Accessory\\AccessoryNonFalsyStringType")); + if (UNEXPECTED(ownedNonFalsy.isUndef())) return zv::Val(); + zval inverse; + ZVAL_TRUE(&inverse); + zv::Val ownedDecimal = kernelNew(PT_LC("PHPStan\\Type\\Accessory\\AccessoryDecimalIntegerStringType"), 1, &inverse); + if (UNEXPECTED(ownedDecimal.isUndef())) return zv::Val(); + zv::Args parts{ownedString.raw(), ownedNonFalsy.raw(), ownedDecimal.raw()}; + phpDocType = kernelStatic(PT_LC("PHPStan\\Type\\TypeCombinator"), PT_LC("intersect"), 3, parts); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + nativeType = stringType(); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + } + + zv::Val reflectionProperty = call(nativeReflection.raw(), PT_LC("getproperty"), 1, &propertyNameArg); + if (UNEXPECTED(reflectionProperty.isUndef())) return zv::Val(); + zval args[20]; + ZVAL_COPY_VALUE(&args[0], declaringClass); + ZVAL_NULL(&args[1]); + ZVAL_COPY_VALUE(&args[2], nativeType.raw()); + ZVAL_COPY_VALUE(&args[3], phpDocType.raw()); + ZVAL_COPY_VALUE(&args[4], phpDocType.raw()); + ZVAL_COPY_VALUE(&args[5], reflectionProperty.raw()); + ZVAL_NULL(&args[6]); + ZVAL_NULL(&args[7]); + ZVAL_NULL(&args[8]); + ZVAL_NULL(&args[9]); + ZVAL_FALSE(&args[10]); + ZVAL_FALSE(&args[11]); + ZVAL_FALSE(&args[12]); + ZVAL_FALSE(&args[13]); + ZVAL_EMPTY_ARRAY(&args[14]); + ZVAL_FALSE(&args[15]); + ZVAL_TRUE(&args[16]); + ZVAL_FALSE(&args[17]); + ZVAL_FALSE(&args[18]); + ZVAL_TRUE(&args[19]); + return pt_type_new(PT_CLASS_PHP_PROPERTY_REFLECTION, 20, args); + } + } + + zv::Val deprecation = call(slot(PT_PCRE_PROP_DEPRECATION_PROVIDER), PT_LC("getpropertydeprecation"), 1, propertyReflection.raw()); + if (UNEXPECTED(deprecation.isUndef())) return zv::Val(); + bool isDeprecated = Z_TYPE_P(deprecation.raw()) != IS_NULL; + zv::Val deprecatedDescription = zv::Val::null(); + if (isDeprecated) { + deprecatedDescription = call(deprecation.raw(), PT_LC("getdescription")); + if (UNEXPECTED(deprecatedDescription.isUndef())) return zv::Val(); + } + bool isInternal = false; + bool isReadOnlyByPhpDoc = callBool(classReflection, PT_LC("isimmutable"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool isFinal = callBool(classReflection, PT_LC("isfinal"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!isFinal) { + isFinal = callBool(propertyReflection.raw(), PT_LC("isfinal"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + bool isAllowedPrivateMutation = false; + + zv::Val docComment = docCommentOf(propertyReflection.raw()); + if (UNEXPECTED(docComment.isUndef())) return zv::Val(); + + zv::Val phpDocType = zv::Val::null(); + zv::Val resolvedPhpDoc = zv::Val::null(); + zv::Val declaringTraitName = findPropertyTrait(propertyReflection.raw()); + if (UNEXPECTED(declaringTraitName.isUndef())) return zv::Val(); + zv::Val constructorName = zv::Val::null(); + bool isPromoted = callBool(propertyReflection.raw(), PT_LC("ispromoted"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isPromoted) { + bool hasConstructor = callBool(declaringClass, PT_LC("hasconstructor"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (hasConstructor) { + zv::Val constructor = call(declaringClass, PT_LC("getconstructor")); + if (UNEXPECTED(constructor.isUndef())) return zv::Val(); + constructorName = call(constructor.raw(), PT_LC("getname")); + if (UNEXPECTED(constructorName.isUndef())) return zv::Val(); + } + } + + if (Z_TYPE_P(constructorName.raw()) == IS_NULL) { + zv::Args stubArgs{&declaringClassNameArg, &propertyNameArg}; + zv::Val currentResolvedPhpDoc = call(slot(PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER), PT_LC("findpropertyphpdoc"), 2, stubArgs); + if (UNEXPECTED(currentResolvedPhpDoc.isUndef())) return zv::Val(); + if (Z_TYPE_P(currentResolvedPhpDoc.raw()) == IS_NULL && Z_TYPE_P(declaringTraitName.raw()) != IS_NULL) { + ZVAL_COPY_VALUE(&stubArgs[0], declaringTraitName.raw()); + currentResolvedPhpDoc = call(slot(PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER), PT_LC("findpropertyphpdoc"), 2, stubArgs); + if (UNEXPECTED(currentResolvedPhpDoc.isUndef())) return zv::Val(); + } + if (Z_TYPE_P(currentResolvedPhpDoc.raw()) == IS_NULL && Z_TYPE_P(docComment.raw()) != IS_NULL) { + zv::Val fileName = call(declaringClass, PT_LC("getfilename")); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + zval nullArg = {}; + ZVAL_NULL(&nullArg); + currentResolvedPhpDoc = getResolvedPhpDoc(fileName.raw(), &declaringClassNameArg, declaringTraitName.raw(), &nullArg, docComment.raw()); + if (UNEXPECTED(currentResolvedPhpDoc.isUndef())) return zv::Val(); + } + zv::Args resolveArgs{declaringClass, &propertyNameArg, currentResolvedPhpDoc.raw()}; + resolvedPhpDoc = call(slot(PT_PCRE_PROP_PHP_DOC_INHERITANCE_RESOLVER), PT_LC("resolvephpdocforproperty"), 3, resolveArgs); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + } else if (Z_TYPE_P(docComment.raw()) != IS_NULL) { + zv::Val fileName = call(declaringClass, PT_LC("getfilename")); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + resolvedPhpDoc = getResolvedPhpDoc(fileName.raw(), &declaringClassNameArg, declaringTraitName.raw(), constructorName.raw(), docComment.raw()); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + } + + if (Z_TYPE_P(resolvedPhpDoc.raw()) != IS_NULL) { + zv::Val varTags = call(resolvedPhpDoc.raw(), PT_LC("getvartags")); + if (UNEXPECTED(varTags.isUndef())) return zv::Val(); + zval *varTag = varTagFor(varTags.raw(), propertyName); + if (varTag != NULL) { + phpDocType = call(varTag, PT_LC("gettype")); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + } + + if (Z_TYPE_P(phpDocType.raw()) != IS_NULL) { + zv::Val activeTemplateTypeMap = call(declaringClass, PT_LC("getactivetemplatetypemap")); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = call(declaringClass, PT_LC("getcallsitevariancemap")); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val invariant = varianceInvariant(); + if (UNEXPECTED(invariant.isUndef())) return zv::Val(); + phpDocType = pt_type_template_type_helper_resolve_template_types(phpDocType.raw(), activeTemplateTypeMap.raw(), callSiteVarianceMap.raw(), invariant.raw(), false); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + } + + if (!isDeprecated) { + zv::Val deprecatedTag = call(resolvedPhpDoc.raw(), PT_LC("getdeprecatedtag")); + if (UNEXPECTED(deprecatedTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(deprecatedTag.raw()) != IS_NULL) { + deprecatedDescription = call(deprecatedTag.raw(), PT_LC("getmessage")); + if (UNEXPECTED(deprecatedDescription.isUndef())) return zv::Val(); + } else { + deprecatedDescription = zv::Val::null(); + } + isDeprecated = callBool(resolvedPhpDoc.raw(), PT_LC("isdeprecated"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + isInternal = callBool(resolvedPhpDoc.raw(), PT_LC("isinternal"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!isReadOnlyByPhpDoc) { + isReadOnlyByPhpDoc = callBool(resolvedPhpDoc.raw(), PT_LC("isreadonly"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + if (!isFinal) { + isFinal = callBool(resolvedPhpDoc.raw(), PT_LC("isfinal"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + isAllowedPrivateMutation = callBool(resolvedPhpDoc.raw(), PT_LC("isallowedprivatemutation"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + + if (Z_TYPE_P(phpDocType.raw()) == IS_NULL && Z_TYPE_P(constructorName.raw()) != IS_NULL) { + zv::Val constructor = call(declaringClass, PT_LC("getconstructor")); + if (UNEXPECTED(constructor.isUndef())) return zv::Val(); + zv::Val resolvedConstructorPhpDoc = call(constructor.raw(), PT_LC("getresolvedphpdoc")); + if (UNEXPECTED(resolvedConstructorPhpDoc.isUndef())) return zv::Val(); + if (Z_TYPE_P(resolvedConstructorPhpDoc.raw()) != IS_NULL) { + zv::Val paramTags = call(resolvedConstructorPhpDoc.raw(), PT_LC("getparamtags")); + if (UNEXPECTED(paramTags.isUndef())) return zv::Val(); + zv::Str reflectionName = callString(propertyReflection.raw(), PT_LC("getname")); + if (UNEXPECTED(reflectionName.isNull())) return zv::Val(); + zval *paramTag = issetIn(paramTags.raw(), reflectionName.get()); + if (paramTag != NULL) { + phpDocType = call(paramTag, PT_LC("gettype")); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + } + } + } + + if (Z_TYPE_P(phpDocType.raw()) == IS_NULL && Z_TYPE_P(slot(PT_PCRE_PROP_INFER_PRIVATE_PROPERTY_TYPE)) == IS_TRUE) { + zv::Val fileName = call(declaringClass, PT_LC("getfilename")); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + bool eligible = Z_TYPE_P(fileName.raw()) != IS_NULL; + if (eligible) { + eligible = callBool(propertyReflection.raw(), PT_LC("isprivate"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + if (eligible) { + eligible = !isPromoted; + } + if (eligible) { + bool hasType = callBool(propertyReflection.raw(), PT_LC("hastype"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + eligible = !hasType; + } + if (eligible) { + eligible = callBool(declaringClass, PT_LC("hasconstructor"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + if (eligible) { + zv::Val constructor = call(declaringClass, PT_LC("getconstructor")); + if (UNEXPECTED(constructor.isUndef())) return zv::Val(); + zv::Val constructorDeclaringClass = call(constructor.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(constructorDeclaringClass.isUndef())) return zv::Val(); + zv::Val constructorDeclaringName = pt_class_reflection_get_name(Z_OBJ_P(constructorDeclaringClass.raw())); + if (UNEXPECTED(constructorDeclaringName.isUndef())) return zv::Val(); + zv::Val declaringName = pt_class_reflection_get_name(Z_OBJ_P(declaringClass)); + if (UNEXPECTED(declaringName.isUndef())) return zv::Val(); + if (zend_string_equals(Z_STR_P(constructorDeclaringName.raw()), Z_STR_P(declaringName.raw()))) { + zv::Str reflectionName = callString(propertyReflection.raw(), PT_LC("getname")); + if (UNEXPECTED(reflectionName.isNull())) return zv::Val(); + phpDocType = inferPrivatePropertyType(reflectionName.get(), constructor.raw()); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + } + } + } + + zv::Val reflectionType = call(propertyReflection.raw(), PT_LC("gettype")); + if (UNEXPECTED(reflectionType.isUndef())) return zv::Val(); + zv::Val nativeType = decideTypeFromReflection(reflectionType.raw(), declaringClass); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + + zv::Val declaringTrait = zv::Val::null(); + zv::Val reflectionProvider = call(slot(PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER), PT_LC("getreflectionprovider")); + if (UNEXPECTED(reflectionProvider.isUndef())) return zv::Val(); + if (Z_TYPE_P(declaringTraitName.raw()) != IS_NULL) { + bool hasClass; + if (UNEXPECTED(!pt_reflection_provider_has_class(Z_OBJ_P(reflectionProvider.raw()), declaringTraitName.raw(), hasClass))) return zv::Val(); + if (hasClass) { + declaringTrait = pt_reflection_provider_get_class(Z_OBJ_P(reflectionProvider.raw()), declaringTraitName.raw()); + if (UNEXPECTED(declaringTrait.isUndef())) return zv::Val(); + } + } + + zv::Val getHook = zv::Val::null(); + zv::Val setHook = zv::Val::null(); + zv::Val betterReflection = call(propertyReflection.raw(), PT_LC("getbetterreflection")); + if (UNEXPECTED(betterReflection.isUndef())) return zv::Val(); + static const char *const hookKinds[] = { "get", "set" }; + for (int kind = 0; kind < 2; kind++) { + zval hookKind; + ZVAL_STRING(&hookKind, hookKinds[kind]); + zv::Val ownedKind = zv::Val::adopt(hookKind); + bool hasHook = callBool(betterReflection.raw(), PT_LC("hashook"), 1, ownedKind.raw(), ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!hasHook) continue; + zv::Val betterReflectionHook = call(betterReflection.raw(), PT_LC("gethook"), 1, ownedKind.raw()); + if (UNEXPECTED(betterReflectionHook.isUndef())) return zv::Val(); + if (Z_TYPE_P(betterReflectionHook.raw()) == IS_NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + zv::Val adapterMethod = pt_type_new(PT_CLASS_ADAPTER_REFLECTION_METHOD, 1, betterReflectionHook.raw()); + if (UNEXPECTED(adapterMethod.isUndef())) return zv::Val(); + zv::Val hook = createUserlandMethodReflection(declaringClass, declaringClass, adapterMethod.raw(), declaringTraitName.raw()); + if (UNEXPECTED(hook.isUndef())) return zv::Val(); + + if (Z_TYPE_P(phpDocType.raw()) != IS_NULL) { + zv::Val variant = call(hook.raw(), PT_LC("getonlyvariant")); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + if (kind == 0) { + zv::Val returnType = call(variant.raw(), PT_LC("getphpdocreturntype")); + if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + if (isMixedType(returnType.raw()) && !isTemplateMixedType(returnType.raw())) { + bool explicitMixed = callBool(returnType.raw(), PT_LC("isexplicitmixed"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!explicitMixed) { + zv::Val changed = call(hook.raw(), PT_LC("changepropertygethookphpdoctype"), 1, phpDocType.raw()); + if (UNEXPECTED(changed.isUndef())) return zv::Val(); + hook = std::move(changed); + } + } + } else { + zv::Val parameters = call(variant.raw(), PT_LC("getparameters")); + if (UNEXPECTED(parameters.isUndef())) return zv::Val(); + zval *parameter = Z_TYPE_P(parameters.raw()) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(parameters.raw()), 0) : NULL; + if (parameter != NULL) { + ZVAL_DEREF(parameter); + } + if (parameter != NULL && Z_TYPE_P(parameter) != IS_NULL) { + zv::Val parameterPhpDocType = call(parameter, PT_LC("getphpdoctype")); + if (UNEXPECTED(parameterPhpDocType.isUndef())) return zv::Val(); + if (isMixedType(parameterPhpDocType.raw()) && !isTemplateMixedType(parameterPhpDocType.raw())) { + bool explicitMixed = callBool(parameterPhpDocType.raw(), PT_LC("isexplicitmixed"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!explicitMixed) { + zv::Val parameterName = call(parameter, PT_LC("getname")); + if (UNEXPECTED(parameterName.isUndef())) return zv::Val(); + zv::Args changeArgs{parameterName.raw(), phpDocType.raw()}; + zv::Val changed = call(hook.raw(), PT_LC("changepropertysethookphpdoctype"), 2, changeArgs); + if (UNEXPECTED(changed.isUndef())) return zv::Val(); + hook = std::move(changed); + } + } + } + } + } + + if (kind == 0) { + getHook = std::move(hook); + } else { + setHook = std::move(hook); + } + } + + // a property the phar build made public for its inlined getters keeps its source visibility here + bool isPrivate = callBool(propertyReflection.raw(), PT_LC("isprivate"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool isPublic = callBool(propertyReflection.raw(), PT_LC("ispublic"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isPublic) { + bool hasPrivateAttribute; + if (UNEXPECTED(!hasAttribute(propertyReflection.raw(), PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE, hasPrivateAttribute))) return zv::Val(); + if (hasPrivateAttribute) { + isPrivate = true; + isPublic = false; + } else { + bool hasProtectedAttribute; + if (UNEXPECTED(!hasAttribute(propertyReflection.raw(), PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE, hasProtectedAttribute))) return zv::Val(); + if (hasProtectedAttribute) { + isPublic = false; + } + } + } + + zv::Val declaringName = pt_class_reflection_get_name(Z_OBJ_P(declaringClass)); + if (UNEXPECTED(declaringName.isUndef())) return zv::Val(); + zv::Val declaringFileName = call(declaringClass, PT_LC("getfilename")); + if (UNEXPECTED(declaringFileName.isUndef())) return zv::Val(); + zv::Val context = initializerExprContextFromClass(declaringName.raw(), declaringFileName.raw()); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Val attributes = attributesOf(propertyReflection.raw(), context.raw()); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + + zval args[20]; + ZVAL_COPY_VALUE(&args[0], declaringClass); + ZVAL_COPY_VALUE(&args[1], declaringTrait.raw()); + ZVAL_COPY_VALUE(&args[2], nativeType.raw()); + ZVAL_COPY_VALUE(&args[3], phpDocType.raw()); + ZVAL_COPY_VALUE(&args[4], phpDocType.raw()); + ZVAL_COPY_VALUE(&args[5], propertyReflection.raw()); + ZVAL_COPY_VALUE(&args[6], getHook.raw()); + ZVAL_COPY_VALUE(&args[7], setHook.raw()); + ZVAL_COPY_VALUE(&args[8], resolvedPhpDoc.raw()); + ZVAL_COPY_VALUE(&args[9], deprecatedDescription.raw()); + ZVAL_BOOL(&args[10], isDeprecated); + ZVAL_BOOL(&args[11], isInternal); + ZVAL_BOOL(&args[12], isReadOnlyByPhpDoc); + ZVAL_BOOL(&args[13], isAllowedPrivateMutation); + ZVAL_COPY_VALUE(&args[14], attributes.raw()); + ZVAL_BOOL(&args[15], isFinal); + ZVAL_TRUE(&args[16]); + ZVAL_TRUE(&args[17]); + ZVAL_BOOL(&args[18], isPrivate); + ZVAL_BOOL(&args[19], isPublic); + zv::Val nativeProperty = pt_type_new(PT_CLASS_PHP_PROPERTY_REFLECTION, 20, args); + if (UNEXPECTED(nativeProperty.isUndef())) return zv::Val(); + + zv::Val annotationProperty = annotationPropertyFor(classReflection, propertyName, scope, includingAnnotations, declaringIsEnum, propertyReflection.raw(), nativeProperty.raw()); + if (UNEXPECTED(annotationProperty.isUndef())) return zv::Val(); + if (Z_TYPE_P(annotationProperty.raw()) == IS_NULL) return nativeProperty; + + /* the annotation property wins: the twin rebuilds the reflection + * from its types and the native property's resolved PHPDoc */ + zv::Val annotationReadableType = call(annotationProperty.raw(), PT_LC("getreadabletype")); + if (UNEXPECTED(annotationReadableType.isUndef())) return zv::Val(); + bool superTypeOfReadable = isSuperTypeOfYes(nativeType.raw(), annotationReadableType.raw(), ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool widenToMixed = superTypeOfReadable; + if (!widenToMixed) { + bool canRead = callBool(scope, PT_LC("canreadproperty"), 1, nativeProperty.raw(), ok); + if (UNEXPECTED(!ok)) return zv::Val(); + widenToMixed = !canRead; + } + if (widenToMixed) { + nativeType = mixedType(); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + } + + zv::Val annotationDeclaringClass = call(annotationProperty.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(annotationDeclaringClass.isUndef())) return zv::Val(); + zv::Val annotationWritableType = call(annotationProperty.raw(), PT_LC("getwritabletype")); + if (UNEXPECTED(annotationWritableType.isUndef())) return zv::Val(); + zv::Val nativeResolvedPhpDoc = call(nativeProperty.raw(), PT_LC("getresolvedphpdoc")); + if (UNEXPECTED(nativeResolvedPhpDoc.isUndef())) return zv::Val(); + zv::Val annotationContext = initializerExprContextFromClass(declaringName.raw(), declaringFileName.raw()); + if (UNEXPECTED(annotationContext.isUndef())) return zv::Val(); + zv::Val annotationAttributes = attributesOf(propertyReflection.raw(), annotationContext.raw()); + if (UNEXPECTED(annotationAttributes.isUndef())) return zv::Val(); + bool annotationReadable = callBool(annotationProperty.raw(), PT_LC("isreadable"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool annotationWritable = callBool(annotationProperty.raw(), PT_LC("iswritable"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + + zval annotationArgs[20]; + ZVAL_COPY_VALUE(&annotationArgs[0], annotationDeclaringClass.raw()); + ZVAL_COPY_VALUE(&annotationArgs[1], declaringTrait.raw()); + ZVAL_COPY_VALUE(&annotationArgs[2], nativeType.raw()); + ZVAL_COPY_VALUE(&annotationArgs[3], annotationReadableType.raw()); + ZVAL_COPY_VALUE(&annotationArgs[4], annotationWritableType.raw()); + ZVAL_COPY_VALUE(&annotationArgs[5], propertyReflection.raw()); + ZVAL_COPY_VALUE(&annotationArgs[6], getHook.raw()); + ZVAL_COPY_VALUE(&annotationArgs[7], setHook.raw()); + ZVAL_COPY_VALUE(&annotationArgs[8], nativeResolvedPhpDoc.raw()); + ZVAL_COPY_VALUE(&annotationArgs[9], deprecatedDescription.raw()); + ZVAL_BOOL(&annotationArgs[10], isDeprecated); + ZVAL_BOOL(&annotationArgs[11], isInternal); + ZVAL_BOOL(&annotationArgs[12], isReadOnlyByPhpDoc); + ZVAL_BOOL(&annotationArgs[13], isAllowedPrivateMutation); + ZVAL_COPY_VALUE(&annotationArgs[14], annotationAttributes.raw()); + ZVAL_BOOL(&annotationArgs[15], isFinal); + ZVAL_BOOL(&annotationArgs[16], annotationReadable); + ZVAL_BOOL(&annotationArgs[17], annotationWritable); + ZVAL_FALSE(&annotationArgs[18]); + ZVAL_TRUE(&annotationArgs[19]); + return pt_type_new(PT_CLASS_PHP_PROPERTY_REFLECTION, 20, annotationArgs); + } + + /* count($reflection->getAttributes()) > 0 */ + bool hasAttribute(zval *reflection, int classIdx, bool &out) + { + zend_class_entry *ce = pt_class(classIdx); + if (UNEXPECTED(ce == NULL)) return false; + zval nameArg; + ZVAL_STR(&nameArg, ce->name); + zv::Val attributes = call(reflection, PT_LC("getattributes"), 1, &nameArg); + if (UNEXPECTED(attributes.isUndef())) return false; + out = Z_TYPE_P(attributes.raw()) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(attributes.raw())) > 0; + return true; + } + + /* + * The annotation property createProperty() prefers over the native one: + * the twin's guard plus the hierarchy-distance comparison. PHP null + * means "keep the native property", UNDEF a pending exception. + */ + zv::Val annotationPropertyFor(zval *classReflection, zend_string *propertyName, zval *scope, bool includingAnnotations, bool declaringIsEnum, zval *propertyReflection, zval *nativeProperty) + { + bool ok; + if (!includingAnnotations || declaringIsEnum) return zv::Val::null(); + bool isStatic = callBool(propertyReflection, PT_LC("isstatic"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isStatic) return zv::Val::null(); + bool allowsDynamicProperties = callBool(classReflection, PT_LC("allowsdynamicproperties"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!allowsDynamicProperties) { + bool canRead = callBool(scope, PT_LC("canreadproperty"), 1, nativeProperty, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!canRead) return zv::Val::null(); + } + + zval propertyNameArg; + ZVAL_STR(&propertyNameArg, propertyName); + zv::Args extensionArgs{classReflection, &propertyNameArg}; + bool hasAnnotationProperty = callBool(slot(PT_PCRE_PROP_ANNOTATIONS_PROPERTIES_EXTENSION), PT_LC("hasproperty"), 2, extensionArgs, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!hasAnnotationProperty) return zv::Val::null(); + + /* the adapter's declaring class name; createProperty() looked + * $declaringClassReflection up with it through + * getAncestorWithClassName(), which answers out of an ancestor map + * keyed by the very same name — so this is also + * $declaringClassReflection->getName(), which the twin compares the + * scope's class against below */ + zv::Val propertyDeclaringClass = call(propertyReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(propertyDeclaringClass.isUndef())) return zv::Val(); + zv::Str propertyDeclaringClassName = callString(propertyDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(propertyDeclaringClassName.isNull())) return zv::Val(); + + bool nativeIsPublic = callBool(nativeProperty, PT_LC("ispublic"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!nativeIsPublic) { + bool isInClass; + if (UNEXPECTED(!pt_scope_is_in_class(Z_OBJ_P(scope), isInClass))) return zv::Val(); + if (isInClass) { + zv::Val scopeClassReflection = pt_scope_get_class_reflection(Z_OBJ_P(scope)); + if (UNEXPECTED(scopeClassReflection.isUndef())) return zv::Val(); + zv::Val scopeClassName = pt_class_reflection_get_name(Z_OBJ_P(scopeClassReflection.raw())); + if (UNEXPECTED(scopeClassName.isUndef())) return zv::Val(); + if (zend_string_equals(Z_STR_P(scopeClassName.raw()), propertyDeclaringClassName.get())) return zv::Val::null(); + } + } + + zv::Val hierarchyDistances = call(classReflection, PT_LC("getclasshierarchydistances")); + if (UNEXPECTED(hierarchyDistances.isUndef())) return zv::Val(); + zv::Val annotationProperty = call(slot(PT_PCRE_PROP_ANNOTATIONS_PROPERTIES_EXTENSION), PT_LC("getproperty"), 2, extensionArgs); + if (UNEXPECTED(annotationProperty.isUndef())) return zv::Val(); + zv::Val annotationDeclaringClass = call(annotationProperty.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(annotationDeclaringClass.isUndef())) return zv::Val(); + zv::Val annotationDeclaringClassName = pt_class_reflection_get_name(Z_OBJ_P(annotationDeclaringClass.raw())); + if (UNEXPECTED(annotationDeclaringClassName.isUndef())) return zv::Val(); + zval *annotationDistance = issetIn(hierarchyDistances.raw(), Z_STR_P(annotationDeclaringClassName.raw())); + if (annotationDistance == NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + + zv::Str distanceDeclaringClass = zv::Str::copyOf(propertyDeclaringClassName.get()); + zv::Val propertyTrait = findPropertyTrait(propertyReflection); + if (UNEXPECTED(propertyTrait.isUndef())) return zv::Val(); + if (Z_TYPE_P(propertyTrait.raw()) != IS_NULL) { + distanceDeclaringClass = zv::Str::copyOf(Z_STR_P(propertyTrait.raw())); + } + zval *nativeDistance = issetIn(hierarchyDistances.raw(), distanceDeclaringClass.get()); + if (nativeDistance == NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + + if (zval_get_long(annotationDistance) <= zval_get_long(nativeDistance)) return annotationProperty; + return zv::Val::null(); + } + + /* $adapter->getMethod($name) — the memo entry wrapped the way the + * adapter wraps it; the adapter's method (which raises its + * ReflectionException for a missing one) otherwise */ + static zv::Val adapterGetMethod(zval *adapter, zend_string *methodName) + { + zval *memo = adapterMemo(adapter, true); + if (EXPECTED(memo != NULL) && ZSTR_LEN(methodName) != 0) { + zval *found = memoFindLowercased(memo, methodName); + if (EXPECTED(found != NULL)) return pt_type_new(PT_CLASS_ADAPTER_REFLECTION_METHOD, 1, found); + } + zval name; + ZVAL_STR(&name, methodName); + return call(adapter, PT_LC("getmethod"), 1, &name); + } + + /* Mirrors hasMethod(). false with `ok` cleared = pending exception */ + [[nodiscard]] bool hasMethod(zval *classReflection, zend_string *methodName, bool &ok) + { + zv::Val nativeReflection = pt_class_reflection_get_native_reflection(Z_OBJ_P(classReflection)); + if (UNEXPECTED(nativeReflection.isUndef())) { + ok = false; + return false; + } + zval *memo = adapterMemo(nativeReflection.raw(), true); + if (EXPECTED(memo != NULL)) { + ok = true; + return ZSTR_LEN(methodName) != 0 && memoFindLowercased(memo, methodName) != NULL; + } + zval name; + ZVAL_STR(&name, methodName); + return callBool(nativeReflection.raw(), PT_LC("hasmethod"), 1, &name, ok); + } + + /* Mirrors getMethod(). */ + zv::Val getMethod(zval *classReflection, zend_string *methodName) + { + zv::Val classCacheKey = pt_class_reflection_get_cache_key(Z_OBJ_P(classReflection)); + if (UNEXPECTED(classCacheKey.isUndef())) return zv::Val(); + zv::Str cacheKey = zv::Str::copyOf(Z_STR_P(classCacheKey.raw())); + if (UNEXPECTED(!touchMemberCacheKey(cacheKey.get()))) return zv::Val(); + zval *cached = issetNested(slot(PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS), cacheKey.get(), methodName); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val nativeReflection = pt_class_reflection_get_native_reflection(Z_OBJ_P(classReflection)); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + zv::Val nativeMethodReflection = adapterGetMethod(nativeReflection.raw(), methodName); + if (UNEXPECTED(nativeMethodReflection.isUndef())) return zv::Val(); + zv::Str realName = callString(nativeMethodReflection.raw(), PT_LC("getname")); + if (UNEXPECTED(realName.isNull())) return zv::Val(); + + zval *cachedByRealName = issetNested(slot(PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS), cacheKey.get(), realName.get()); + if (cachedByRealName != NULL) return zv::Val::copyOf(zv::Ref(cachedByRealName)); + + zv::Val method = createMethod(classReflection, methodName, nativeMethodReflection.raw(), true); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + zv::Val result = zv::Val::copyOf(zv::Ref(method.raw())); + if (!zend_string_equals(realName.get(), methodName)) { + setNested(slot(PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS), cacheKey.get(), realName.get(), zv::Val::copyOf(zv::Ref(method.raw()))); + setNested(slot(PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS), cacheKey.get(), methodName, std::move(method)); + } else { + setNested(slot(PT_PCRE_PROP_METHODS_INCLUDING_ANNOTATIONS), cacheKey.get(), realName.get(), std::move(method)); + } + return result; + } + + /* Mirrors hasNativeMethod(): $this->hasMethod() — the class is final, + * so the twin's `$this->` is this body */ + bool hasNativeMethod(zval *classReflection, zend_string *methodName, bool &ok) + { + return hasMethod(classReflection, methodName, ok); + } + + /* Mirrors getNativeMethod(). */ + zv::Val getNativeMethod(zval *classReflection, zend_string *methodName) + { + zv::Val classCacheKey = pt_class_reflection_get_cache_key(Z_OBJ_P(classReflection)); + if (UNEXPECTED(classCacheKey.isUndef())) return zv::Val(); + zv::Str cacheKey = zv::Str::copyOf(Z_STR_P(classCacheKey.raw())); + if (UNEXPECTED(!touchMemberCacheKey(cacheKey.get()))) return zv::Val(); + zval *cached = issetNested(slot(PT_PCRE_PROP_NATIVE_METHODS), cacheKey.get(), methodName); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val nativeReflection = pt_class_reflection_get_native_reflection(Z_OBJ_P(classReflection)); + if (UNEXPECTED(nativeReflection.isUndef())) return zv::Val(); + bool ok; + bool exists = hasMethod(classReflection, methodName, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!exists) { + throwShouldNotHappen(); + return zv::Val(); + } + zv::Val nativeMethodReflection = adapterGetMethod(nativeReflection.raw(), methodName); + if (UNEXPECTED(nativeMethodReflection.isUndef())) return zv::Val(); + zv::Str realName = callString(nativeMethodReflection.raw(), PT_LC("getname")); + if (UNEXPECTED(realName.isNull())) return zv::Val(); + zval *cachedByRealName = issetNested(slot(PT_PCRE_PROP_NATIVE_METHODS), cacheKey.get(), realName.get()); + if (cachedByRealName != NULL) return zv::Val::copyOf(zv::Ref(cachedByRealName)); + + zv::Val method = createMethod(classReflection, methodName, nativeMethodReflection.raw(), false); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + zv::Val result = zv::Val::copyOf(zv::Ref(method.raw())); + setNested(slot(PT_PCRE_PROP_NATIVE_METHODS), cacheKey.get(), realName.get(), std::move(method)); + return result; + } + + /* Mirrors findPropertyTrait(): the trait name or PHP null; UNDEF = + * pending exception */ + zv::Val findPropertyTrait(zval *propertyReflection) + { + return findMemberTrait(propertyReflection); + } + + /* Mirrors findMethodTrait(). */ + zv::Val findMethodTrait(zval *methodReflection) + { + return findMemberTrait(methodReflection); + } + + /* The shared body of findPropertyTrait() and findMethodTrait(): the + * better-reflection declaring class's name when the member comes from a + * trait used elsewhere, PHP null otherwise. */ + static zv::Val findMemberTrait(zval *memberReflection) + { + bool ok; + zv::Val betterReflection = call(memberReflection, PT_LC("getbetterreflection")); + if (UNEXPECTED(betterReflection.isUndef())) return zv::Val(); + zv::Val declaringClass = call(betterReflection.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + bool isTrait = callBool(declaringClass.raw(), PT_LC("istrait"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!isTrait) return zv::Val::null(); + zv::Val adapterDeclaringClass = call(memberReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(adapterDeclaringClass.isUndef())) return zv::Val(); + bool adapterIsTrait = callBool(adapterDeclaringClass.raw(), PT_LC("istrait"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + zv::Str declaringClassName = callString(declaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(declaringClassName.isNull())) return zv::Val(); + if (adapterIsTrait) { + zv::Str adapterDeclaringClassName = callString(adapterDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(adapterDeclaringClassName.isNull())) return zv::Val(); + if (zend_string_equals(adapterDeclaringClassName.get(), declaringClassName.get())) return zv::Val::null(); + } + return zv::Val::adoptString(declaringClassName.take()); + } + + /* $this->signatureMapProvider->$method($className, $methodName) as a bool */ + bool signatureMapBool(const char *lcname, size_t len, zval *className, zval *methodName, bool &ok) + { + zv::Args args{className, methodName}; + return callBool(slot(PT_PCRE_PROP_SIGNATURE_MAP_PROVIDER), lcname, len, 2, args, ok); + } + + /* Mirrors createMethod(). */ + zv::Val createMethod(zval *classReflection, zend_string *requestedMethodName, zval *methodReflection, bool includingAnnotations) + { + bool ok; + zv::Str methodNameStr = callString(methodReflection, PT_LC("getname")); + if (UNEXPECTED(methodNameStr.isNull())) return zv::Val(); + zval methodNameArg; + ZVAL_STR(&methodNameArg, methodNameStr.get()); + + if (includingAnnotations) { + zv::Args extensionArgs{classReflection, &methodNameArg}; + bool hasAnnotationMethod = callBool(slot(PT_PCRE_PROP_ANNOTATIONS_METHODS_EXTENSION), PT_LC("hasmethod"), 2, extensionArgs, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (hasAnnotationMethod) { + zv::Val hierarchyDistances = call(classReflection, PT_LC("getclasshierarchydistances")); + if (UNEXPECTED(hierarchyDistances.isUndef())) return zv::Val(); + zv::Val annotationMethod = call(slot(PT_PCRE_PROP_ANNOTATIONS_METHODS_EXTENSION), PT_LC("getmethod"), 2, extensionArgs); + if (UNEXPECTED(annotationMethod.isUndef())) return zv::Val(); + zv::Val annotationDeclaringClass = call(annotationMethod.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(annotationDeclaringClass.isUndef())) return zv::Val(); + zv::Val annotationDeclaringClassName = pt_class_reflection_get_name(Z_OBJ_P(annotationDeclaringClass.raw())); + if (UNEXPECTED(annotationDeclaringClassName.isUndef())) return zv::Val(); + zval *annotationDistance = issetIn(hierarchyDistances.raw(), Z_STR_P(annotationDeclaringClassName.raw())); + if (annotationDistance == NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + + zv::Val methodDeclaringClass = call(methodReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(methodDeclaringClass.isUndef())) return zv::Val(); + zv::Str distanceDeclaringClass = callString(methodDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(distanceDeclaringClass.isNull())) return zv::Val(); + zv::Val methodTrait = findMethodTrait(methodReflection); + if (UNEXPECTED(methodTrait.isUndef())) return zv::Val(); + if (Z_TYPE_P(methodTrait.raw()) != IS_NULL) { + distanceDeclaringClass = zv::Str::copyOf(Z_STR_P(methodTrait.raw())); + } + zval *methodDistance = issetIn(hierarchyDistances.raw(), distanceDeclaringClass.get()); + if (methodDistance == NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + if (zval_get_long(annotationDistance) <= zval_get_long(methodDistance)) return annotationMethod; + } + + return getNativeMethod(classReflection, requestedMethodName); + } + + zv::Val methodDeclaringClass = call(methodReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(methodDeclaringClass.isUndef())) return zv::Val(); + zv::Str declaringClassNameStr = callString(methodDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(declaringClassNameStr.isNull())) return zv::Val(); + zval declaringClassNameArg; + ZVAL_STR(&declaringClassNameArg, declaringClassNameStr.get()); + + zv::Val declaringClassReflection = call(classReflection, PT_LC("getancestorwithclassname"), 1, &declaringClassNameArg); + if (UNEXPECTED(declaringClassReflection.isUndef())) return zv::Val(); + if (Z_TYPE_P(declaringClassReflection.raw()) != IS_OBJECT) { + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(classReflection)); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Str message = ancestorMessage(declaringClassNameStr.get(), Z_STR_P(className.raw())); + throwShouldNotHappenStr(message.get()); + return zv::Val(); + } + zval *declaringClass = declaringClassReflection.raw(); + + bool declaringIsEnum; + if (UNEXPECTED(!pt_class_reflection_is_enum(Z_OBJ_P(declaringClass), declaringIsEnum))) return zv::Val(); + if (declaringIsEnum) { + zv::Val declaringName = pt_class_reflection_get_name(Z_OBJ_P(declaringClass)); + if (UNEXPECTED(declaringName.isUndef())) return zv::Val(); + if (!zend_string_equals_literal(Z_STR_P(declaringName.raw()), "UnitEnum")) { + zv::Str lowered = zv::Str::adopt(zend_string_tolower(methodNameStr.get())); + if (zend_string_equals_literal(lowered.get(), "cases")) { + zv::Val builder = pt_constant_array_type_builder_create_empty(); + if (UNEXPECTED(builder.isUndef())) return zv::Val(); + zv::Val enumCases = call(classReflection, PT_LC("getenumcases")); + if (UNEXPECTED(enumCases.isUndef()) || Z_TYPE_P(enumCases.raw()) != IS_ARRAY) return zv::Val(); + zv::Val ownerName = pt_class_reflection_get_name(Z_OBJ_P(classReflection)); + if (UNEXPECTED(ownerName.isUndef())) return zv::Val(); + for (zv::ArrayEntry entry : zv::ArrRef(enumCases.raw())) { + zend_string *caseName = entry.stringKeyOrNull(); + zv::Str owned; + if (caseName == NULL) { + owned = zv::Str::adopt(zend_long_to_str((zend_long) entry.indexKey())); + caseName = owned.get(); + } + zv::Val ownedCaseType = enumCaseObjectType(Z_STR_P(ownerName.raw()), caseName); + if (UNEXPECTED(ownedCaseType.isUndef())) return zv::Val(); + if (UNEXPECTED(!pt_constant_array_type_builder_set_offset_value_type(builder.raw(), NULL, ownedCaseType.raw()))) return zv::Val(); + } + zv::Val array = pt_constant_array_type_builder_get_array(builder.raw()); + if (UNEXPECTED(array.isUndef())) return zv::Val(); + zv::Args args{declaringClass, array.raw()}; + return pt_type_new(PT_CLASS_ENUM_CASES_METHOD_REFLECTION, 2, args); + } + } + } + + bool isBuiltin = callBool(declaringClass, PT_LC("isbuiltin"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool signatureMapped = false; + if (isBuiltin || declaringIsEnum) { + signatureMapped = signatureMapBool(PT_LC("hasmethodsignature"), &declaringClassNameArg, &methodNameArg, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + if (signatureMapped) return createNativeMethod(declaringClass, methodReflection, &declaringClassNameArg, &methodNameArg); + + zv::Val methodTrait = findMethodTrait(methodReflection); + if (UNEXPECTED(methodTrait.isUndef())) return zv::Val(); + return createUserlandMethodReflection(declaringClass, declaringClass, methodReflection, methodTrait.raw()); + } + + /* + * The signature-map branch of createMethod(): builds the variants from + * the signature map, merged with the stub/inherited PHPDoc, and returns + * a NativeMethodReflection. + */ + zv::Val createNativeMethod(zval *declaringClass, zval *methodReflection, zval *declaringClassNameArg, zval *methodNameArg) + { + bool ok; + zv::Arr positionalVariants = zv::Arr::create(1); + zv::Val namedVariants = zv::Val::null(); + zv::Val throwType = zv::Val::null(); + zv::Val asserts = pt_type_call_static(PT_CLASS_ASSERTIONS, PT_LC("createempty"), 0, NULL); + if (UNEXPECTED(asserts.isUndef())) return zv::Val(); + bool acceptsNamedArguments = true; + zv::Val selfOutType = zv::Val::null(); + zv::Val phpDocComment = zv::Val::null(); + + /* the twin's ?bool $isPure */ + int isPure = -1; + bool hasMetadata = signatureMapBool(PT_LC("hasmethodmetadata"), declaringClassNameArg, methodNameArg, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (hasMetadata) { + zv::Args metadataArgs{declaringClassNameArg, methodNameArg}; + zv::Val metadata = call(slot(PT_PCRE_PROP_SIGNATURE_MAP_PROVIDER), PT_LC("getmethodmetadata"), 2, metadataArgs); + if (UNEXPECTED(metadata.isUndef())) return zv::Val(); + bool hasSideEffects = true; + zval *stored = Z_TYPE_P(metadata.raw()) == IS_ARRAY ? zend_hash_str_find(Z_ARRVAL_P(metadata.raw()), PT_LC("hasSideEffects")) : NULL; + if (stored != NULL) { + ZVAL_DEREF(stored); + if (Z_TYPE_P(stored) != IS_NULL) { + hasSideEffects = zend_is_true(stored); + } + } + isPure = hasSideEffects ? 0 : 1; + } + + zv::Args signatureArgs{declaringClassNameArg, methodNameArg, methodReflection}; + zv::Val signaturesResult = call(slot(PT_PCRE_PROP_SIGNATURE_MAP_PROVIDER), PT_LC("getmethodsignatures"), 3, signatureArgs); + if (UNEXPECTED(signaturesResult.isUndef()) || Z_TYPE_P(signaturesResult.raw()) != IS_ARRAY) return zv::Val(); + + /* the twin reads $currentResolvedPhpDoc after the loops: the value + * the last inner iteration left, or null when none ran */ + zv::Val lastResolvedPhpDoc = zv::Val::null(); + + for (zv::ArrayEntry group : zv::ArrRef(signaturesResult.raw())) { + zend_string *signatureType = group.stringKeyOrNull(); + zval *methodSignatures = group.value().raw(); + ZVAL_DEREF(methodSignatures); + if (Z_TYPE_P(methodSignatures) == IS_NULL || Z_TYPE_P(methodSignatures) != IS_ARRAY) continue; + bool isNamed = signatureType != NULL && zend_string_equals_literal(signatureType, "named"); + uint32_t signatureCount = zend_hash_num_elements(Z_ARRVAL_P(methodSignatures)); + zv::Arr variants = zv::Arr::create(signatureCount); + + zv::Val ownedSignatures = zv::Val::copyOf(zv::Ref(methodSignatures)); + for (zv::ArrayEntry signatureEntry : zv::ArrRef(ownedSignatures.raw())) { + zval *methodSignature = signatureEntry.value().raw(); + ZVAL_DEREF(methodSignature); + + zv::Val signatureParameters = call(methodSignature, PT_LC("getparameters")); + if (UNEXPECTED(signatureParameters.isUndef())) return zv::Val(); + zv::Arr phpDocParameterNameMapping = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(signatureParameters.raw()))); + for (zv::ArrayEntry parameterEntry : zv::ArrRef(signatureParameters.raw())) { + zv::Str parameterName = callString(parameterEntry.value().raw(), PT_LC("getname")); + if (UNEXPECTED(parameterName.isNull())) return zv::Val(); + phpDocParameterNameMapping.set(parameterName.get(), zv::Val::string(parameterName.get())); + } + zv::Arr phpDocParameterTypes = zv::Arr::create(0); + zv::Val phpDocReturnType = zv::Val::null(); + zv::Arr phpDocParameterOutTypes = zv::Arr::create(0); + zv::Val immediatelyInvokedCallableParameters = zv::Val(zv::Arr::empty()); + zv::Val closureThisParameters = zv::Val(zv::Arr::empty()); + zv::Val currentResolvedPhpDoc = zv::Val::null(); + zv::Val phpDocDeclaringClass = zv::Val::copyOf(zv::Ref(declaringClass)); + bool phpDocFromStubs = false; + + if (signatureCount == 1) { + zv::Arr positionalParameterNames = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(signatureParameters.raw()))); + for (zv::ArrayEntry parameterEntry : zv::ArrRef(signatureParameters.raw())) { + zv::Str parameterName = callString(parameterEntry.value().raw(), PT_LC("getname")); + if (UNEXPECTED(parameterName.isNull())) return zv::Val(); + positionalParameterNames.push(zv::Val::string(parameterName.get())); + } + zv::Val stubPhpDocPair = findMethodPhpDocIncludingAncestors(declaringClass, declaringClass, Z_STR_P(methodNameArg), positionalParameterNames.raw()); + if (UNEXPECTED(stubPhpDocPair.isUndef())) return zv::Val(); + if (Z_TYPE_P(stubPhpDocPair.raw()) == IS_ARRAY) { + zval *resolved = zend_hash_index_find(Z_ARRVAL_P(stubPhpDocPair.raw()), 0); + zval *owner = zend_hash_index_find(Z_ARRVAL_P(stubPhpDocPair.raw()), 1); + if (resolved != NULL && owner != NULL) { + currentResolvedPhpDoc = zv::Val::copyOf(zv::Ref(resolved)); + phpDocDeclaringClass = zv::Val::copyOf(zv::Ref(owner)); + phpDocFromStubs = true; + } + } + } + + zv::Val methodDocComment = docCommentOf(methodReflection); + if (UNEXPECTED(methodDocComment.isUndef())) return zv::Val(); + if (Z_TYPE_P(currentResolvedPhpDoc.raw()) == IS_NULL && Z_TYPE_P(methodDocComment.raw()) != IS_NULL) { + zv::Val methodFileName = call(methodReflection, PT_LC("getfilename")); + if (UNEXPECTED(methodFileName.isUndef())) return zv::Val(); + zval fileNameArg = {}; + if (Z_TYPE_P(methodFileName.raw()) == IS_STRING) { + ZVAL_COPY_VALUE(&fileNameArg, methodFileName.raw()); + } else { + ZVAL_NULL(&fileNameArg); + } + zval nullArg = {}; + ZVAL_NULL(&nullArg); + zv::Val fileResolved = getResolvedPhpDoc(&fileNameArg, declaringClassNameArg, &nullArg, methodNameArg, methodDocComment.raw()); + if (UNEXPECTED(fileResolved.isUndef())) return zv::Val(); + zv::Val reflectionParameters = call(methodReflection, PT_LC("getparameters")); + if (UNEXPECTED(reflectionParameters.isUndef())) return zv::Val(); + zv::Arr reflectionParameterNames = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(reflectionParameters.raw()))); + for (zv::ArrayEntry parameterEntry : zv::ArrRef(reflectionParameters.raw())) { + zv::Str parameterName = callString(parameterEntry.value().raw(), PT_LC("getname")); + if (UNEXPECTED(parameterName.isNull())) return zv::Val(); + reflectionParameterNames.push(zv::Val::string(parameterName.get())); + } + zv::Args resolveArgs{declaringClass, methodNameArg, fileResolved.raw(), reflectionParameterNames.raw()}; + currentResolvedPhpDoc = call(slot(PT_PCRE_PROP_PHP_DOC_INHERITANCE_RESOLVER), PT_LC("resolvephpdocformethod"), 4, resolveArgs); + if (UNEXPECTED(currentResolvedPhpDoc.isUndef())) return zv::Val(); + } + + if (Z_TYPE_P(currentResolvedPhpDoc.raw()) != IS_NULL) { + zv::Val templateTypeMap = call(phpDocDeclaringClass.raw(), PT_LC("getactivetemplatetypemap")); + if (UNEXPECTED(templateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = call(phpDocDeclaringClass.raw(), PT_LC("getcallsitevariancemap")); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val returnTag = call(currentResolvedPhpDoc.raw(), PT_LC("getreturntag")); + if (UNEXPECTED(returnTag.isUndef())) return zv::Val(); + zv::Val immediatelyInvoked = call(currentResolvedPhpDoc.raw(), PT_LC("getparamsimmediatelyinvokedcallable")); + if (UNEXPECTED(immediatelyInvoked.isUndef())) return zv::Val(); + immediatelyInvokedCallableParameters = trinaryMapOf(immediatelyInvoked.raw()); + if (UNEXPECTED(immediatelyInvokedCallableParameters.isUndef())) return zv::Val(); + if (Z_TYPE_P(returnTag.raw()) != IS_NULL && signatureCount == 1) { + zv::Val tagType = call(returnTag.raw(), PT_LC("gettype")); + if (UNEXPECTED(tagType.isUndef())) return zv::Val(); + zv::Val covariantVal = varianceCovariant(); + if (UNEXPECTED(covariantVal.isUndef())) return zv::Val(); + zval *covariant = covariantVal.raw(); + phpDocReturnType = pt_type_template_type_helper_resolve_template_types(tagType.raw(), templateTypeMap.raw(), callSiteVarianceMap.raw(), covariant, false); + if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); + } + + zv::Val closureThisTags = call(currentResolvedPhpDoc.raw(), PT_LC("getparamclosurethistags")); + if (UNEXPECTED(closureThisTags.isUndef())) return zv::Val(); + closureThisParameters = tagTypeMapOf(closureThisTags.raw()); + if (UNEXPECTED(closureThisParameters.isUndef())) return zv::Val(); + + zv::Val paramTags = call(currentResolvedPhpDoc.raw(), PT_LC("getparamtags")); + if (UNEXPECTED(paramTags.isUndef())) return zv::Val(); + zv::Val contravariantVal = varianceContravariant(); + if (UNEXPECTED(contravariantVal.isUndef())) return zv::Val(); + zval *contravariant = contravariantVal.raw(); + for (zv::ArrayEntry tagEntry : zv::ArrRef(paramTags.raw())) { + zend_string *name = tagEntry.stringKeyOrNull(); + if (name == NULL) continue; + zv::Val tagType = call(tagEntry.value().raw(), PT_LC("gettype")); + if (UNEXPECTED(tagType.isUndef())) return zv::Val(); + zv::Val resolved = pt_type_template_type_helper_resolve_template_types(tagType.raw(), templateTypeMap.raw(), callSiteVarianceMap.raw(), contravariant, false); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + phpDocParameterTypes.set(name, std::move(resolved)); + } + + zv::Val throwsTag = call(currentResolvedPhpDoc.raw(), PT_LC("getthrowstag")); + if (UNEXPECTED(throwsTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(throwsTag.raw()) != IS_NULL) { + throwType = call(throwsTag.raw(), PT_LC("gettype")); + if (UNEXPECTED(throwType.isUndef())) return zv::Val(); + } + + asserts = pt_type_call_static(PT_CLASS_ASSERTIONS, PT_LC("createfromresolvedphpdocblock"), 1, currentResolvedPhpDoc.raw()); + if (UNEXPECTED(asserts.isUndef())) return zv::Val(); + acceptsNamedArguments = callBool(currentResolvedPhpDoc.raw(), PT_LC("acceptsnamedarguments"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isPure < 0) { + /* isPure() is ?bool: `??=` leaves $isPure null when the + * block says nothing */ + zv::Val pure = call(currentResolvedPhpDoc.raw(), PT_LC("ispure")); + if (UNEXPECTED(pure.isUndef())) return zv::Val(); + if (Z_TYPE_P(pure.raw()) != IS_NULL) { + isPure = zend_is_true(pure.raw()) ? 1 : 0; + } + } + + zv::Val selfOutTag = call(currentResolvedPhpDoc.raw(), PT_LC("getselfouttag")); + if (UNEXPECTED(selfOutTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(selfOutTag.raw()) != IS_NULL) { + selfOutType = call(selfOutTag.raw(), PT_LC("gettype")); + if (UNEXPECTED(selfOutType.isUndef())) return zv::Val(); + } + + zv::Val paramOutTags = call(currentResolvedPhpDoc.raw(), PT_LC("getparamouttags")); + if (UNEXPECTED(paramOutTags.isUndef())) return zv::Val(); + zv::Val covariantVal = varianceCovariant(); + if (UNEXPECTED(covariantVal.isUndef())) return zv::Val(); + zval *covariant = covariantVal.raw(); + for (zv::ArrayEntry tagEntry : zv::ArrRef(paramOutTags.raw())) { + zend_string *name = tagEntry.stringKeyOrNull(); + if (name == NULL) continue; + zv::Val tagType = call(tagEntry.value().raw(), PT_LC("gettype")); + if (UNEXPECTED(tagType.isUndef())) return zv::Val(); + zv::Val resolved = pt_type_template_type_helper_resolve_template_types(tagType.raw(), templateTypeMap.raw(), callSiteVarianceMap.raw(), covariant, false); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + phpDocParameterOutTypes.set(name, std::move(resolved)); + } + + bool hasPhpDocString = callBool(currentResolvedPhpDoc.raw(), PT_LC("hasphpdocstring"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (hasPhpDocString) { + phpDocComment = call(currentResolvedPhpDoc.raw(), PT_LC("getphpdocstring")); + if (UNEXPECTED(phpDocComment.isUndef())) return zv::Val(); + } + + if (!phpDocFromStubs) { + zv::Val reflectionParameters = call(methodReflection, PT_LC("getparameters")); + if (UNEXPECTED(reflectionParameters.isUndef())) return zv::Val(); + zend_ulong index = 0; + for (zv::ArrayEntry parameterEntry : zv::ArrRef(reflectionParameters.raw())) { + zval *signatureParameter = zend_hash_index_find(Z_ARRVAL_P(signatureParameters.raw()), index); + index++; + if (signatureParameter == NULL) continue; + ZVAL_DEREF(signatureParameter); + zv::Str signatureName = callString(signatureParameter, PT_LC("getname")); + if (UNEXPECTED(signatureName.isNull())) return zv::Val(); + zv::Str reflectionName = callString(parameterEntry.value().raw(), PT_LC("getname")); + if (UNEXPECTED(reflectionName.isNull())) return zv::Val(); + phpDocParameterNameMapping.set(signatureName.get(), zv::Val::string(reflectionName.get())); + } + } + } + + zv::Val variant = createNativeMethodVariant(declaringClassNameArg, methodNameArg, methodSignature, phpDocParameterTypes.raw(), phpDocReturnType.raw(), phpDocParameterNameMapping.raw(), phpDocParameterOutTypes.raw(), immediatelyInvokedCallableParameters.raw(), closureThisParameters.raw(), phpDocFromStubs, !isNamed); + if (UNEXPECTED(variant.isUndef())) return zv::Val(); + variants.push(std::move(variant)); + lastResolvedPhpDoc = std::move(currentResolvedPhpDoc); + } + + if (isNamed) { + /* the twin's `$variantsByType[$signatureType][] = …` only + * creates the key when a signature was actually built, so an + * empty group leaves getNamedArgumentsVariants() null */ + if (zend_hash_num_elements(variants.table()) > 0) { + namedVariants = zv::Val(std::move(variants)); + } + } else if (signatureType != NULL && zend_string_equals_literal(signatureType, "positional")) { + positionalVariants = std::move(variants); + } + } + + if (isPure < 0) { + zv::Val classResolvedPhpDoc = call(declaringClass, PT_LC("getresolvedphpdoc")); + if (UNEXPECTED(classResolvedPhpDoc.isUndef())) return zv::Val(); + if (Z_TYPE_P(classResolvedPhpDoc.raw()) != IS_NULL) { + bool allPure = callBool(classResolvedPhpDoc.raw(), PT_LC("areallmethodspure"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (allPure) { + isPure = 1; + } else { + bool allImpure = callBool(classResolvedPhpDoc.raw(), PT_LC("areallmethodsimpure"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (allImpure) { + isPure = 0; + } + } + } + } + + zv::Val reflectionProvider = call(slot(PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER), PT_LC("getreflectionprovider")); + if (UNEXPECTED(reflectionProvider.isUndef())) return zv::Val(); + zv::Val hasSideEffects = isPure < 0 ? trinaryMaybe() : trinaryFromBoolean(isPure != 1); + if (UNEXPECTED(hasSideEffects.isUndef())) return zv::Val(); + zv::Args methodContextArgs{declaringClassNameArg, zv::null, methodNameArg, zv::null}; + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclassmethod"), 4, methodContextArgs); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Val attributes = attributesOf(methodReflection, context.raw()); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + + zval args[13]; + ZVAL_COPY_VALUE(&args[0], reflectionProvider.raw()); + ZVAL_COPY_VALUE(&args[1], declaringClass); + ZVAL_COPY_VALUE(&args[2], methodReflection); + ZVAL_COPY_VALUE(&args[3], lastResolvedPhpDoc.raw()); + ZVAL_COPY_VALUE(&args[4], positionalVariants.raw()); + ZVAL_COPY_VALUE(&args[5], namedVariants.raw()); + ZVAL_COPY_VALUE(&args[6], hasSideEffects.raw()); + ZVAL_COPY_VALUE(&args[7], throwType.raw()); + ZVAL_COPY_VALUE(&args[8], asserts.raw()); + ZVAL_BOOL(&args[9], acceptsNamedArguments); + ZVAL_COPY_VALUE(&args[10], selfOutType.raw()); + ZVAL_COPY_VALUE(&args[11], phpDocComment.raw()); + ZVAL_COPY_VALUE(&args[12], attributes.raw()); + return pt_type_new(PT_CLASS_NATIVE_METHOD_REFLECTION, 13, args); + } + + /* array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $map) */ + static zv::Val trinaryMapOf(zval *map) + { + if (Z_TYPE_P(map) != IS_ARRAY) return zv::Val(zv::Arr::empty()); + zv::Arr result = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(map))); + for (zv::ArrayEntry entry : zv::ArrRef(map)) { + zv::Val trinary = trinaryFromBoolean(zend_is_true(entry.value().raw())); + if (UNEXPECTED(trinary.isUndef())) return zv::Val(); + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL) { + result.push(std::move(trinary)); + continue; + } + result.set(key, std::move(trinary)); + } + return zv::Val(std::move(result)); + } + + /* array_map(static fn ($tag) => $tag->getType(), $tags) */ + static zv::Val tagTypeMapOf(zval *tags) + { + if (Z_TYPE_P(tags) != IS_ARRAY) return zv::Val(zv::Arr::empty()); + zv::Arr result = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(tags))); + for (zv::ArrayEntry entry : zv::ArrRef(tags)) { + zv::Val type = call(entry.value().raw(), PT_LC("gettype")); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL) { + result.push(std::move(type)); + continue; + } + result.set(key, std::move(type)); + } + return zv::Val(std::move(result)); + } + + /* TypehintHelper::decideType($type, $phpDocType) — by the real name, + * see decideTypeFromReflection() */ + static zv::Val decideType(zval *type, zval *phpDocType) + { + zv::Args args{type, phpDocType}; + return kernelStatic(PT_LC("PHPStan\\Type\\TypehintHelper"), PT_LC("decidetype"), 2, args); + } + + /* Mirrors createNativeMethodVariant(). */ + zv::Val createNativeMethodVariant(zval *declaringClassName, zval *methodName, zval *methodSignature, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *phpDocParameterNameMapping, zval *phpDocParameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *closureThisParameters, bool phpDocFromStubs, bool usePhpDocParameterNames) + { + bool ok; + zv::Val signatureParameters = call(methodSignature, PT_LC("getparameters")); + if (UNEXPECTED(signatureParameters.isUndef())) return zv::Val(); + zv::Arr parameters = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(signatureParameters.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(signatureParameters.raw())) { + zval *parameterSignature = entry.value().raw(); + ZVAL_DEREF(parameterSignature); + zv::Str signatureName = callString(parameterSignature, PT_LC("getname")); + if (UNEXPECTED(signatureName.isNull())) return zv::Val(); + zval *mapped = keyIn(phpDocParameterNameMapping, signatureName.get()); + zv::Str phpDocParameterName = mapped != NULL && Z_TYPE_P(mapped) == IS_STRING + ? zv::Str::copyOf(Z_STR_P(mapped)) + : zv::Str::copyOf(signatureName.get()); + + zv::Val signatureType = call(parameterSignature, PT_LC("gettype")); + if (UNEXPECTED(signatureType.isUndef())) return zv::Val(); + zv::Val type; + zv::Val phpDocType; + zval *storedPhpDocType = issetIn(phpDocParameterTypes, phpDocParameterName.get()); + if (storedPhpDocType != NULL) { + phpDocType = zv::Val::copyOf(zv::Ref(storedPhpDocType)); + if (phpDocFromStubs) { + type = zv::Val::copyOf(zv::Ref(storedPhpDocType)); + } else { + type = decideType(signatureType.raw(), storedPhpDocType); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + } + } + + zv::Val parameterOutType; + zval *storedOutType = issetIn(phpDocParameterOutTypes, phpDocParameterName.get()); + if (storedOutType != NULL) { + parameterOutType = zv::Val::copyOf(zv::Ref(storedOutType)); + } + + zv::Val immediatelyInvoked; + zval *storedImmediate = issetIn(immediatelyInvokedCallableParameters, phpDocParameterName.get()); + if (storedImmediate != NULL) { + immediatelyInvoked = zv::Val::copyOf(zv::Ref(storedImmediate)); + } else { + immediatelyInvoked = trinaryMaybe(); + if (UNEXPECTED(immediatelyInvoked.isUndef())) return zv::Val(); + } + + zv::Val closureThisType = zv::Val::null(); + zval *storedClosureThis = issetIn(closureThisParameters, phpDocParameterName.get()); + if (storedClosureThis != NULL) { + closureThisType = zv::Val::copyOf(zv::Ref(storedClosureThis)); + } + + bool isOptional = callBool(parameterSignature, PT_LC("isoptional"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + zv::Val nativeType = call(parameterSignature, PT_LC("getnativetype")); + if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); + zv::Val passedByReference = call(parameterSignature, PT_LC("passedbyreference")); + if (UNEXPECTED(passedByReference.isUndef())) return zv::Val(); + bool isVariadic = callBool(parameterSignature, PT_LC("isvariadic"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + zv::Val defaultValue = call(parameterSignature, PT_LC("getdefaultvalue")); + if (UNEXPECTED(defaultValue.isUndef())) return zv::Val(); + zv::Val outType = call(parameterSignature, PT_LC("getouttype")); + if (UNEXPECTED(outType.isUndef())) return zv::Val(); + zv::Args allowedConstantsArgs{declaringClassName, methodName, signatureName.get()}; + zv::Val allowedConstants = call(slot(PT_PCRE_PROP_ALLOWED_CONSTANTS_MAP_PROVIDER), PT_LC("getformethodparameter"), 3, allowedConstantsArgs); + if (UNEXPECTED(allowedConstants.isUndef())) return zv::Val(); + zv::Val pureUnlessCallableIsImpure = trinaryNo(); + if (UNEXPECTED(pureUnlessCallableIsImpure.isUndef())) return zv::Val(); + zv::Val mixedPhpDocType; + if (phpDocType.isUndef()) { + mixedPhpDocType = mixedType(); + if (UNEXPECTED(mixedPhpDocType.isUndef())) return zv::Val(); + } + + zval args[14]; + if (usePhpDocParameterNames) { + ZVAL_STR(&args[0], phpDocParameterName.get()); + } else { + ZVAL_STR(&args[0], signatureName.get()); + } + ZVAL_BOOL(&args[1], isOptional); + ZVAL_COPY_VALUE(&args[2], type.isUndef() ? signatureType.raw() : type.raw()); + ZVAL_COPY_VALUE(&args[3], phpDocType.isUndef() ? mixedPhpDocType.raw() : phpDocType.raw()); + ZVAL_COPY_VALUE(&args[4], nativeType.raw()); + ZVAL_COPY_VALUE(&args[5], passedByReference.raw()); + ZVAL_BOOL(&args[6], isVariadic); + ZVAL_COPY_VALUE(&args[7], defaultValue.raw()); + ZVAL_COPY_VALUE(&args[8], parameterOutType.isUndef() ? outType.raw() : parameterOutType.raw()); + ZVAL_COPY_VALUE(&args[9], immediatelyInvoked.raw()); + ZVAL_COPY_VALUE(&args[10], closureThisType.raw()); + ZVAL_EMPTY_ARRAY(&args[11]); + ZVAL_COPY_VALUE(&args[12], allowedConstants.raw()); + ZVAL_COPY_VALUE(&args[13], pureUnlessCallableIsImpure.raw()); + zv::Val parameter = pt_type_new(PT_CLASS_EXTENDED_NATIVE_PARAMETER_REFLECTION, 14, args); + if (UNEXPECTED(parameter.isUndef())) return zv::Val(); + parameters.push(std::move(parameter)); + } + + zv::Val signatureReturnType = call(methodSignature, PT_LC("getreturntype")); + if (UNEXPECTED(signatureReturnType.isUndef())) return zv::Val(); + zv::Val returnType; + if (phpDocFromStubs && Z_TYPE_P(phpDocReturnType) != IS_NULL) { + returnType = zv::Val::copyOf(zv::Ref(phpDocReturnType)); + } else { + returnType = decideType(signatureReturnType.raw(), phpDocReturnType); + if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + } + + zv::Val ownedEmptyMap = templateTypeMapEmpty(); + if (UNEXPECTED(ownedEmptyMap.isUndef())) return zv::Val(); + bool isVariadicSignature = callBool(methodSignature, PT_LC("isvariadic"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + zv::Val nativeReturnType = call(methodSignature, PT_LC("getnativereturntype")); + if (UNEXPECTED(nativeReturnType.isUndef())) return zv::Val(); + zv::Val mixedReturnType; + if (Z_TYPE_P(phpDocReturnType) == IS_NULL) { + mixedReturnType = mixedType(); + if (UNEXPECTED(mixedReturnType.isUndef())) return zv::Val(); + } + + zval variantArgs[7]; + ZVAL_COPY_VALUE(&variantArgs[0], ownedEmptyMap.raw()); + ZVAL_NULL(&variantArgs[1]); + ZVAL_COPY_VALUE(&variantArgs[2], parameters.raw()); + ZVAL_BOOL(&variantArgs[3], isVariadicSignature); + ZVAL_COPY_VALUE(&variantArgs[4], returnType.raw()); + ZVAL_COPY_VALUE(&variantArgs[5], Z_TYPE_P(phpDocReturnType) == IS_NULL ? mixedReturnType.raw() : phpDocReturnType); + ZVAL_COPY_VALUE(&variantArgs[6], nativeReturnType.raw()); + return pt_type_new(PT_CLASS_EXTENDED_FUNCTION_VARIANT, 7, variantArgs); + } + + /* array_map(static fn (ReflectionParameter $p): string => $p->getName(), $reflection->getParameters()) */ + static zv::Val parameterNamesOf(zval *methodReflection) + { + zv::Val parameters = call(methodReflection, PT_LC("getparameters")); + if (UNEXPECTED(parameters.isUndef()) || Z_TYPE_P(parameters.raw()) != IS_ARRAY) return zv::Val(); + zv::Arr names = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(parameters.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(parameters.raw())) { + zv::Str name = callString(entry.value().raw(), PT_LC("getname")); + if (UNEXPECTED(name.isNull())) return zv::Val(); + names.push(zv::Val::string(name.get())); + } + return zv::Val(std::move(names)); + } + + /* Mirrors createUserlandMethodReflection(). */ + zv::Val createUserlandMethodReflection(zval *fileDeclaringClass, zval *actualDeclaringClass, zval *methodReflection, zval *declaringTraitName) + { + bool ok; + zv::Val deprecation = call(slot(PT_PCRE_PROP_DEPRECATION_PROVIDER), PT_LC("getmethoddeprecation"), 1, methodReflection); + if (UNEXPECTED(deprecation.isUndef())) return zv::Val(); + bool isDeprecated = Z_TYPE_P(deprecation.raw()) != IS_NULL; + zv::Val deprecatedDescription = zv::Val::null(); + if (isDeprecated) { + deprecatedDescription = call(deprecation.raw(), PT_LC("getdescription")); + if (UNEXPECTED(deprecatedDescription.isUndef())) return zv::Val(); + } + + zv::Str methodNameStr = callString(methodReflection, PT_LC("getname")); + if (UNEXPECTED(methodNameStr.isNull())) return zv::Val(); + zval methodNameArg; + ZVAL_STR(&methodNameArg, methodNameStr.get()); + + zv::Val parameterNames = parameterNamesOf(methodReflection); + if (UNEXPECTED(parameterNames.isUndef())) return zv::Val(); + zv::Val currentResolvedPhpDoc = zv::Val::null(); + zv::Val stubPhpDocPair = findMethodPhpDocIncludingAncestors(fileDeclaringClass, fileDeclaringClass, methodNameStr.get(), parameterNames.raw()); + if (UNEXPECTED(stubPhpDocPair.isUndef())) return zv::Val(); + zv::Val phpDocBlockClassReflection = zv::Val::copyOf(zv::Ref(fileDeclaringClass)); + + zv::Val betterReflection = call(methodReflection, PT_LC("getbetterreflection")); + if (UNEXPECTED(betterReflection.isUndef())) return zv::Val(); + zv::Val methodDeclaringClass = call(betterReflection.raw(), PT_LC("getdeclaringclass")); + if (UNEXPECTED(methodDeclaringClass.isUndef())) return zv::Val(); + + if (Z_TYPE_P(stubPhpDocPair.raw()) == IS_NULL) { + bool isTrait = callBool(methodDeclaringClass.raw(), PT_LC("istrait"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isTrait) { + zv::Val adapterDeclaringClass = call(methodReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(adapterDeclaringClass.isUndef())) return zv::Val(); + bool adapterIsTrait = callBool(adapterDeclaringClass.raw(), PT_LC("istrait"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + zv::Str betterName = callString(methodDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(betterName.isNull())) return zv::Val(); + zv::Str adapterName = callString(adapterDeclaringClass.raw(), PT_LC("getname")); + if (UNEXPECTED(adapterName.isNull())) return zv::Val(); + if (!adapterIsTrait || !zend_string_equals(betterName.get(), adapterName.get())) { + zv::Val reflectionProvider = call(slot(PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER), PT_LC("getreflectionprovider")); + if (UNEXPECTED(reflectionProvider.isUndef())) return zv::Val(); + zval betterNameArg; + ZVAL_STR(&betterNameArg, betterName.get()); + zv::Val traitClass = pt_reflection_provider_get_class(Z_OBJ_P(reflectionProvider.raw()), &betterNameArg); + if (UNEXPECTED(traitClass.isUndef())) return zv::Val(); + zv::Val reflectionProvider2 = call(slot(PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER), PT_LC("getreflectionprovider")); + if (UNEXPECTED(reflectionProvider2.isUndef())) return zv::Val(); + zval adapterNameArg; + ZVAL_STR(&adapterNameArg, adapterName.get()); + zv::Val implementingClass = pt_reflection_provider_get_class(Z_OBJ_P(reflectionProvider2.raw()), &adapterNameArg); + if (UNEXPECTED(implementingClass.isUndef())) return zv::Val(); + zv::Val traitParameterNames = parameterNamesOf(methodReflection); + if (UNEXPECTED(traitParameterNames.isUndef())) return zv::Val(); + stubPhpDocPair = findMethodPhpDocIncludingAncestors(traitClass.raw(), implementingClass.raw(), methodNameStr.get(), traitParameterNames.raw()); + if (UNEXPECTED(stubPhpDocPair.isUndef())) return zv::Val(); + } + } + } + + if (Z_TYPE_P(stubPhpDocPair.raw()) == IS_ARRAY) { + zval *resolved = zend_hash_index_find(Z_ARRVAL_P(stubPhpDocPair.raw()), 0); + zval *owner = zend_hash_index_find(Z_ARRVAL_P(stubPhpDocPair.raw()), 1); + if (resolved != NULL && owner != NULL) { + currentResolvedPhpDoc = zv::Val::copyOf(zv::Ref(resolved)); + phpDocBlockClassReflection = zv::Val::copyOf(zv::Ref(owner)); + } + } + + zv::Val methodDocComment = docCommentOf(methodReflection); + if (UNEXPECTED(methodDocComment.isUndef())) return zv::Val(); + if (Z_TYPE_P(currentResolvedPhpDoc.raw()) == IS_NULL && Z_TYPE_P(methodDocComment.raw()) != IS_NULL) { + zv::Val fileName = call(actualDeclaringClass, PT_LC("getfilename")); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(actualDeclaringClass)); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + currentResolvedPhpDoc = getResolvedPhpDoc(fileName.raw(), className.raw(), declaringTraitName, &methodNameArg, methodDocComment.raw()); + if (UNEXPECTED(currentResolvedPhpDoc.isUndef())) return zv::Val(); + } + + zv::Val inheritanceParameterNames = parameterNamesOf(methodReflection); + if (UNEXPECTED(inheritanceParameterNames.isUndef())) return zv::Val(); + zv::Args resolveArgs{actualDeclaringClass, &methodNameArg, currentResolvedPhpDoc.raw(), inheritanceParameterNames.raw()}; + zv::Val resolvedPhpDoc = call(slot(PT_PCRE_PROP_PHP_DOC_INHERITANCE_RESOLVER), PT_LC("resolvephpdocformethod"), 4, resolveArgs); + if (UNEXPECTED(resolvedPhpDoc.isUndef())) return zv::Val(); + + zv::Val declaringTrait = zv::Val::null(); + zv::Val reflectionProvider = call(slot(PT_PCRE_PROP_REFLECTION_PROVIDER_PROVIDER), PT_LC("getreflectionprovider")); + if (UNEXPECTED(reflectionProvider.isUndef())) return zv::Val(); + if (Z_TYPE_P(declaringTraitName) != IS_NULL) { + bool hasClass; + if (UNEXPECTED(!pt_reflection_provider_has_class(Z_OBJ_P(reflectionProvider.raw()), declaringTraitName, hasClass))) return zv::Val(); + if (hasClass) { + declaringTrait = pt_reflection_provider_get_class(Z_OBJ_P(reflectionProvider.raw()), declaringTraitName); + if (UNEXPECTED(declaringTrait.isUndef())) return zv::Val(); + } + } + + zv::Arr phpDocParameterTypes = zv::Arr::create(0); + bool isConstructor = callBool(methodReflection, PT_LC("isconstructor"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isConstructor) { + zv::Val parameters = call(methodReflection, PT_LC("getparameters")); + if (UNEXPECTED(parameters.isUndef()) || Z_TYPE_P(parameters.raw()) != IS_ARRAY) return zv::Val(); + for (zv::ArrayEntry entry : zv::ArrRef(parameters.raw())) { + zval *parameter = entry.value().raw(); + ZVAL_DEREF(parameter); + bool isPromoted = callBool(parameter, PT_LC("ispromoted"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!isPromoted) continue; + zv::Str parameterName = callString(parameter, PT_LC("getname")); + if (UNEXPECTED(parameterName.isNull())) return zv::Val(); + zval parameterNameArg; + ZVAL_STR(¶meterNameArg, parameterName.get()); + zv::Val adapterDeclaringClass = call(methodReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(adapterDeclaringClass.isUndef())) return zv::Val(); + bool hasParameterProperty = callBool(adapterDeclaringClass.raw(), PT_LC("hasproperty"), 1, ¶meterNameArg, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!hasParameterProperty) continue; + zv::Val adapterDeclaringClass2 = call(methodReflection, PT_LC("getdeclaringclass")); + if (UNEXPECTED(adapterDeclaringClass2.isUndef())) return zv::Val(); + zv::Val parameterProperty = call(adapterDeclaringClass2.raw(), PT_LC("getproperty"), 1, ¶meterNameArg); + if (UNEXPECTED(parameterProperty.isUndef())) return zv::Val(); + bool propertyPromoted = callBool(parameterProperty.raw(), PT_LC("ispromoted"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!propertyPromoted) continue; + zv::Val propertyDocComment = docCommentOf(parameterProperty.raw()); + if (UNEXPECTED(propertyDocComment.isUndef())) return zv::Val(); + if (Z_TYPE_P(propertyDocComment.raw()) == IS_NULL) continue; + zv::Val fileName = call(fileDeclaringClass, PT_LC("getfilename")); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + zv::Val className = pt_class_reflection_get_name(Z_OBJ_P(fileDeclaringClass)); + if (UNEXPECTED(className.isUndef())) return zv::Val(); + zv::Val propertyDocblock = getResolvedPhpDoc(fileName.raw(), className.raw(), declaringTraitName, &methodNameArg, propertyDocComment.raw()); + if (UNEXPECTED(propertyDocblock.isUndef())) return zv::Val(); + zv::Val varTags = call(propertyDocblock.raw(), PT_LC("getvartags")); + if (UNEXPECTED(varTags.isUndef())) return zv::Val(); + zval *varTag = varTagFor(varTags.raw(), parameterName.get()); + if (varTag == NULL) continue; + zv::Val phpDocType = call(varTag, PT_LC("gettype")); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); + phpDocParameterTypes.set(parameterName.get(), std::move(phpDocType)); + } + } + + zv::Val reflectionReturnType = call(methodReflection, PT_LC("getreturntype")); + if (UNEXPECTED(reflectionReturnType.isUndef())) return zv::Val(); + zv::Val nativeReturnType = decideTypeFromReflection(reflectionReturnType.raw(), actualDeclaringClass); + if (UNEXPECTED(nativeReturnType.isUndef())) return zv::Val(); + + int isPure = -1; + zv::Arr pureUnlessCallableIsImpureParameters = zv::Arr::create(0); + bool isBuiltin = callBool(actualDeclaringClass, PT_LC("isbuiltin"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + bool actualIsEnum = false; + if (!isBuiltin) { + if (UNEXPECTED(!pt_class_reflection_is_enum(Z_OBJ_P(actualDeclaringClass), actualIsEnum))) return zv::Val(); + } + if (isBuiltin || actualIsEnum) { + zv::Val ancestors = call(actualDeclaringClass, PT_LC("getancestors")); + if (UNEXPECTED(ancestors.isUndef()) || Z_TYPE_P(ancestors.raw()) != IS_ARRAY) return zv::Val(); + for (zv::ArrayEntry entry : zv::ArrRef(ancestors.raw())) { + zend_string *ancestorName = entry.stringKeyOrNull(); + if (ancestorName == NULL) continue; + zval ancestorNameArg; + ZVAL_STR(&ancestorNameArg, ancestorName); + bool hasMetadata = signatureMapBool(PT_LC("hasmethodmetadata"), &ancestorNameArg, &methodNameArg, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!hasMetadata) continue; + zv::Args metadataArgs{&ancestorNameArg, &methodNameArg}; + zv::Val metadata = call(slot(PT_PCRE_PROP_SIGNATURE_MAP_PROVIDER), PT_LC("getmethodmetadata"), 2, metadataArgs); + if (UNEXPECTED(metadata.isUndef())) return zv::Val(); + bool hasSideEffects = true; + zval *stored = Z_TYPE_P(metadata.raw()) == IS_ARRAY ? zend_hash_str_find(Z_ARRVAL_P(metadata.raw()), PT_LC("hasSideEffects")) : NULL; + if (stored != NULL) { + ZVAL_DEREF(stored); + if (Z_TYPE_P(stored) != IS_NULL) { + hasSideEffects = zend_is_true(stored); + } + } + isPure = hasSideEffects ? 0 : 1; + zval *pureUnless = Z_TYPE_P(metadata.raw()) == IS_ARRAY ? zend_hash_str_find(Z_ARRVAL_P(metadata.raw()), PT_LC("pureUnlessCallableIsImpureParameters")) : NULL; + if (pureUnless != NULL) { + ZVAL_DEREF(pureUnless); + if (Z_TYPE_P(pureUnless) == IS_ARRAY) { + for (zv::ArrayEntry pureEntry : zv::ArrRef(pureUnless)) { + zend_string *key = pureEntry.stringKeyOrNull(); + if (key == NULL) continue; + if (!zend_symtable_exists(pureUnlessCallableIsImpureParameters.table(), key)) { + pureUnlessCallableIsImpureParameters.set(key, zv::Val::copyOf(pureEntry.value())); + } + } + } + } + break; + } + } + + zv::Arr phpDocParameterOutTypes = zv::Arr::create(0); + zv::Val phpDocReturnType = zv::Val::null(); + zv::Val templateTypeMap = templateTypeMapEmpty(); + if (UNEXPECTED(templateTypeMap.isUndef())) return zv::Val(); + zv::Val immediatelyInvokedCallableParameters = zv::Val(zv::Arr::empty()); + zv::Val closureThisParameters = zv::Val(zv::Arr::empty()); + zv::Val phpDocThrowType = zv::Val::null(); + bool isInternal = false; + bool isFinal = false; + zv::Val asserts = pt_type_call_static(PT_CLASS_ASSERTIONS, PT_LC("createempty"), 0, NULL); + if (UNEXPECTED(asserts.isUndef())) return zv::Val(); + bool acceptsNamedArguments = true; + zv::Val selfOutType = zv::Val::null(); + zv::Val phpDocComment = zv::Val::null(); + + if (Z_TYPE_P(resolvedPhpDoc.raw()) != IS_NULL) { + templateTypeMap = call(resolvedPhpDoc.raw(), PT_LC("gettemplatetypemap")); + if (UNEXPECTED(templateTypeMap.isUndef())) return zv::Val(); + zv::Val immediatelyInvoked = call(resolvedPhpDoc.raw(), PT_LC("getparamsimmediatelyinvokedcallable")); + if (UNEXPECTED(immediatelyInvoked.isUndef())) return zv::Val(); + immediatelyInvokedCallableParameters = trinaryMapOf(immediatelyInvoked.raw()); + if (UNEXPECTED(immediatelyInvokedCallableParameters.isUndef())) return zv::Val(); + zv::Val closureThisTags = call(resolvedPhpDoc.raw(), PT_LC("getparamclosurethistags")); + if (UNEXPECTED(closureThisTags.isUndef())) return zv::Val(); + closureThisParameters = tagTypeMapOf(closureThisTags.raw()); + if (UNEXPECTED(closureThisParameters.isUndef())) return zv::Val(); + zv::Val pureUnless = call(resolvedPhpDoc.raw(), PT_LC("getparamspureunlesscallableisimpure")); + if (UNEXPECTED(pureUnless.isUndef())) return zv::Val(); + if (Z_TYPE_P(pureUnless.raw()) == IS_ARRAY) { + for (zv::ArrayEntry pureEntry : zv::ArrRef(pureUnless.raw())) { + zend_string *key = pureEntry.stringKeyOrNull(); + if (key == NULL) continue; + pureUnlessCallableIsImpureParameters.set(key, zv::Val::copyOf(pureEntry.value())); + } + } + + phpDocReturnType = getPhpDocReturnType(phpDocBlockClassReflection.raw(), resolvedPhpDoc.raw(), nativeReturnType.raw()); + if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); + zv::Val throwsTag = call(resolvedPhpDoc.raw(), PT_LC("getthrowstag")); + if (UNEXPECTED(throwsTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(throwsTag.raw()) != IS_NULL) { + phpDocThrowType = call(throwsTag.raw(), PT_LC("gettype")); + if (UNEXPECTED(phpDocThrowType.isUndef())) return zv::Val(); + } + + zv::Val paramTags = call(resolvedPhpDoc.raw(), PT_LC("getparamtags")); + if (UNEXPECTED(paramTags.isUndef())) return zv::Val(); + if (Z_TYPE_P(paramTags.raw()) == IS_ARRAY) { + for (zv::ArrayEntry tagEntry : zv::ArrRef(paramTags.raw())) { + zend_string *key = tagEntry.stringKeyOrNull(); + if (key == NULL) continue; + if (zend_symtable_exists(phpDocParameterTypes.table(), key)) continue; + zv::Val tagType = call(tagEntry.value().raw(), PT_LC("gettype")); + if (UNEXPECTED(tagType.isUndef())) return zv::Val(); + phpDocParameterTypes.set(key, std::move(tagType)); + } + } + + zv::Val paramOutTags = call(resolvedPhpDoc.raw(), PT_LC("getparamouttags")); + if (UNEXPECTED(paramOutTags.isUndef())) return zv::Val(); + if (Z_TYPE_P(paramOutTags.raw()) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(paramOutTags.raw())) > 0) { + zv::Val activeTemplateTypeMap = call(phpDocBlockClassReflection.raw(), PT_LC("getactivetemplatetypemap")); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = call(phpDocBlockClassReflection.raw(), PT_LC("getcallsitevariancemap")); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val covariantVal = varianceCovariant(); + if (UNEXPECTED(covariantVal.isUndef())) return zv::Val(); + zval *covariant = covariantVal.raw(); + for (zv::ArrayEntry tagEntry : zv::ArrRef(paramOutTags.raw())) { + zend_string *key = tagEntry.stringKeyOrNull(); + if (key == NULL) continue; + zv::Val tagType = call(tagEntry.value().raw(), PT_LC("gettype")); + if (UNEXPECTED(tagType.isUndef())) return zv::Val(); + zv::Val resolved = pt_type_template_type_helper_resolve_template_types(tagType.raw(), activeTemplateTypeMap.raw(), callSiteVarianceMap.raw(), covariant, false); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + phpDocParameterOutTypes.set(key, std::move(resolved)); + } + } + + if (!isDeprecated) { + zv::Val deprecatedTag = call(resolvedPhpDoc.raw(), PT_LC("getdeprecatedtag")); + if (UNEXPECTED(deprecatedTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(deprecatedTag.raw()) != IS_NULL) { + deprecatedDescription = call(deprecatedTag.raw(), PT_LC("getmessage")); + if (UNEXPECTED(deprecatedDescription.isUndef())) return zv::Val(); + } else { + deprecatedDescription = zv::Val::null(); + } + isDeprecated = callBool(resolvedPhpDoc.raw(), PT_LC("isdeprecated"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + isInternal = callBool(resolvedPhpDoc.raw(), PT_LC("isinternal"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + isFinal = callBool(resolvedPhpDoc.raw(), PT_LC("isfinal"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isPure < 0) { + /* isPure() is ?bool: `??=` leaves $isPure null when the block + * says nothing */ + zv::Val pure = call(resolvedPhpDoc.raw(), PT_LC("ispure")); + if (UNEXPECTED(pure.isUndef())) return zv::Val(); + if (Z_TYPE_P(pure.raw()) != IS_NULL) { + isPure = zend_is_true(pure.raw()) ? 1 : 0; + } + } + asserts = pt_type_call_static(PT_CLASS_ASSERTIONS, PT_LC("createfromresolvedphpdocblock"), 1, resolvedPhpDoc.raw()); + if (UNEXPECTED(asserts.isUndef())) return zv::Val(); + acceptsNamedArguments = callBool(resolvedPhpDoc.raw(), PT_LC("acceptsnamedarguments"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + zv::Val selfOutTag = call(resolvedPhpDoc.raw(), PT_LC("getselfouttag")); + if (UNEXPECTED(selfOutTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(selfOutTag.raw()) != IS_NULL) { + selfOutType = call(selfOutTag.raw(), PT_LC("gettype")); + if (UNEXPECTED(selfOutType.isUndef())) return zv::Val(); + } + bool hasPhpDocString = callBool(resolvedPhpDoc.raw(), PT_LC("hasphpdocstring"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (hasPhpDocString) { + phpDocComment = call(resolvedPhpDoc.raw(), PT_LC("getphpdocstring")); + if (UNEXPECTED(phpDocComment.isUndef())) return zv::Val(); + } + } + + if (isPure < 0) { + zv::Val classResolvedPhpDoc = call(phpDocBlockClassReflection.raw(), PT_LC("getresolvedphpdoc")); + if (UNEXPECTED(classResolvedPhpDoc.isUndef())) return zv::Val(); + if (Z_TYPE_P(classResolvedPhpDoc.raw()) != IS_NULL) { + bool allPure = callBool(classResolvedPhpDoc.raw(), PT_LC("areallmethodspure"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (allPure) { + zv::Str lowered = zv::Str::adopt(zend_string_tolower(methodNameStr.get())); + bool pure = zend_string_equals_literal(lowered.get(), "__construct"); + if (!pure) { + bool phpDocVoid = false; + if (Z_TYPE_P(phpDocReturnType.raw()) != IS_NULL) { + phpDocVoid = typeOpYes(phpDocReturnType.raw(), PT_OP_IS_VOID, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + } + if (!phpDocVoid) { + bool nativeVoid = typeOpYes(nativeReturnType.raw(), PT_OP_IS_VOID, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + pure = !nativeVoid; + } + } + if (pure) { + isPure = 1; + } + } else { + bool allImpure = callBool(classResolvedPhpDoc.raw(), PT_LC("areallmethodsimpure"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (allImpure) { + isPure = 0; + } + } + } + } + + if (zend_hash_num_elements(phpDocParameterTypes.table()) > 0) { + zv::Val activeTemplateTypeMap = call(phpDocBlockClassReflection.raw(), PT_LC("getactivetemplatetypemap")); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = call(phpDocBlockClassReflection.raw(), PT_LC("getcallsitevariancemap")); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val contravariantVal = varianceContravariant(); + if (UNEXPECTED(contravariantVal.isUndef())) return zv::Val(); + zval *contravariant = contravariantVal.raw(); + zv::Arr resolvedParameterTypes = zv::Arr::create(zend_hash_num_elements(phpDocParameterTypes.table())); + for (zv::ArrayEntry entry : zv::ArrRef(phpDocParameterTypes.raw())) { + zv::Val resolved = pt_type_template_type_helper_resolve_template_types(entry.value().raw(), activeTemplateTypeMap.raw(), callSiteVarianceMap.raw(), contravariant, false); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL) { + resolvedParameterTypes.push(std::move(resolved)); + continue; + } + resolvedParameterTypes.set(key, std::move(resolved)); + } + phpDocParameterTypes = std::move(resolvedParameterTypes); + } + + zv::Val actualClassName = pt_class_reflection_get_name(Z_OBJ_P(actualDeclaringClass)); + if (UNEXPECTED(actualClassName.isUndef())) return zv::Val(); + zv::Val actualFileName = call(actualDeclaringClass, PT_LC("getfilename")); + if (UNEXPECTED(actualFileName.isUndef())) return zv::Val(); + zv::Args contextArgs{actualClassName.raw(), declaringTraitName, &methodNameArg, actualFileName.raw()}; + zv::Val context = pt_type_call_static(PT_CLASS_INITIALIZER_EXPR_CONTEXT, PT_LC("fromclassmethod"), 4, contextArgs); + if (UNEXPECTED(context.isUndef())) return zv::Val(); + zv::Val attributes = attributesOf(methodReflection, context.raw()); + if (UNEXPECTED(attributes.isUndef())) return zv::Val(); + + zval args[22]; + ZVAL_COPY_VALUE(&args[0], actualDeclaringClass); + ZVAL_COPY_VALUE(&args[1], declaringTrait.raw()); + ZVAL_COPY_VALUE(&args[2], methodReflection); + ZVAL_COPY_VALUE(&args[3], templateTypeMap.raw()); + ZVAL_COPY_VALUE(&args[4], phpDocParameterTypes.raw()); + ZVAL_COPY_VALUE(&args[5], phpDocReturnType.raw()); + ZVAL_COPY_VALUE(&args[6], phpDocThrowType.raw()); + ZVAL_COPY_VALUE(&args[7], resolvedPhpDoc.raw()); + ZVAL_COPY_VALUE(&args[8], deprecatedDescription.raw()); + ZVAL_BOOL(&args[9], isDeprecated); + ZVAL_BOOL(&args[10], isInternal); + ZVAL_BOOL(&args[11], isFinal); + if (isPure < 0) { + ZVAL_NULL(&args[12]); + } else { + ZVAL_BOOL(&args[12], isPure == 1); + } + ZVAL_COPY_VALUE(&args[13], asserts.raw()); + ZVAL_COPY_VALUE(&args[14], selfOutType.raw()); + ZVAL_COPY_VALUE(&args[15], phpDocComment.raw()); + ZVAL_COPY_VALUE(&args[16], phpDocParameterOutTypes.raw()); + ZVAL_COPY_VALUE(&args[17], immediatelyInvokedCallableParameters.raw()); + ZVAL_COPY_VALUE(&args[18], closureThisParameters.raw()); + ZVAL_BOOL(&args[19], acceptsNamedArguments); + ZVAL_COPY_VALUE(&args[20], attributes.raw()); + ZVAL_COPY_VALUE(&args[21], pureUnlessCallableIsImpureParameters.raw()); + return call(slot(PT_PCRE_PROP_METHOD_REFLECTION_FACTORY), PT_LC("create"), 22, args); + } + + /* Mirrors getPhpDocReturnType(). */ + zv::Val getPhpDocReturnType(zval *phpDocBlockClassReflection, zval *resolvedPhpDoc, zval *nativeReturnType) + { + bool ok; + zv::Val returnTag = call(resolvedPhpDoc, PT_LC("getreturntag")); + if (UNEXPECTED(returnTag.isUndef())) return zv::Val(); + if (Z_TYPE_P(returnTag.raw()) == IS_NULL) return zv::Val::null(); + zv::Val tagType = call(returnTag.raw(), PT_LC("gettype")); + if (UNEXPECTED(tagType.isUndef())) return zv::Val(); + zv::Val activeTemplateTypeMap = call(phpDocBlockClassReflection, PT_LC("getactivetemplatetypemap")); + if (UNEXPECTED(activeTemplateTypeMap.isUndef())) return zv::Val(); + zv::Val callSiteVarianceMap = call(phpDocBlockClassReflection, PT_LC("getcallsitevariancemap")); + if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + zv::Val covariantVal = varianceCovariant(); + if (UNEXPECTED(covariantVal.isUndef())) return zv::Val(); + zval *covariant = covariantVal.raw(); + zv::Val phpDocReturnType = pt_type_template_type_helper_resolve_template_types(tagType.raw(), activeTemplateTypeMap.raw(), callSiteVarianceMap.raw(), covariant, false); + if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); + + bool isExplicit = callBool(returnTag.raw(), PT_LC("isexplicit"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isExplicit) return phpDocReturnType; + bool superType = isSuperTypeOfYes(nativeReturnType, phpDocReturnType.raw(), ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (superType) return phpDocReturnType; + if (!isUnionType(phpDocReturnType.raw())) return zv::Val::null(); + zv::Val innerTypes = pt_type_op(Z_OBJ_P(phpDocReturnType.raw()), PT_OP_GET_TYPES, 0, NULL); + if (UNEXPECTED(innerTypes.isUndef()) || Z_TYPE_P(innerTypes.raw()) != IS_ARRAY) return zv::Val(); + zv::Arr kept = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(innerTypes.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(innerTypes.raw())) { + bool accepted = isSuperTypeOfYes(nativeReturnType, entry.value().raw(), ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!accepted) continue; + kept.push(entry.value()); + } + if (zend_hash_num_elements(kept.table()) == 0) return zv::Val::null(); + return kernelStaticSpread(PT_LC("PHPStan\\Type\\TypeCombinator"), PT_LC("union"), kept.table()); + } + + /* Mirrors findMethodPhpDocIncludingAncestors(): [ResolvedPhpDocBlock, + * ClassReflection] or PHP null. */ + zv::Val findMethodPhpDocIncludingAncestors(zval *declaringClass, zval *implementingClass, zend_string *methodName, zval *positionalParameterNames) + { + bool ok; + zv::Val declaringClassName = pt_class_reflection_get_name(Z_OBJ_P(declaringClass)); + if (UNEXPECTED(declaringClassName.isUndef())) return zv::Val(); + zv::Val implementingClassName = pt_class_reflection_get_name(Z_OBJ_P(implementingClass)); + if (UNEXPECTED(implementingClassName.isUndef())) return zv::Val(); + zval methodNameArg; + ZVAL_STR(&methodNameArg, methodName); + + zv::Args args{declaringClassName.raw(), implementingClassName.raw(), &methodNameArg, positionalParameterNames}; + zv::Val resolved = call(slot(PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER), PT_LC("findmethodphpdoc"), 4, args); + if (UNEXPECTED(resolved.isUndef())) return zv::Val(); + if (Z_TYPE_P(resolved.raw()) != IS_NULL) { + zv::Arr pair = zv::Arr::create(2); + pair.push(zv::Val::copyOf(zv::Ref(resolved.raw()))); + pair.push(zv::Val::copyOf(zv::Ref(declaringClass))); + return zv::Val(std::move(pair)); + } + + bool isKnownClass = callBool(slot(PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER), PT_LC("isknownclass"), 1, declaringClassName.raw(), ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!isKnownClass) { + bool isBuiltin = callBool(declaringClass, PT_LC("isbuiltin"), 0, NULL, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!isBuiltin) return zv::Val::null(); + } + + zv::Val ancestors = call(declaringClass, PT_LC("getancestors")); + if (UNEXPECTED(ancestors.isUndef()) || Z_TYPE_P(ancestors.raw()) != IS_ARRAY) return zv::Val(); + for (zv::ArrayEntry entry : zv::ArrRef(ancestors.raw())) { + zval *ancestor = entry.value().raw(); + ZVAL_DEREF(ancestor); + if (Z_TYPE_P(ancestor) != IS_OBJECT) continue; + zv::Val ancestorName = pt_class_reflection_get_name(Z_OBJ_P(ancestor)); + if (UNEXPECTED(ancestorName.isUndef())) return zv::Val(); + if (zend_string_equals(Z_STR_P(ancestorName.raw()), Z_STR_P(declaringClassName.raw()))) continue; + bool hasNative = callBool(ancestor, PT_LC("hasnativemethod"), 1, &methodNameArg, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (!hasNative) continue; + zv::Args ancestorArgs{ancestorName.raw(), ancestorName.raw(), &methodNameArg, positionalParameterNames}; + zv::Val ancestorResolved = call(slot(PT_PCRE_PROP_STUB_PHP_DOC_PROVIDER), PT_LC("findmethodphpdoc"), 4, ancestorArgs); + if (UNEXPECTED(ancestorResolved.isUndef())) return zv::Val(); + if (Z_TYPE_P(ancestorResolved.raw()) == IS_NULL) continue; + if (!isKnownClass) { + bool isGeneric; + if (UNEXPECTED(!pt_class_reflection_is_generic(Z_OBJ_P(ancestor), isGeneric))) return zv::Val(); + if (isGeneric) continue; + } + zv::Arr pair = zv::Arr::create(2); + pair.push(zv::Val::copyOf(zv::Ref(ancestorResolved.raw()))); + pair.push(zv::Val::copyOf(zv::Ref(ancestor))); + return zv::Val(std::move(pair)); + } + + return zv::Val::null(); + } + + /* Mirrors inferPrivatePropertyType(). */ + zv::Val inferPrivatePropertyType(zend_string *propertyName, zval *constructor) + { + zv::Val declaringClass = call(constructor, PT_LC("getdeclaringclass")); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + zv::Val declaringClassName = pt_class_reflection_get_name(Z_OBJ_P(declaringClass.raw())); + if (UNEXPECTED(declaringClassName.isUndef())) return zv::Val(); + zend_string *className = Z_STR_P(declaringClassName.raw()); + if (issetIn(slot(PT_PCRE_PROP_INFER_IN_PROCESS), className) != NULL) return zv::Val::null(); + setIn(slot(PT_PCRE_PROP_INFER_IN_PROCESS), className, zv::Val::boolean(true)); + zv::Val propertyTypes = inferAndCachePropertyTypes(constructor); + unsetIn(slot(PT_PCRE_PROP_INFER_IN_PROCESS), className); + if (UNEXPECTED(propertyTypes.isUndef())) return zv::Val(); + zval *found = keyIn(propertyTypes.raw(), propertyName); + if (found != NULL) return zv::Val::copyOf(zv::Ref(found)); + return zv::Val::null(); + } + + /* Mirrors inferAndCachePropertyTypes(): an array. */ + zv::Val inferAndCachePropertyTypes(zval *constructor) + { + zv::Val declaringClass = call(constructor, PT_LC("getdeclaringclass")); + if (UNEXPECTED(declaringClass.isUndef())) return zv::Val(); + zv::Val declaringClassNameVal = pt_class_reflection_get_name(Z_OBJ_P(declaringClass.raw())); + if (UNEXPECTED(declaringClassNameVal.isUndef())) return zv::Val(); + zv::Str className = zv::Str::copyOf(Z_STR_P(declaringClassNameVal.raw())); + zval *cached = issetIn(slot(PT_PCRE_PROP_PROPERTY_TYPES_CACHE), className.get()); + if (cached != NULL) return zv::Val::copyOf(zv::Ref(cached)); + + zv::Val fileName = call(declaringClass.raw(), PT_LC("getfilename")); + if (UNEXPECTED(fileName.isUndef())) return zv::Val(); + if (Z_TYPE_P(fileName.raw()) != IS_STRING) return cachePropertyTypes(className.get(), zv::Val(zv::Arr::empty())); + + zv::Val nodes = call(slot(PT_PCRE_PROP_PARSER), PT_LC("parsefile"), 1, fileName.raw()); + if (UNEXPECTED(nodes.isUndef())) return zv::Val(); + zv::Val classNode = findClassNode(className.get(), nodes.raw()); + if (UNEXPECTED(classNode.isUndef())) return zv::Val(); + if (Z_TYPE_P(classNode.raw()) != IS_OBJECT) return cachePropertyTypes(className.get(), zv::Val(zv::Arr::empty())); + + zv::Str constructorName = callString(constructor, PT_LC("getname")); + if (UNEXPECTED(constructorName.isNull())) return zv::Val(); + zv::Ref classStmts = zv::ObjRef(Z_OBJ_P(classNode.raw())).prop(PT_LC("stmts")); + if (classStmts.raw() == NULL || Z_TYPE_P(classStmts.raw()) != IS_ARRAY) return cachePropertyTypes(className.get(), zv::Val(zv::Arr::empty())); + zv::Val methodNode = findConstructorNode(constructorName.get(), classStmts.raw()); + if (UNEXPECTED(methodNode.isUndef())) return zv::Val(); + if (Z_TYPE_P(methodNode.raw()) != IS_OBJECT) return cachePropertyTypes(className.get(), zv::Val(zv::Arr::empty())); + zv::Ref methodStmts = zv::ObjRef(Z_OBJ_P(methodNode.raw())).prop(PT_LC("stmts")); + if (methodStmts.raw() == NULL || Z_TYPE_P(methodStmts.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(methodStmts.raw())) == 0) { + return cachePropertyTypes(className.get(), zv::Val(zv::Arr::empty())); + } + + zv::Val scopeContext = pt_type_call_static_ce(pt_ce_scope_context, PT_LC("create"), 1, fileName.raw()); + if (UNEXPECTED(scopeContext.isUndef())) return zv::Val(); + zv::Val classScope = call(slot(PT_PCRE_PROP_SCOPE_FACTORY), PT_LC("create"), 1, scopeContext.raw()); + if (UNEXPECTED(classScope.isUndef())) return zv::Val(); + const char *lastSeparator = zend_memnrstr(ZSTR_VAL(className.get()), "\\", 1, ZSTR_VAL(className.get()) + ZSTR_LEN(className.get())); + if (lastSeparator != NULL) { + zv::Val ns = zv::Val::string(ZSTR_VAL(className.get()), (size_t) (lastSeparator - ZSTR_VAL(className.get()))); + classScope = call(classScope.raw(), PT_LC("enternamespace"), 1, ns.raw()); + if (UNEXPECTED(classScope.isUndef())) return zv::Val(); + } + classScope = call(classScope.raw(), PT_LC("enterclass"), 1, declaringClass.raw()); + if (UNEXPECTED(classScope.isUndef())) return zv::Val(); + + zv::Args phpDocsArgs{classScope.raw(), methodNode.raw()}; + zv::Val phpDocs = call(slot(PT_PCRE_PROP_PHP_DOCS_RESOLVER), PT_LC("getphpdocs"), 2, phpDocsArgs); + if (UNEXPECTED(phpDocs.isUndef()) || Z_TYPE_P(phpDocs.raw()) != IS_ARRAY) return zv::Val(); + zval *docs[21]; + for (uint32_t i = 0; i < 21; i++) { + docs[i] = zend_hash_index_find(Z_ARRVAL_P(phpDocs.raw()), i); + if (docs[i] == NULL) { + throwShouldNotHappen(); + return zv::Val(); + } + ZVAL_DEREF(docs[i]); + } + + zval enterArgs[20]; + ZVAL_COPY_VALUE(&enterArgs[0], methodNode.raw()); + ZVAL_COPY_VALUE(&enterArgs[1], docs[0]); /* templateTypeMap */ + ZVAL_COPY_VALUE(&enterArgs[2], docs[1]); /* phpDocParameterTypes */ + ZVAL_COPY_VALUE(&enterArgs[3], docs[4]); /* phpDocReturnType */ + ZVAL_COPY_VALUE(&enterArgs[4], docs[5]); /* phpDocThrowType */ + ZVAL_COPY_VALUE(&enterArgs[5], docs[6]); /* deprecatedDescription */ + ZVAL_COPY_VALUE(&enterArgs[6], docs[7]); /* isDeprecated */ + ZVAL_COPY_VALUE(&enterArgs[7], docs[8]); /* isInternal */ + ZVAL_COPY_VALUE(&enterArgs[8], docs[9]); /* isFinal */ + ZVAL_COPY_VALUE(&enterArgs[9], docs[10]); /* isPure */ + ZVAL_COPY_VALUE(&enterArgs[10], docs[11]); /* acceptsNamedArguments */ + ZVAL_COPY_VALUE(&enterArgs[11], docs[14]); /* asserts */ + ZVAL_COPY_VALUE(&enterArgs[12], docs[15]); /* selfOutType */ + ZVAL_COPY_VALUE(&enterArgs[13], docs[13]); /* phpDocComment */ + ZVAL_COPY_VALUE(&enterArgs[14], docs[16]); /* phpDocParameterOutTypes */ + ZVAL_COPY_VALUE(&enterArgs[15], docs[2]); /* immediatelyInvokedCallableParameters */ + ZVAL_COPY_VALUE(&enterArgs[16], docs[3]); /* phpDocClosureThisTypeParameters */ + ZVAL_FALSE(&enterArgs[17]); + ZVAL_NULL(&enterArgs[18]); + ZVAL_COPY_VALUE(&enterArgs[19], docs[20]); /* pureUnlessCallableIsImpureParameters */ + zv::Val methodScope = call(classScope.raw(), PT_LC("enterclassmethod"), 20, enterArgs); + if (UNEXPECTED(methodScope.isUndef())) return zv::Val(); + + zend_class_entry *expressionStmtCe = pt_class(PT_CLASS_EXPRESSION_STMT); + zend_class_entry *assignCe = pt_class(PT_CLASS_ASSIGN_EXPR); + zend_class_entry *propertyFetchCe = pt_class(PT_CLASS_PROPERTY_FETCH); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *identifierCe = pt_class(PT_CLASS_IDENTIFIER); + if (UNEXPECTED(expressionStmtCe == NULL || assignCe == NULL || propertyFetchCe == NULL || variableCe == NULL || identifierCe == NULL)) return zv::Val(); + + zv::Arr propertyTypes = zv::Arr::create(0); + zv::Val ownedStmts = zv::Val::copyOf(methodStmts); + for (zv::ArrayEntry entry : zv::ArrRef(ownedStmts.raw())) { + zval *statement = entry.value().raw(); + ZVAL_DEREF(statement); + if (Z_TYPE_P(statement) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(statement), expressionStmtCe)) continue; + zv::Ref exprRef = zv::ObjRef(Z_OBJ_P(statement)).prop(PT_LC("expr")); + if (exprRef.raw() == NULL) continue; + zval *expr = exprRef.deref().raw(); + if (Z_TYPE_P(expr) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(expr), assignCe)) continue; + zv::Ref varRef = zv::ObjRef(Z_OBJ_P(expr)).prop(PT_LC("var")); + if (varRef.raw() == NULL) continue; + zval *propertyFetch = varRef.deref().raw(); + if (Z_TYPE_P(propertyFetch) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(propertyFetch), propertyFetchCe)) continue; + zv::Ref fetchVarRef = zv::ObjRef(Z_OBJ_P(propertyFetch)).prop(PT_LC("var")); + if (fetchVarRef.raw() == NULL) continue; + zval *fetchVar = fetchVarRef.deref().raw(); + if (Z_TYPE_P(fetchVar) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(fetchVar), variableCe)) continue; + zv::Ref fetchVarName = zv::ObjRef(Z_OBJ_P(fetchVar)).prop(PT_LC("name")); + if (fetchVarName.raw() == NULL || !fetchVarName.deref().stringEquals("this")) continue; + zv::Ref fetchNameRef = zv::ObjRef(Z_OBJ_P(propertyFetch)).prop(PT_LC("name")); + if (fetchNameRef.raw() == NULL) continue; + zval *fetchName = fetchNameRef.deref().raw(); + if (Z_TYPE_P(fetchName) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(fetchName), identifierCe)) continue; + + // an independent lazy pass on its own scope - never read through + // Scope::getType(), which is reserved for the file's main walk + zv::Ref assignExprRef = zv::ObjRef(Z_OBJ_P(expr)).prop(PT_LC("expr")); + if (assignExprRef.raw() == NULL) continue; + zv::Val storage = pt_expression_result_storage_new(); + if (UNEXPECTED(storage.isUndef())) return zv::Val(); + zv::Args processArgs{assignExprRef.deref().raw(), methodScope.raw(), storage.raw()}; + zv::Val result = call(slot(PT_PCRE_PROP_NODE_SCOPE_RESOLVER), PT_LC("processexprondemand"), 3, processArgs); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + zv::Val propertyType = call(result.raw(), PT_LC("gettype")); + if (UNEXPECTED(propertyType.isUndef())) return zv::Val(); + if (isErrorType(propertyType.raw()) || isNeverType(propertyType.raw())) continue; + + zv::Val precision = pt_type_call_static(PT_CLASS_GENERALIZE_PRECISION, PT_LC("lessspecific"), 0, NULL); + if (UNEXPECTED(precision.isUndef())) return zv::Val(); + propertyType = call(propertyType.raw(), PT_LC("generalize"), 1, precision.raw()); + if (UNEXPECTED(propertyType.isUndef())) return zv::Val(); + bool ok; + bool isConstantArray = typeOpYes(propertyType.raw(), PT_OP_IS_CONSTANT_ARRAY, ok); + if (UNEXPECTED(!ok)) return zv::Val(); + if (isConstantArray) { + zv::Val ownedKeyType = explicitMixedType(); + if (UNEXPECTED(ownedKeyType.isUndef())) return zv::Val(); + zv::Val ownedItemType = explicitMixedType(); + if (UNEXPECTED(ownedItemType.isUndef())) return zv::Val(); + propertyType = arrayType(ownedKeyType.raw(), ownedItemType.raw()); + if (UNEXPECTED(propertyType.isUndef())) return zv::Val(); + } + + zv::Str propertyKey = callString(fetchName, PT_LC("tostring")); + if (UNEXPECTED(propertyKey.isNull())) return zv::Val(); + propertyTypes.set(propertyKey.get(), std::move(propertyType)); + } + + return cachePropertyTypes(className.get(), zv::Val(std::move(propertyTypes))); + } + + /* $this->propertyTypesCache[$className] = $types; return $types; */ + zv::Val cachePropertyTypes(zend_string *className, zv::Val types) + { + zv::Val result = zv::Val::copyOf(zv::Ref(types.raw())); + setIn(slot(PT_PCRE_PROP_PROPERTY_TYPES_CACHE), className, std::move(types)); + return result; + } + + /* Mirrors findClassNode(): the Class_ node or PHP null. */ + zv::Val findClassNode(zend_string *className, zval *nodes) + { + if (Z_TYPE_P(nodes) != IS_ARRAY) return zv::Val::null(); + zend_class_entry *classStmtCe = pt_class(PT_CLASS_CLASS_STMT); + zend_class_entry *namespaceCe = pt_class(PT_CLASS_NAMESPACE_STMT); + zend_class_entry *declareCe = pt_class(PT_CLASS_DECLARE_STMT); + if (UNEXPECTED(classStmtCe == NULL || namespaceCe == NULL || declareCe == NULL)) return zv::Val(); + zv::Val ownedNodes = zv::Val::copyOf(zv::Ref(nodes)); + for (zv::ArrayEntry entry : zv::ArrRef(ownedNodes.raw())) { + zval *node = entry.value().raw(); + ZVAL_DEREF(node); + if (Z_TYPE_P(node) != IS_OBJECT) continue; + if (instanceof_function(Z_OBJCE_P(node), classStmtCe)) { + zv::Ref namespacedName = zv::ObjRef(Z_OBJ_P(node)).prop(PT_LC("namespacedName")); + if (namespacedName.raw() != NULL && namespacedName.deref().isObject()) { + zv::Str asString = callString(namespacedName.deref().raw(), PT_LC("tostring")); + if (UNEXPECTED(asString.isNull())) return zv::Val(); + if (zend_string_equals(asString.get(), className)) return zv::Val::copyOf(zv::Ref(node)); + } + } + if (!instanceof_function(Z_OBJCE_P(node), namespaceCe) && !instanceof_function(Z_OBJCE_P(node), declareCe)) continue; + zv::Val subNodeNames = call(node, PT_LC("getsubnodenames")); + if (UNEXPECTED(subNodeNames.isUndef()) || Z_TYPE_P(subNodeNames.raw()) != IS_ARRAY) return zv::Val(); + for (zv::ArrayEntry nameEntry : zv::ArrRef(subNodeNames.raw())) { + zval *subNodeName = nameEntry.value().raw(); + ZVAL_DEREF(subNodeName); + if (Z_TYPE_P(subNodeName) != IS_STRING) continue; + zv::Ref subNode = zv::ObjRef(Z_OBJ_P(node)).prop(ZSTR_VAL(Z_STR_P(subNodeName)), ZSTR_LEN(Z_STR_P(subNodeName))); + if (subNode.raw() == NULL) continue; + zv::Val wrapped; + zval *subNodeArray; + if (subNode.deref().isArray()) { + subNodeArray = subNode.deref().raw(); + } else { + zv::Arr single = zv::Arr::create(1); + single.push(subNode.deref()); + wrapped = zv::Val(std::move(single)); + subNodeArray = wrapped.raw(); + } + zv::Val result = findClassNode(className, subNodeArray); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (Z_TYPE_P(result.raw()) == IS_NULL) continue; + return result; + } + } + return zv::Val::null(); + } + + /* Mirrors findConstructorNode(): the ClassMethod node or PHP null. */ + zv::Val findConstructorNode(zend_string *methodName, zval *classStatements) + { + if (Z_TYPE_P(classStatements) != IS_ARRAY) return zv::Val::null(); + zend_class_entry *classMethodCe = pt_class(PT_CLASS_CLASS_METHOD_STMT); + if (UNEXPECTED(classMethodCe == NULL)) return zv::Val(); + zv::Val ownedStatements = zv::Val::copyOf(zv::Ref(classStatements)); + for (zv::ArrayEntry entry : zv::ArrRef(ownedStatements.raw())) { + zval *statement = entry.value().raw(); + ZVAL_DEREF(statement); + if (Z_TYPE_P(statement) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(statement), classMethodCe)) continue; + zv::Ref name = zv::ObjRef(Z_OBJ_P(statement)).prop(PT_LC("name")); + if (name.raw() == NULL || !name.deref().isObject()) continue; + zv::Str asString = callString(name.deref().raw(), PT_LC("tostring")); + if (UNEXPECTED(asString.isNull())) return zv::Val(); + if (zend_string_equals(asString.get(), methodName)) return zv::Val::copyOf(zv::Ref(statement)); + } + return zv::Val::null(); + } + +private: + zend_object *self; +}; + +} // namespace phpstanturbo + +/* {{{ registration — the engine ABI glue */ + +namespace { + +/* the twin's promoted constructor parameter class names (persistent + * literals; Nette reflects them while it compiles the container) */ +constexpr const char *pcreScopeFactory = "PHPStan\\Analyser\\ScopeFactory"; +constexpr const char *pcrePhpDocsResolver = "PHPStan\\Analyser\\PhpDocsResolver"; +constexpr const char *pcreNodeScopeResolver = "PHPStan\\Analyser\\NodeScopeResolver"; +constexpr const char *pcreMethodReflectionFactory = "PHPStan\\Reflection\\Php\\PhpMethodReflectionFactory"; +constexpr const char *pcrePhpDocInheritanceResolver = "PHPStan\\PhpDoc\\PhpDocInheritanceResolver"; +constexpr const char *pcreDeprecationProvider = "PHPStan\\Reflection\\Deprecation\\DeprecationProvider"; +constexpr const char *pcreAnnotationsMethods = "PHPStan\\Reflection\\Annotations\\AnnotationsMethodsClassReflectionExtension"; +constexpr const char *pcreAnnotationsProperties = "PHPStan\\Reflection\\Annotations\\AnnotationsPropertiesClassReflectionExtension"; +constexpr const char *pcreSignatureMapProvider = "PHPStan\\Reflection\\SignatureMap\\SignatureMapProvider"; +constexpr const char *pcreParser = "PHPStan\\Parser\\Parser"; +constexpr const char *pcreStubPhpDocProvider = "PHPStan\\PhpDoc\\StubPhpDocProvider"; +constexpr const char *pcreReflectionProviderProvider = "PHPStan\\Reflection\\ReflectionProvider\\ReflectionProviderProvider"; +constexpr const char *pcreFileTypeMapper = "PHPStan\\Type\\FileTypeMapper"; +constexpr const char *pcreAttributeReflectionFactory = "PHPStan\\Reflection\\AttributeReflectionFactory"; +constexpr const char *pcreAllowedConstantsMapProvider = "PHPStan\\Reflection\\ParameterAllowedConstantsMapProvider"; +constexpr const char *pcrePhpVersion = "PHPStan\\Php\\PhpVersion"; +constexpr const char *pcreLruCache = "PHPStan\\Internal\\LruCache"; + +} // namespace + +void pt_php_class_reflection_extension_rinit() +{ + pt_pcre_adapter_slots = { false, false, NULL, 0, false, NULL, 0, NULL, 0, 0 }; +} + +void pt_register_php_class_reflection_extension() +{ + reg::Class cls("PHPStan\\Reflection\\Php\\PhpClassReflectionExtension"); + ptdecl::PhpClassReflectionExtension::declareClass(cls); + + /* the twin's properties in declaration order (the OBJ_PROP_NUM slots) */ + cls.privateTypedClassProperty("memberCacheOrder", pcreLruCache, false); + cls.privateTypedArrayPropertyDefaultEmpty("propertiesIncludingAnnotations"); + cls.privateTypedArrayPropertyDefaultEmpty("nativeProperties"); + cls.privateTypedArrayPropertyDefaultEmpty("methodsIncludingAnnotations"); + cls.privateTypedArrayPropertyDefaultEmpty("nativeMethods"); + cls.privateTypedArrayPropertyDefaultEmpty("propertyTypesCache"); + cls.privateTypedArrayPropertyDefaultEmpty("inferClassConstructorPropertyTypesInProcess"); + cls.privateTypedClassProperty("scopeFactory", pcreScopeFactory, false); + cls.privateTypedClassProperty("phpDocsResolver", pcrePhpDocsResolver, false); + cls.privateTypedClassProperty("nodeScopeResolver", pcreNodeScopeResolver, false); + cls.privateTypedClassProperty("methodReflectionFactory", pcreMethodReflectionFactory, false); + cls.privateTypedClassProperty("phpDocInheritanceResolver", pcrePhpDocInheritanceResolver, false); + cls.privateTypedClassProperty("deprecationProvider", pcreDeprecationProvider, false); + cls.privateTypedClassProperty("annotationsMethodsClassReflectionExtension", pcreAnnotationsMethods, false); + cls.privateTypedClassProperty("annotationsPropertiesClassReflectionExtension", pcreAnnotationsProperties, false); + cls.privateTypedClassProperty("signatureMapProvider", pcreSignatureMapProvider, false); + cls.privateTypedClassProperty("parser", pcreParser, false); + cls.privateTypedClassProperty("stubPhpDocProvider", pcreStubPhpDocProvider, false); + cls.privateTypedClassProperty("reflectionProviderProvider", pcreReflectionProviderProvider, false); + cls.privateTypedClassProperty("fileTypeMapper", pcreFileTypeMapper, false); + cls.privateTypedClassProperty("attributeReflectionFactory", pcreAttributeReflectionFactory, false); + cls.privateTypedClassProperty("allowedConstantsMapProvider", pcreAllowedConstantsMapProvider, false); + cls.privateTypedProperty("inferPrivatePropertyTypeFromConstructor", MAY_BE_BOOL); + cls.privateTypedClassProperty("phpVersion", pcrePhpVersion, false); + + /* the DI service's constructor: the parameter class names are the + * twin's exactly — Nette reflects them to autowire the service and + * pairs the two #[AutowiredParameter]s by name (README rule 6) */ + cls.method("__construct", reg::Public, 18, { + reg::obj("scopeFactory", pcreScopeFactory), + reg::obj("phpDocsResolver", pcrePhpDocsResolver), + reg::obj("nodeScopeResolver", pcreNodeScopeResolver), + reg::obj("methodReflectionFactory", pcreMethodReflectionFactory), + reg::obj("phpDocInheritanceResolver", pcrePhpDocInheritanceResolver), + reg::obj("deprecationProvider", pcreDeprecationProvider), + reg::obj("annotationsMethodsClassReflectionExtension", pcreAnnotationsMethods), + reg::obj("annotationsPropertiesClassReflectionExtension", pcreAnnotationsProperties), + reg::obj("signatureMapProvider", pcreSignatureMapProvider), + reg::obj("parser", pcreParser), + reg::obj("stubPhpDocProvider", pcreStubPhpDocProvider), + reg::obj("reflectionProviderProvider", pcreReflectionProviderProvider), + reg::obj("fileTypeMapper", pcreFileTypeMapper), + reg::obj("attributeReflectionFactory", pcreAttributeReflectionFactory), + reg::obj("allowedConstantsMapProvider", pcreAllowedConstantsMapProvider), + reg::boolArg("inferPrivatePropertyTypeFromConstructor"), + reg::obj("phpVersion", pcrePhpVersion), + reg::longArg("memberCacheKeysMax"), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + ConstructorArgs a; + bool inferPrivatePropertyTypeFromConstructor; + ZEND_PARSE_PARAMETERS_START(18, 18) + Z_PARAM_OBJECT(a.scopeFactory) + Z_PARAM_OBJECT(a.phpDocsResolver) + Z_PARAM_OBJECT(a.nodeScopeResolver) + Z_PARAM_OBJECT(a.methodReflectionFactory) + Z_PARAM_OBJECT(a.phpDocInheritanceResolver) + Z_PARAM_OBJECT(a.deprecationProvider) + Z_PARAM_OBJECT(a.annotationsMethodsClassReflectionExtension) + Z_PARAM_OBJECT(a.annotationsPropertiesClassReflectionExtension) + Z_PARAM_OBJECT(a.signatureMapProvider) + Z_PARAM_OBJECT(a.parser) + Z_PARAM_OBJECT(a.stubPhpDocProvider) + Z_PARAM_OBJECT(a.reflectionProviderProvider) + Z_PARAM_OBJECT(a.fileTypeMapper) + Z_PARAM_OBJECT(a.attributeReflectionFactory) + Z_PARAM_OBJECT(a.allowedConstantsMapProvider) + Z_PARAM_BOOL(inferPrivatePropertyTypeFromConstructor) + Z_PARAM_OBJECT(a.phpVersion) + Z_PARAM_LONG(a.memberCacheKeysMax) + ZEND_PARSE_PARAMETERS_END(); + a.inferPrivatePropertyTypeFromConstructor = inferPrivatePropertyTypeFromConstructor; + if (UNEXPECTED(!phpstanturbo::PhpClassReflectionExtension::construct(Z_OBJ_P(ZEND_THIS), a))) RETURN_THROWS(); + }); + + cls.method("hasProperty", reg::Public, 2, { reg::objectArg("classReflection"), reg::stringArg("propertyName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + zend_string *propertyName; + if (!zp::parse(execute_data, classReflection, propertyName)) RETURN_THROWS(); + bool ok; + bool result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).hasProperty(classReflection, propertyName, ok); + if (UNEXPECTED(!ok)) RETURN_THROWS(); + RETURN_BOOL(result); + }); + + cls.method("getProperty", reg::Public, 3, { reg::objectArg("classReflection"), reg::stringArg("propertyName"), reg::objectArg("scope") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection, *scope; + zend_string *propertyName; + if (!zp::parse(execute_data, classReflection, propertyName, scope)) RETURN_THROWS(); + zv::Val result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).getProperty(classReflection, propertyName, scope); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.method("getNativeProperty", reg::Public, 2, { reg::objectArg("classReflection"), reg::stringArg("propertyName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + zend_string *propertyName; + if (!zp::parse(execute_data, classReflection, propertyName)) RETURN_THROWS(); + zv::Val result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).getNativeProperty(classReflection, propertyName); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.method("hasMethod", reg::Public, 2, { reg::objectArg("classReflection"), reg::stringArg("methodName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + zend_string *methodName; + if (!zp::parse(execute_data, classReflection, methodName)) RETURN_THROWS(); + bool ok; + bool result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).hasMethod(classReflection, methodName, ok); + if (UNEXPECTED(!ok)) RETURN_THROWS(); + RETURN_BOOL(result); + }); + + cls.method("getMethod", reg::Public, 2, { reg::objectArg("classReflection"), reg::stringArg("methodName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + zend_string *methodName; + if (!zp::parse(execute_data, classReflection, methodName)) RETURN_THROWS(); + zv::Val result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).getMethod(classReflection, methodName); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.method("hasNativeMethod", reg::Public, 2, { reg::objectArg("classReflection"), reg::stringArg("methodName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + zend_string *methodName; + if (!zp::parse(execute_data, classReflection, methodName)) RETURN_THROWS(); + bool ok; + bool result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).hasNativeMethod(classReflection, methodName, ok); + if (UNEXPECTED(!ok)) RETURN_THROWS(); + RETURN_BOOL(result); + }); + + cls.method("getNativeMethod", reg::Public, 2, { reg::objectArg("classReflection"), reg::stringArg("methodName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *classReflection; + zend_string *methodName; + if (!zp::parse(execute_data, classReflection, methodName)) RETURN_THROWS(); + zv::Val result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).getNativeMethod(classReflection, methodName); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.method("createUserlandMethodReflection", reg::Public, 4, { + reg::objectArg("fileDeclaringClass"), + reg::objectArg("actualDeclaringClass"), + reg::objectArg("methodReflection"), + reg::stringArg("declaringTraitName", true), + }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *fileDeclaringClass, *actualDeclaringClass, *methodReflection; + zend_string *declaringTraitName; + if (!zp::parse(execute_data, fileDeclaringClass, actualDeclaringClass, methodReflection, declaringTraitName)) RETURN_THROWS(); + zval traitNameArg; + if (declaringTraitName != NULL) { + ZVAL_STR(&traitNameArg, declaringTraitName); + } else { + ZVAL_NULL(&traitNameArg); + } + zv::Val result = phpstanturbo::PhpClassReflectionExtension(Z_OBJ_P(ZEND_THIS)).createUserlandMethodReflection(fileDeclaringClass, actualDeclaringClass, methodReflection, &traitNameArg); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.shadow(&pt_ce_php_class_reflection_extension); +} + +/* }}} */ diff --git a/turbo-ext/src/ReflectionAccess.cpp b/turbo-ext/src/ReflectionAccess.cpp index 30f8466d1fb..3b84622d16b 100644 --- a/turbo-ext/src/ReflectionAccess.cpp +++ b/turbo-ext/src/ReflectionAccess.cpp @@ -8,9 +8,14 @@ * $instance` (the method, which throws, while it is null); * - MemoizingReflectionProvider::hasClass() / getClass(): the memoized * answer from $knownClasses / $unknownClasses / $classes (the method, - * which asks the decorated provider and memoizes, on a miss). + * which asks the decorated provider and memoizes, on a miss); + * - LazyClassReflectionExtensionRegistryProvider::getRegistry() followed by + * one ClassReflectionExtensionRegistry getter: the built registry from the + * provider's $registry memo and the extension from the registry's own + * constructor-written slot (the methods while the memo is still null, or + * for any other provider implementation). * - * Same contract as ClassReflectionAccess.cpp: only an object of exactly + * Same contract as the scope readers of ScopeContext.cpp: only an object of exactly * the twin's class entry (resolved once through the class map, without * autoloading — an object of an undeclared class cannot exist) takes the * fast path, its slot offsets cached per class entry and forgotten at @@ -45,6 +50,39 @@ struct StaticSlot MemoizingProviderSlots pt_mrp_slots = { NULL, 0, 0, 0 }; StaticSlot pt_rpsa_slot = { NULL, NULL }; +/* the $registry memo slot of the lazy registry provider's class entry */ +struct RegistryProviderSlots +{ + zend_class_entry *ce; + uint32_t registry; +}; + +/* the registry's constructor-written slots, in pt_registry_member order */ +struct RegistrySlots +{ + zend_class_entry *ce; + uint32_t members[PT_REGISTRY_MEMBER_COUNT]; +}; + +RegistryProviderSlots pt_registry_provider_slots = { NULL, 0 }; +RegistrySlots pt_registry_slots = { NULL, { 0, 0, 0, 0, 0, 0 } }; + +/* the registry property and the twin's getter behind each member */ +struct RegistryMemberNames +{ + const char *property; + const char *getter; +}; + +const RegistryMemberNames pt_registry_member_names[PT_REGISTRY_MEMBER_COUNT] = { + /* PT_REGISTRY_PHP_CLASS_REFLECTION_EXTENSION */ { "phpClassReflectionExtension", "getphpclassreflectionextension" }, + /* PT_REGISTRY_METHODS_EXTENSIONS */ { "methodsClassReflectionExtensions", "getmethodsclassreflectionextensions" }, + /* PT_REGISTRY_PROPERTIES_EXTENSIONS */ { "propertiesClassReflectionExtensions", "getpropertiesclassreflectionextensions" }, + /* PT_REGISTRY_REQUIRE_EXTENDS_METHODS_EXTENSION */ { "requireExtendsMethodsClassReflectionExtension", "getrequireextendsmethodsclassreflectionextension" }, + /* PT_REGISTRY_REQUIRE_EXTENDS_PROPERTIES_EXTENSION */ { "requireExtendsPropertiesClassReflectionExtension", "getrequireextendspropertyclassreflectionextension" }, + /* PT_REGISTRY_ALLOWED_SUB_TYPES_EXTENSIONS */ { "allowedSubTypesClassReflectionExtensions", "getallowedsubtypesclassreflectionextensions" }, +}; + /* the static-property slot of a declared user class once the engine has * initialized its statics (the constants updated, the table allocated — * what the first static access through the engine does); NULL until @@ -109,6 +147,8 @@ void pt_reflection_access_rinit() { pt_mrp_slots.ce = NULL; pt_rpsa_slot = { NULL, NULL }; + pt_registry_provider_slots.ce = NULL; + pt_registry_slots.ce = NULL; } /* {{{ ReflectionProviderStaticAccessor */ @@ -199,3 +239,107 @@ zv::Val pt_reflection_provider_get_class(zend_object *provider, zval *className) } /* }}} */ + +/* {{{ ClassReflectionExtensionRegistryProvider */ + +namespace { + +/* the memo slot of an object that is exactly a + * LazyClassReflectionExtensionRegistryProvider; NULL when it is of some other + * class (the caller then calls getRegistry()) — or, with `error` set and an + * exception pending, when the class map cannot resolve the class at all */ +const RegistryProviderSlots *registryProviderSlots(zend_object *provider, bool &error) +{ + error = false; + if (EXPECTED(provider->ce == pt_registry_provider_slots.ce)) return &pt_registry_provider_slots; + zend_class_entry *ce = pt_class_loaded(PT_CLASS_LAZY_CLASS_REFLECTION_EXTENSION_REGISTRY_PROVIDER); + if (ce == NULL) { + error = EG(exception) != NULL; + return NULL; + } + if (provider->ce != ce) return NULL; + int32_t registry = pt_instance_prop_offset(ce, PT_LC("registry")); + if (UNEXPECTED(registry < 0)) { + /* not the twin this reader knows: every call goes through the method */ + return NULL; + } + pt_registry_provider_slots = { ce, (uint32_t) registry }; + return &pt_registry_provider_slots; +} + +/* the slots of an object that is exactly a ClassReflectionExtensionRegistry; + * the same contract as registryProviderSlots() */ +const RegistrySlots *registrySlots(zend_object *registry, bool &error) +{ + error = false; + if (EXPECTED(registry->ce == pt_registry_slots.ce)) return &pt_registry_slots; + zend_class_entry *ce = pt_class_loaded(PT_CLASS_CLASS_REFLECTION_EXTENSION_REGISTRY); + if (ce == NULL) { + error = EG(exception) != NULL; + return NULL; + } + if (registry->ce != ce) return NULL; + RegistrySlots slots; + slots.ce = ce; + for (int i = 0; i < PT_REGISTRY_MEMBER_COUNT; i++) { + const char *property = pt_registry_member_names[i].property; + int32_t offset = pt_instance_prop_offset(ce, property, strlen(property)); + if (UNEXPECTED(offset < 0)) return NULL; + slots.members[i] = (uint32_t) offset; + } + pt_registry_slots = slots; + return &pt_registry_slots; +} + +} // namespace + +/* + * $this->classReflectionExtensionRegistryProvider->getRegistry()->() + * — the two accessor hops every member lookup of the native ClassReflection + * starts with, as property reads. + * + * The lazy provider builds its registry once and keeps it in $registry + * forever (it drops its container reference right after), and the registry is + * a final value class whose slots only its constructor writes — so a non-null + * memo and the slot behind the getter are what the two methods would answer, + * by construction. Anything else — the first call of a run, a provider or + * registry of some other class — takes the methods. + */ +zv::Val pt_class_reflection_extension_registry_member(zend_object *provider, pt_registry_member member) +{ + bool error; + zv::Val owned; + zval *registry = NULL; + const RegistryProviderSlots *providerSlots = registryProviderSlots(provider, error); + if (EXPECTED(providerSlots != NULL)) { + zval *memo = OBJ_PROP(provider, providerSlots->registry); + if (EXPECTED(Z_TYPE_P(memo) == IS_OBJECT)) { + registry = memo; + } + } else if (UNEXPECTED(error)) { + return zv::Val(); + } + if (registry == NULL) { + owned = pt_type_call(provider, PT_LC("getregistry"), 0, NULL); + if (UNEXPECTED(owned.isUndef())) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(owned.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function %s() on %s", pt_registry_member_names[member].getter, zend_zval_value_name(owned.raw())); + return zv::Val(); + } + registry = owned.raw(); + } + + const RegistrySlots *slots = registrySlots(Z_OBJ_P(registry), error); + if (EXPECTED(slots != NULL)) { + zval *value = OBJ_PROP(Z_OBJ_P(registry), slots->members[member]); + if (EXPECTED(Z_TYPE_P(value) != IS_UNDEF)) return zv::Val::copyOf(zv::Ref(value)); + /* a promoted property the constructor never wrote — the getter, which + * raises the twin's Error */ + } else if (UNEXPECTED(error)) { + return zv::Val(); + } + const char *getter = pt_registry_member_names[member].getter; + return pt_type_call(Z_OBJ_P(registry), getter, strlen(getter), 0, NULL); +} + +/* }}} */ diff --git a/turbo-ext/src/ScopeContext.cpp b/turbo-ext/src/ScopeContext.cpp index faa2aa32e85..b7bc3c4c90b 100644 --- a/turbo-ext/src/ScopeContext.cpp +++ b/turbo-ext/src/ScopeContext.cpp @@ -7,6 +7,11 @@ * * State lives in the three declared private property slots, so the standard * object handlers do GC/free/clone — no custom object struct. + * + * The file also carries pt_scope_is_in_class() / pt_scope_get_class_reflection(), + * the native readers of MutatingScope::isInClass()/getClassReflection(): both + * answer out of a native ScopeContext's $classReflection slot, so they live + * next to the slot readers above (see the block at the end of the file). */ #include "support.h" @@ -14,6 +19,7 @@ namespace slots = ptdecl::ScopeContext::slot; #include "zv.h" +#include "TypeTraits.h" zend_class_entry *pt_ce_scope_context = nullptr; @@ -22,6 +28,16 @@ zval *pt_scope_context_class_reflection(zend_object *context) return OBJ_PROP_NUM(context, slots::classReflection); } +zval *pt_scope_context_file(zend_object *context) +{ + return OBJ_PROP_NUM(context, slots::file); +} + +zval *pt_scope_context_trait_reflection(zend_object *context) +{ + return OBJ_PROP_NUM(context, slots::traitReflection); +} + namespace phpstanturbo { /* Mirrors PHPStan\Analyser\ScopeContext. State lives in the PHP object's @@ -167,8 +183,8 @@ class ScopeContext return pt_call_scope_bool(reflection.raw(), lcname, len, 0, NULL, &out); } - /* $reflection->getName() — a ClassReflection's memoized name read - * natively (ClassReflectionAccess.cpp); UNDEF = pending exception */ + /* $reflection->getName() — the native ClassReflection's body called + * directly (ClassReflection.cpp); UNDEF = pending exception */ static zv::Val callGetName(zv::Ref reflection) { return pt_class_reflection_get_name(reflection.asObject()); @@ -283,3 +299,111 @@ void pt_register_scope_context() } /* }}} */ + +/* {{{ MutatingScope::isInClass() / getClassReflection() for native callers + * + * Both read $this->context->getClassReflection(): when the scope is exactly + * a MutatingScope (a PHP subclass may override them) holding a native + * ScopeContext, the class reflection comes straight out of the context's + * slot; anything else goes through the PHP method as before. + * pt_ce_mutating_scope is the shadowed class (MutatingScope.cpp), NULL + * until activateShadowing() declared it — an object of a class that is + * not declared cannot exist, so a NULL entry simply means "not this + * class". */ + +namespace { + +/* the property slot offset of a class entry, resolved once; a user class's + * entry is per request without opcache, so rinit forgets it */ +struct ScopeSlots +{ + zend_class_entry *ce; + uint32_t context; +}; + +ScopeSlots pt_ms_slots = { NULL, 0 }; + +/* the subclass of MutatingScope last approved by the inheritance check */ +zend_class_entry *pt_ms_inherited_ce = NULL; + +/* whether a subclass inherits isInClass() and getClassReflection() from + * MutatingScope itself (declared there, not re-declared below it) */ +bool inheritsScopeGetters(zend_class_entry *ce, zend_class_entry *mutatingScope) +{ + static const struct { const char *name; size_t len; } methods[] = { { "isinclass", sizeof("isinclass") - 1 }, { "getclassreflection", sizeof("getclassreflection") - 1 } }; + for (const auto &method : methods) { + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, method.name, method.len); + if (fn == NULL || fn->common.scope != mutatingScope) return false; + } + return true; +} + +/* the $classReflection slot of the scope's context when the fast path + * applies (the scope exactly a MutatingScope, its context a native + * ScopeContext); NULL otherwise */ +zval *scopeClassReflectionSlot(zend_object *scope) +{ + if (scope->ce != pt_ms_slots.ce && scope->ce != pt_ms_inherited_ce) { + zend_class_entry *ce = pt_ce_mutating_scope; + if (ce == NULL) return NULL; + if (scope->ce != ce) { + /* a subclass (NodeCallbackScope) qualifies when it inherits both + * methods from MutatingScope unchanged: the bodies are then the + * twin's, reading the same inherited $context slot; the last + * such class is remembered (one subclass exists in practice) */ + if (!instanceof_function(scope->ce, ce) || !inheritsScopeGetters(scope->ce, ce)) return NULL; + } + int32_t context = pt_instance_prop_offset(ce, "context", sizeof("context") - 1); + if (UNEXPECTED(context < 0)) return NULL; + if (scope->ce == ce) { + pt_ms_slots = { ce, (uint32_t) context }; + } else { + pt_ms_slots.context = (uint32_t) context; + pt_ms_inherited_ce = scope->ce; + } + } + zval *context = OBJ_PROP(scope, pt_ms_slots.context); + if (Z_TYPE_P(context) != IS_OBJECT || Z_OBJCE_P(context) != pt_ce_scope_context) return NULL; + return pt_scope_context_class_reflection(Z_OBJ_P(context)); +} + +} // namespace + +void pt_scope_access_rinit() +{ + pt_ms_slots.ce = NULL; + pt_ms_inherited_ce = NULL; +} + +/* return $this->context->getClassReflection() !== null; */ +bool pt_scope_is_in_class(zend_object *scope, bool &out) +{ + zval *classReflection = scopeClassReflectionSlot(scope); + if (classReflection != NULL) { + if (Z_TYPE_P(classReflection) == IS_NULL) { + out = false; + return true; + } + if (EXPECTED(Z_TYPE_P(classReflection) == IS_OBJECT)) { + out = true; + return true; + } + } + zv::Val result = pt_type_call(scope, "isinclass", sizeof("isinclass") - 1, 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; +} + +/* return $this->context->getClassReflection(); */ +zv::Val pt_scope_get_class_reflection(zend_object *scope) +{ + zval *classReflection = scopeClassReflectionSlot(scope); + if (classReflection != NULL) { + if (Z_TYPE_P(classReflection) == IS_NULL) return zv::Val::null(); + if (EXPECTED(Z_TYPE_P(classReflection) == IS_OBJECT)) return zv::Val::copyOf(zv::Ref(classReflection)); + } + return pt_type_call(scope, "getclassreflection", sizeof("getclassreflection") - 1, 0, NULL); +} + +/* }}} */ diff --git a/turbo-ext/src/ScopeOps.cpp b/turbo-ext/src/ScopeOps.cpp index 65e0380da82..474e870e1ef 100644 --- a/turbo-ext/src/ScopeOps.cpp +++ b/turbo-ext/src/ScopeOps.cpp @@ -2014,3 +2014,82 @@ void pt_register_scope_ops() } /* }}} */ + +/* {{{ direct entries for the native MutatingScope (MutatingScope.cpp): the + * bodies the twin reaches through ScopeOps::hasVariableType() / + * ScopeOps::hasExpressionType() static calls, without the method-call ABI */ + +zv::Val pt_scope_ops_has_variable_type(zval *scope, zend_string *variableName) +{ + return ScopeOps::hasVariableType(scope, variableName); +} + +zv::Val pt_scope_ops_has_expression_type(zval *scope, zend_object *node, zval *exprPrinter) +{ + return ScopeOps::hasExpressionType(scope, node, exprPrinter); +} + +zv::Val pt_scope_ops_get_type_from_cache(zval *scope, zend_object *node, zend_string **keyOut) +{ + return ScopeOps::getTypeFromCache(scope, node, keyOut); +} + +zv::Val pt_scope_ops_expression_type_by_key(zval *scope, zend_object *node, zend_string *exprString) +{ + return ScopeOps::expressionTypeByKey(scope, node, exprString); +} + +zv::Val pt_scope_ops_scope_with(zval *scope, HashTable *expressionTypes, HashTable *nativeExpressionTypes, HashTable *conditionalExpressions, HashTable *currentlyAssignedExpressions, HashTable *currentlyAllowedUndefinedExpressions, HashTable *inFunctionCallsStack, bool inFirstLevelStatement, bool afterExtractCall) +{ + return ScopeOps::scopeWith(scope, expressionTypes, nativeExpressionTypes, conditionalExpressions, currentlyAssignedExpressions, currentlyAllowedUndefinedExpressions, inFunctionCallsStack, inFirstLevelStatement, afterExtractCall); +} + +zv::Val pt_scope_ops_invalidate_expression_entries(zval *scope, zval *exprPrinter, zend_string *exprStringToInvalidate, zval *expressionToInvalidate, bool requireMoreCharacters, zval *invalidatingClass, HashTable *expressionTypes, HashTable *nativeExpressionTypes, HashTable *conditionalExpressions, bool keepPropertyFetches) +{ + pt_init_strs(); + return ScopeOps::invalidateExpressionEntries(scope, exprPrinter, exprStringToInvalidate, expressionToInvalidate, requireMoreCharacters, invalidatingClass, zv::TableRef(expressionTypes), zv::TableRef(nativeExpressionTypes), zv::TableRef(conditionalExpressions), keepPropertyFetches); +} + +zv::Val pt_scope_ops_invalidate_methods_on_expression(zval *exprPrinter, zend_string *exprStringToInvalidate, HashTable *expressionTypes, HashTable *nativeExpressionTypes) +{ + pt_init_strs(); + return ScopeOps::invalidateMethodsOnExpression(exprPrinter, exprStringToInvalidate, zv::TableRef(expressionTypes), zv::TableRef(nativeExpressionTypes)); +} + +zv::Val pt_scope_ops_intertwined_ref_root_variable_name(zend_object *expr) +{ + return ScopeOps::getIntertwinedRefRootVariableName(expr); +} + +zv::Val pt_scope_ops_match_conditional_expressions(HashTable *conditionalExpressions, HashTable *specifiedExpressions) +{ + return ScopeOps::matchConditionalExpressions(zv::TableRef(conditionalExpressions), zv::TableRef(specifiedExpressions)); +} + +zv::Val pt_scope_ops_merge_variable_holders(HashTable *ourVariableTypeHolders, HashTable *theirVariableTypeHolders, HashTable *differingKeys) +{ + return ScopeOps::mergeVariableHolders(zv::TableRef(ourVariableTypeHolders), zv::TableRef(theirVariableTypeHolders), differingKeys); +} + +zv::Val pt_scope_ops_finish_merge(HashTable *mergedExpressionTypes, HashTable *ourExpressionTypes, HashTable *theirExpressionTypes, HashTable *ourNativeExpressionTypes, HashTable *theirNativeExpressionTypes) +{ + return ScopeOps::finishMerge(zv::TableRef(mergedExpressionTypes), zv::TableRef(ourExpressionTypes), zv::TableRef(theirExpressionTypes), zv::TableRef(ourNativeExpressionTypes), zv::TableRef(theirNativeExpressionTypes)); +} + +zv::Val pt_scope_ops_intersect_conditional_expressions(HashTable *ourConditionalExpressions, HashTable *theirConditionalExpressions) +{ + return ScopeOps::intersectConditionalExpressions(zv::TableRef(ourConditionalExpressions), zv::TableRef(theirConditionalExpressions)); +} + +zv::Val pt_scope_ops_create_conditional_expressions(HashTable *conditionalExpressions, HashTable *ourExpressionTypes, HashTable *theirExpressionTypes, HashTable *mergedExpressionTypes, HashTable *differingKeys) +{ + return ScopeOps::createConditionalExpressions(zv::TableRef(conditionalExpressions), zv::TableRef(ourExpressionTypes), zv::TableRef(theirExpressionTypes), zv::TableRef(mergedExpressionTypes), zv::TableRef(differingKeys)); +} + +bool pt_scope_ops_should_invalidate_expression(zval *scope, zval *exprPrinter, zend_string *exprStringToInvalidate, zval *exprToInvalidate, zend_object *expr, zend_string *exprString, bool requireMoreCharacters, zval *invalidatingClass, bool keepPropertyFetches, bool *failed) +{ + pt_init_strs(); + return ScopeOps::shouldInvalidateExpression(scope, exprPrinter, exprStringToInvalidate, exprToInvalidate, expr, exprString, requireMoreCharacters, invalidatingClass, keepPropertyFetches, failed); +} + +/* }}} */ diff --git a/turbo-ext/src/Shadow.cpp b/turbo-ext/src/Shadow.cpp index 9cd4f7e4e5c..03ff5471482 100644 --- a/turbo-ext/src/Shadow.cpp +++ b/turbo-ext/src/Shadow.cpp @@ -213,6 +213,9 @@ static bool pt_shadow_materialize(reg::ShadowPlan &plan, HashTable *twinFiles, z return false; } for (reg::ShadowPlan &plan : pt_shadow_plans()) { + /* an incomplete port is declared only next to its twin, under the + * prefix, for the differential tests (reg::Class::shadowDifferentialOnly()) */ + if (plan.differentialOnly && prefix == NULL) continue; if (!pt_shadow_materialize(plan, twinFiles, prefix)) return false; } pt_shadow_active = true; diff --git a/turbo-ext/src/StaticTypeFactory.cpp b/turbo-ext/src/StaticTypeFactory.cpp index 9a87cd157e1..3ead302d994 100644 --- a/turbo-ext/src/StaticTypeFactory.cpp +++ b/turbo-ext/src/StaticTypeFactory.cpp @@ -164,6 +164,26 @@ zv::Val pt_static_type_factory_truthy() return StaticTypeFactory::truthy(); } +zv::Val pt_static_type_factory_argc() +{ + return StaticTypeFactory::argc(); +} + +zv::Val pt_static_type_factory_argv() +{ + return StaticTypeFactory::argv(); +} + +zv::Val pt_static_type_factory_general_offset_accessible() +{ + return StaticTypeFactory::generalOffsetAccessibleType(); +} + +zv::Val pt_static_type_factory_int_offset_accessible() +{ + return StaticTypeFactory::intOffsetAccessibleType(); +} + void pt_static_type_factory_rinit() { ZVAL_UNDEF(&pt_stf_falsey); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index 6dbbcf1465a..80febe933e8 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -1247,6 +1247,13 @@ zend_always_inline zv::Val pt_this_call(zend_object *self, bool exact, const cha return pt_type_call(self, lcname, len, argc, argv); } +template +[[nodiscard]] zend_always_inline bool pt_this_call_bool(zend_object *self, bool exact, const char *lcname, size_t len, zif_handler handler, uint32_t argc, zval *argv, bool &out, Direct direct) +{ + if (EXPECTED(exact || pt_type_method_is(self, lcname, len, handler))) return direct(out); + return pt_type_call_bool(self, lcname, len, argc, argv, out); +} + template [[nodiscard]] zend_always_inline zend_long pt_this_call_trinary(zend_object *self, bool exact, const char *lcname, size_t len, zif_handler handler, uint32_t argc, zval *argv, Direct direct) { @@ -1395,4 +1402,9 @@ zv::Val pt_prototype_resolved_property(phpstanturbo::PrototypeKind kind, const p /* }}} */ +/* TypeUtils::resolveLateResolvableTypes($type) with the default + * $resolveUnresolvableTypes = true — the shadowing class's body + * (TypeUtils.cpp); UNDEF = pending exception */ +zv::Val pt_type_utils_resolve_late_resolvable_types(zval *type); + #endif /* PHPSTANTURBO_TYPETRAITS_H */ diff --git a/turbo-ext/src/TypeUtils.cpp b/turbo-ext/src/TypeUtils.cpp index ff14525336f..8f21ff05f9a 100644 --- a/turbo-ext/src/TypeUtils.cpp +++ b/turbo-ext/src/TypeUtils.cpp @@ -451,6 +451,11 @@ zv::Val pt_type_utils_get_integer_ranges(zval *type) return TypeUtils::getIntegerRanges(type); } +zv::Val pt_type_utils_resolve_late_resolvable_types(zval *type) +{ + return TypeUtils::resolveLateResolvableTypes(type, true); +} + /* {{{ engine ABI glue: parameter parsing + registration */ /* one handler per Type-taking static returning through fn */ diff --git a/turbo-ext/src/TypehintHelper.cpp b/turbo-ext/src/TypehintHelper.cpp index e868b8c54dd..32262ada08a 100644 --- a/turbo-ext/src/TypehintHelper.cpp +++ b/turbo-ext/src/TypehintHelper.cpp @@ -394,6 +394,14 @@ class TypehintHelper using phpstanturbo::TypehintHelper; +/* TypehintHelper::decideTypeFromReflection($reflectionType, $phpDocType, + * $selfClass, $isVariadic) for native callers (every argument borrowed, + * NULL for a null / the default); UNDEF = pending exception */ +zv::Val pt_typehint_helper_decide_type_from_reflection(zval *reflectionType, zval *phpDocType, zval *selfClass, bool isVariadic) +{ + return phpstanturbo::TypehintHelper::decideTypeFromReflection(reflectionType, phpDocType, selfClass, isVariadic); +} + /* {{{ engine ABI glue: parameter parsing + registration */ void pt_register_typehint_helper() diff --git a/turbo-ext/src/VariableFlow.cpp b/turbo-ext/src/VariableFlow.cpp new file mode 100644 index 00000000000..283acd5e4fb --- /dev/null +++ b/turbo-ext/src/VariableFlow.cpp @@ -0,0 +1,760 @@ +/* + * PHPStanTurbo\VariableFlow — native implementation of + * PHPStan\Analyser\VariableFlow. + * + * Declared as PHPStan\Analyser\VariableFlow itself at activation (abstract, + * like the twin): the four PHP flow classes (VariableAccessFlow, + * VariableSequenceFlow, VariableControlFlow, VariableInputFlow) extend it, + * call its protected constructor for the `public readonly string $kind` + * slot, and are what the static factories here instantiate — through the + * class map and their real constructors, with every optional constructor + * parameter passed at its default, so the objects are the ones the twin's + * `new` expressions create. + * + * The VariableFlow handle class below mirrors the twin method for method, + * in the same order (`switch` and `exit` carry a trailing underscore + * natively). + */ + +#include "support.h" +#include "generated/VariableFlow.h" + +namespace sigs = ptdecl::VariableFlow::sig; +#include "zv.h" +#include "TypeTraits.h" + +#include + +zend_class_entry *pt_ce_variable_flow; + +namespace { + +/* the twin's kind constants, as permanent interned strings (module startup) */ +enum pt_vf_kind +{ + PT_VF_SEQUENCE, + PT_VF_CHOICE, + PT_VF_LOOP, + PT_VF_TRY_CATCH, + PT_VF_SWITCH, + PT_VF_READ, + PT_VF_WRITE, + PT_VF_DEFINE, + PT_VF_DISCARD, + PT_VF_ESCAPE, + PT_VF_MENTION, + PT_VF_READ_ALL, + PT_VF_MENTION_ALL, + PT_VF_OPAQUE, + PT_VF_DEAD, + PT_VF_RETURN, + PT_VF_BREAK, + PT_VF_CONTINUE, + PT_VF_THROW, + PT_VF_STOP, + PT_VF_ARROW, + PT_VF_LOOP_STATEMENT, + PT_VF_KIND_COUNT, +}; + +const struct { const char *constant; const char *value; } pt_vf_kinds[PT_VF_KIND_COUNT] = { + { "SEQUENCE", "sequence" }, + { "CHOICE", "choice" }, + { "LOOP", "loop" }, + { "TRY_CATCH", "try" }, + { "SWITCH", "switch" }, + { "READ", "read" }, + { "WRITE", "write" }, + { "DEFINE", "define" }, + { "DISCARD", "discard" }, + { "ESCAPE", "escape" }, + { "MENTION", "mention" }, + { "READ_ALL", "readAll" }, + { "MENTION_ALL", "mentionAll" }, + { "OPAQUE", "opaque" }, + { "DEAD", "dead" }, + { "RETURN", "return" }, + { "BREAK", "break" }, + { "CONTINUE", "continue" }, + { "THROW", "throw" }, + { "STOP", "stop" }, + { "ARROW", "arrow" }, + { "LOOP_STATEMENT", "loopStatement" }, +}; + +zend_string *pt_vf_kind_strings[PT_VF_KIND_COUNT]; + +/* a PHP null for the flow arguments the glue passes as NULL */ +zval pt_vf_null; + +zval *flowOrNull(zval *flow) +{ + return flow != NULL ? flow : &pt_vf_null; +} + +/* the VariableWrite slot cache (pt_variable_write_slots_of); rinit forgets it */ +pt_variable_write_slots pt_vf_write_slots = { NULL, 0, 0, 0, 0, 0, 0, 0, 0 }; + +/* $write->getVariableName() (an owned string in *name) and + * $write->getParentId() === null; false = pending exception */ +[[nodiscard]] bool variableWriteInfo(zval *write, zv::Val &name, bool &parentIdIsNull) +{ + bool error; + const pt_variable_write_slots *slots = pt_variable_write_slots_of(Z_OBJ_P(write), error); + if (slots != NULL) { + name = zv::Val::copyOf(zv::ObjRef(write).propAtOffset(slots->variableName)); + parentIdIsNull = Z_TYPE_P(OBJ_PROP(Z_OBJ_P(write), slots->parentId)) == IS_NULL; + return true; + } + if (UNEXPECTED(error)) return false; + zv::Val parentId = pt_type_call(Z_OBJ_P(write), PT_LC("getparentid"), 0, NULL); + if (UNEXPECTED(parentId.isUndef())) return false; + parentIdIsNull = Z_TYPE_P(parentId.raw()) == IS_NULL; + name = pt_type_call(Z_OBJ_P(write), PT_LC("getvariablename"), 0, NULL); + return !name.isUndef(); +} + +/* new VariableAccessFlow($kind, $name, $write, $type, $targetId, $container, + * $offset) — NULL stands for a null argument */ +zv::Val newAccessFlow(pt_vf_kind kind, zval *name, zval *write, zval *type, zval *targetId, bool container, zval *offset) +{ + zval argv[7]; + ZVAL_STR(&argv[0], pt_vf_kind_strings[kind]); + ZVAL_COPY_VALUE(&argv[1], name); + if (write != NULL) { + ZVAL_COPY_VALUE(&argv[2], write); + } else { + ZVAL_NULL(&argv[2]); + } + if (type != NULL) { + ZVAL_COPY_VALUE(&argv[3], type); + } else { + ZVAL_NULL(&argv[3]); + } + if (targetId != NULL) { + ZVAL_COPY_VALUE(&argv[4], targetId); + } else { + ZVAL_NULL(&argv[4]); + } + ZVAL_BOOL(&argv[5], container); + if (offset != NULL) { + ZVAL_COPY_VALUE(&argv[6], offset); + } else { + ZVAL_NULL(&argv[6]); + } + return pt_type_new(PT_CLASS_VARIABLE_ACCESS_FLOW, 7, argv); +} + +/* new VariableSequenceFlow($kind, $children) */ +zv::Val newSequenceFlow(pt_vf_kind kind, zval *children) +{ + zv::Args argv{pt_vf_kind_strings[kind], children}; + return pt_type_new(PT_CLASS_VARIABLE_SEQUENCE_FLOW, 2, argv); +} + +/* the optional constructor parameters of VariableControlFlow, NULL / the + * twin's defaults where a factory leaves them out */ +struct ControlFlowArgs +{ + zval *children = NULL; + zend_string *name = NULL; + zval *type = NULL; + zend_long level = 1; + bool atLeastOnce = false; + bool canExit = true; + zval *catches = NULL; + zval *arrow = NULL; + zval *cases = NULL; + bool canRepeat = true; + bool canContainAnyThrowable = false; + zval *stmt = NULL; + zval *bindings = NULL; + zval *ownWrites = NULL; +}; + +void controlFlowArg(zval *slot, zval *value) +{ + if (value != NULL) { + ZVAL_COPY_VALUE(slot, value); + } else { + ZVAL_NULL(slot); + } +} + +void controlFlowArrayArg(zval *slot, zval *value) +{ + if (value != NULL) { + ZVAL_COPY_VALUE(slot, value); + } else { + ZVAL_EMPTY_ARRAY(slot); + } +} + +/* new VariableControlFlow($kind, ...) with every parameter positional; + * $kind is the constant's value (all() and exit() take it as a parameter) */ +zv::Val newControlFlow(zend_string *kind, const ControlFlowArgs &a) +{ + zval argv[15]; + ZVAL_STR(&argv[0], kind); + controlFlowArrayArg(&argv[1], a.children); + if (a.name != NULL) { + ZVAL_STR(&argv[2], a.name); + } else { + ZVAL_NULL(&argv[2]); + } + controlFlowArg(&argv[3], a.type); + ZVAL_LONG(&argv[4], a.level); + ZVAL_BOOL(&argv[5], a.atLeastOnce); + ZVAL_BOOL(&argv[6], a.canExit); + controlFlowArrayArg(&argv[7], a.catches); + controlFlowArg(&argv[8], a.arrow); + controlFlowArrayArg(&argv[9], a.cases); + ZVAL_BOOL(&argv[10], a.canRepeat); + ZVAL_BOOL(&argv[11], a.canContainAnyThrowable); + controlFlowArg(&argv[12], a.stmt); + controlFlowArrayArg(&argv[13], a.bindings); + controlFlowArrayArg(&argv[14], a.ownWrites); + return pt_type_new(PT_CLASS_VARIABLE_CONTROL_FLOW, 15, argv); +} + +zv::Val newControlFlow(pt_vf_kind kind, const ControlFlowArgs &a) +{ + return newControlFlow(pt_vf_kind_strings[kind], a); +} + +/* [$a, $b, ...] of borrowed, possibly null flows */ +zv::Val flowList(uint32_t count, zval *const *flows) +{ + zv::Arr list = zv::Arr::create(count); + for (uint32_t i = 0; i < count; i++) { + if (flows[i] != NULL) { + list.push(zv::Ref(flows[i])); + } else { + list.push(zv::Val::null()); + } + } + return zv::Val(std::move(list)); +} + +} // namespace + +const pt_variable_write_slots *pt_variable_write_slots_of(zend_object *object, bool &error) +{ + error = false; + if (UNEXPECTED(object->ce != pt_vf_write_slots.ce)) { + zend_class_entry *ce = pt_class_loaded(PT_CLASS_VARIABLE_WRITE); + if (ce == NULL) { + error = EG(exception) != NULL; + return NULL; + } + if (object->ce != ce) return NULL; + int32_t offsets[8]; + static const char *const names[8] = { "variableName", "node", "id", "kind", "offsetWrite", "offset", "parentId", "replacesOffset" }; + for (int i = 0; i < 8; i++) { + offsets[i] = pt_instance_prop_offset(ce, names[i], strlen(names[i])); + if (offsets[i] < 0) return NULL; + } + pt_vf_write_slots.ce = ce; + pt_vf_write_slots.variableName = (uint32_t) offsets[0]; + pt_vf_write_slots.node = (uint32_t) offsets[1]; + pt_vf_write_slots.id = (uint32_t) offsets[2]; + pt_vf_write_slots.kind = (uint32_t) offsets[3]; + pt_vf_write_slots.offsetWrite = (uint32_t) offsets[4]; + pt_vf_write_slots.offset = (uint32_t) offsets[5]; + pt_vf_write_slots.parentId = (uint32_t) offsets[6]; + pt_vf_write_slots.replacesOffset = (uint32_t) offsets[7]; + } + const pt_variable_write_slots &slots = pt_vf_write_slots; + if (Z_TYPE_P(OBJ_PROP(object, slots.variableName)) != IS_STRING + || Z_TYPE_P(OBJ_PROP(object, slots.node)) != IS_OBJECT + || Z_TYPE_P(OBJ_PROP(object, slots.id)) != IS_LONG + || Z_TYPE_P(OBJ_PROP(object, slots.kind)) != IS_LONG + || Z_TYPE_P(OBJ_PROP(object, slots.offsetWrite)) == IS_UNDEF + || Z_TYPE_P(OBJ_PROP(object, slots.offset)) == IS_UNDEF + || Z_TYPE_P(OBJ_PROP(object, slots.parentId)) == IS_UNDEF + || Z_TYPE_P(OBJ_PROP(object, slots.replacesOffset)) == IS_UNDEF) { + return NULL; + } + return &slots; +} + +namespace phpstanturbo { + +/* + * Mirrors PHPStan\Analyser\VariableFlow. Flow arguments are borrowed zvals + * (NULL or IS_NULL for a null flow); methods return the flow, PHP null where + * the twin returns null, UNDEF for a pending exception. + */ +class VariableFlow +{ +public: + /* Mirrors sequence(?self ...$flows). */ + static zv::Val sequence(uint32_t argc, zval *argv) + { + uint32_t count = 0; + zval *single = NULL; + for (uint32_t i = 0; i < argc; i++) { + if (Z_TYPE(argv[i]) == IS_NULL) continue; + count++; + single = &argv[i]; + } + if (count == 0) return zv::Val::null(); + if (count == 1) return zv::Val::copyOf(zv::Ref(single)); + + zv::Arr nonEmpty = zv::Arr::create(count); + for (uint32_t i = 0; i < argc; i++) { + if (Z_TYPE(argv[i]) == IS_NULL) continue; + nonEmpty.push(zv::Ref(&argv[i])); + } + return newSequenceFlow(PT_VF_SEQUENCE, nonEmpty.raw()); + } + + /* Mirrors choice(?self ...$branches). */ + static zv::Val choice(uint32_t argc, zval *argv) + { + if (argc == 0) return zv::Val::null(); + if (argc == 1 || (argc == 2 && zend_is_identical(&argv[0], &argv[1]))) return zv::Val::copyOf(zv::Ref(&argv[0])); + + zv::Arr branches = zv::Arr::create(argc); + for (uint32_t i = 0; i < argc; i++) { + branches.push(zv::Ref(&argv[i])); + } + return newSequenceFlow(PT_VF_CHOICE, branches.raw()); + } + + /* Mirrors arrow(). */ + static zv::Val arrow(zval *arrow, zval *body, zval *outputs) + { + zval *const flows[2] = { body, outputs }; + zv::Val children = flowList(2, flows); + ControlFlowArgs a; + a.children = children.raw(); + a.arrow = arrow; + return newControlFlow(PT_VF_ARROW, a); + } + + /* Mirrors read(); $targetId and $offset NULL for null. */ + static zv::Val read(zval *name, zval *targetId, bool container, zval *offset) + { + if (zend_string_equals_literal(Z_STR_P(name), "this") || pt_is_superglobal_name(Z_STR_P(name))) return zv::Val::null(); + + return newAccessFlow(PT_VF_READ, name, NULL, NULL, targetId, container, offset); + } + + /* Mirrors conditional(); $truthy is IS_TRUE, IS_FALSE or IS_NULL. */ + static zv::Val conditional(zval *condition, zval *ifFlow, zval *elseFlow, zval *truthy) + { + condition = flowOrNull(condition); + ifFlow = flowOrNull(ifFlow); + elseFlow = flowOrNull(elseFlow); + zv::Val branch; + if (Z_TYPE_P(truthy) == IS_TRUE) { + zv::Val deadElse = dead(elseFlow); + if (UNEXPECTED(deadElse.isUndef())) return zv::Val(); + zv::Args argv{ifFlow, deadElse.raw()}; + branch = sequence(2, argv); + } else if (Z_TYPE_P(truthy) == IS_FALSE) { + zv::Val deadIf = dead(ifFlow); + if (UNEXPECTED(deadIf.isUndef())) return zv::Val(); + zv::Args argv{deadIf.raw(), elseFlow}; + branch = sequence(2, argv); + } else { + zv::Args argv{ifFlow, elseFlow}; + branch = choice(2, argv); + } + if (UNEXPECTED(branch.isUndef())) return zv::Val(); + zv::Args argv{condition, branch.raw()}; + return sequence(2, argv); + } + + /* Mirrors switch(). */ + static zv::Val switch_(zval *condition, zval *cases, bool exhaustive) + { + zval *const flows[1] = { condition }; + zv::Val children = flowList(1, flows); + ControlFlowArgs a; + a.children = children.raw(); + a.canExit = !exhaustive; + a.cases = cases; + return newControlFlow(PT_VF_SWITCH, a); + } + + /* Mirrors write(); $redundantType NULL for null. */ + static zv::Val write(zval *write, zval *redundantType) + { + zv::Val name; + bool parentIdIsNull; + if (UNEXPECTED(!variableWriteInfo(write, name, parentIdIsNull))) return zv::Val(); + return newAccessFlow(parentIdIsNull ? PT_VF_WRITE : PT_VF_DEFINE, name.raw(), write, redundantType, NULL, false, NULL); + } + + /* Mirrors discard(). */ + static zv::Val discard(zval *write) + { + zv::Val name; + bool parentIdIsNull; + if (UNEXPECTED(!variableWriteInfo(write, name, parentIdIsNull))) return zv::Val(); + return newAccessFlow(PT_VF_DISCARD, name.raw(), write, NULL, NULL, false, NULL); + } + + /* Mirrors inputs(); $targetId NULL for null. */ + static zv::Val inputs(zend_long writeId, zval *targetId) + { + zval argv[2]; + ZVAL_LONG(&argv[0], writeId); + if (targetId != NULL) { + ZVAL_COPY_VALUE(&argv[1], targetId); + } else { + ZVAL_NULL(&argv[1]); + } + return pt_type_new(PT_CLASS_VARIABLE_INPUT_FLOW, 2, argv); + } + + /* Mirrors escape(). */ + static zv::Val escape(zval *name) + { + return newAccessFlow(PT_VF_ESCAPE, name, NULL, NULL, NULL, false, NULL); + } + + /* Mirrors mention(). */ + static zv::Val mention(zval *name) + { + return newAccessFlow(PT_VF_MENTION, name, NULL, NULL, NULL, false, NULL); + } + + /* Mirrors all(). */ + static zv::Val all(zend_string *kind) + { + return newControlFlow(kind, ControlFlowArgs()); + } + + /* Mirrors exit(); $name NULL for null. */ + static zv::Val exit_(zend_string *kind, zend_long level, zend_string *name) + { + ControlFlowArgs a; + a.name = name; + a.level = level; + return newControlFlow(kind, a); + } + + /* Mirrors throwing(). */ + static zv::Val throwing(zval *type, bool canContinue, bool canContainAnyThrowable) + { + ControlFlowArgs a; + a.type = type; + a.canExit = canContinue; + a.canContainAnyThrowable = canContainAnyThrowable; + return newControlFlow(PT_VF_THROW, a); + } + + /* Mirrors dead(). */ + static zv::Val dead(zval *flow) + { + if (flow == NULL || Z_TYPE_P(flow) == IS_NULL) return zv::Val::null(); + zval *const flows[1] = { flow }; + zv::Val children = flowList(1, flows); + ControlFlowArgs a; + a.children = children.raw(); + return newControlFlow(PT_VF_DEAD, a); + } + + /* Mirrors loop(). */ + static zv::Val loop(zval *condition, zval *body, zval *update, bool atLeastOnce, bool canExit, bool canRepeat) + { + zval *const flows[3] = { condition, body, update }; + zv::Val children = flowList(3, flows); + ControlFlowArgs a; + a.children = children.raw(); + a.atLeastOnce = atLeastOnce; + a.canExit = canExit; + a.canRepeat = canRepeat; + return newControlFlow(PT_VF_LOOP, a); + } + + /* Mirrors loopStatement(). */ + static zv::Val loopStatement(zval *stmt, zval *flow, zval *bindings, zval *ownWrites) + { + if (zend_hash_num_elements(Z_ARRVAL_P(bindings)) == 0) return zv::Val::copyOf(zv::Ref(flowOrNull(flow))); + + zval *const flows[1] = { flow }; + zv::Val children = flowList(1, flows); + ControlFlowArgs a; + a.children = children.raw(); + a.stmt = stmt; + a.bindings = bindings; + a.ownWrites = ownWrites; + return newControlFlow(PT_VF_LOOP_STATEMENT, a); + } + + /* Mirrors tryCatch(). */ + static zv::Val tryCatch(zval *body, zval *catches, zval *finally) + { + zval *const flows[2] = { body, finally }; + zv::Val children = flowList(2, flows); + ControlFlowArgs a; + a.children = children.raw(); + a.catches = catches; + return newControlFlow(PT_VF_TRY_CATCH, a); + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::VariableFlow; + +void pt_variable_flow_rinit() +{ + pt_vf_write_slots.ce = NULL; +} + +zv::Val pt_variable_flow_sequence(uint32_t argc, zval *argv) +{ + return VariableFlow::sequence(argc, argv); +} + +zv::Val pt_variable_flow_sequence_list(HashTable *flows) +{ + /* a hole-free packed list is already the contiguous argument vector + * sequence() walks; anything else is copied into one */ + uint32_t count = zend_hash_num_elements(flows); + if (HT_IS_PACKED(flows) && flows->nNumUsed == count) return VariableFlow::sequence(count, flows->arPacked); + zval *argv = (zval *) safe_emalloc(count, sizeof(zval), 0); + uint32_t i = 0; + for (auto entry : zv::TableRef(flows)) { + ZVAL_COPY_VALUE(&argv[i++], entry.value().deref().raw()); + } + zv::Val result = VariableFlow::sequence(i, argv); + efree(argv); + return result; +} + +zv::Val pt_variable_flow_read(zend_string *name, zval *targetId, bool container, zval *offset) +{ + zval nameValue; + ZVAL_STR(&nameValue, name); + return VariableFlow::read(&nameValue, targetId, container, offset); +} + +zv::Val pt_variable_flow_write(zval *write, zval *redundantType) +{ + return VariableFlow::write(write, redundantType); +} + +zv::Val pt_variable_flow_escape(zend_string *name) +{ + zval nameValue; + ZVAL_STR(&nameValue, name); + return VariableFlow::escape(&nameValue); +} + +zv::Val pt_variable_flow_dead(zval *flow) +{ + return VariableFlow::dead(flow); +} + +zv::Val pt_variable_flow_throwing(zval *type, bool canContinue, bool canContainAnyThrowable) +{ + return VariableFlow::throwing(type, canContinue, canContainAnyThrowable); +} + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define PT_VF_RETURN(expr) \ + do { \ + zv::Val pt_vf_result = (expr); \ + if (UNEXPECTED(pt_vf_result.isUndef())) { \ + RETURN_THROWS(); \ + } \ + pt_vf_result.intoReturnValue(return_value); \ + } while (0) + +namespace { + +} // namespace + +void pt_register_variable_flow() +{ + ZVAL_NULL(&pt_vf_null); + for (int i = 0; i < PT_VF_KIND_COUNT; i++) { + pt_vf_kind_strings[i] = zend_string_init_interned(pt_vf_kinds[i].value, strlen(pt_vf_kinds[i].value), 1); + } + + reg::Class cls("PHPStan\\Analyser\\VariableFlow"); + ptdecl::VariableFlow::declareClass(cls); + for (int i = 0; i < PT_VF_KIND_COUNT; i++) { + cls.publicClassConstantString(pt_vf_kinds[i].constant, pt_vf_kinds[i].value); + } + /* slot 0: the promoted `public readonly string $kind` */ + ptdecl::VariableFlow::declareProperties(cls); + + cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *kind; + if (!zp::parse(execute_data, kind)) RETURN_THROWS(); + zval *slot = OBJ_PROP_NUM(Z_OBJ_P(ZEND_THIS), 0); + if (UNEXPECTED(Z_TYPE_P(slot) != IS_UNDEF)) { + zend_throw_error(NULL, "Cannot modify readonly property %s::$kind", ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)); + RETURN_THROWS(); + } + ZVAL_STR_COPY(slot, kind); + }); + + cls.method(sigs::sequence, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *flows; + uint32_t count; + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('*', flows, count) + ZEND_PARSE_PARAMETERS_END(); + PT_VF_RETURN(VariableFlow::sequence(count, flows)); + }); + + cls.method(sigs::choice, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *branches; + uint32_t count; + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('*', branches, count) + ZEND_PARSE_PARAMETERS_END(); + PT_VF_RETURN(VariableFlow::choice(count, branches)); + }); + + cls.method(sigs::arrow, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *arrow, *body, *outputs; + if (!zp::parse(execute_data, arrow, body, outputs)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::arrow(arrow, body, outputs)); + }); + + cls.method(sigs::read, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *name; + zval *offset = NULL; + zend_long targetId = 0; + bool targetIdIsNull = true; + bool container = false; + ZEND_PARSE_PARAMETERS_START(1, 4) + Z_PARAM_STR(name) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(targetId, targetIdIsNull) + Z_PARAM_BOOL(container) + Z_PARAM_ZVAL(offset) + ZEND_PARSE_PARAMETERS_END(); + zval nameValue, targetIdValue; + ZVAL_STR(&nameValue, name); + ZVAL_LONG(&targetIdValue, targetId); + PT_VF_RETURN(VariableFlow::read(&nameValue, targetIdIsNull ? NULL : &targetIdValue, container, offset)); + }); + + cls.method(sigs::conditional, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *condition, *ifFlow, *elseFlow; + bool truthy = false; + bool truthyIsNull = true; + ZEND_PARSE_PARAMETERS_START(4, 4) + Z_PARAM_OBJECT_OR_NULL(condition) + Z_PARAM_OBJECT_OR_NULL(ifFlow) + Z_PARAM_OBJECT_OR_NULL(elseFlow) + Z_PARAM_BOOL_OR_NULL(truthy, truthyIsNull) + ZEND_PARSE_PARAMETERS_END(); + zval truthyValue; + if (truthyIsNull) { + ZVAL_NULL(&truthyValue); + } else { + ZVAL_BOOL(&truthyValue, truthy); + } + PT_VF_RETURN(VariableFlow::conditional(condition, ifFlow, elseFlow, &truthyValue)); + }); + + cls.method(sigs::switch_, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *condition, *cases; + bool exhaustive; + if (!zp::parse(execute_data, condition, cases, exhaustive)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::switch_(condition, cases, exhaustive)); + }); + + cls.method(sigs::write, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *write, *redundantType = NULL; + if (!zp::parse>(execute_data, write, redundantType)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::write(write, redundantType)); + }); + + cls.method(sigs::discard, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *write; + if (!zp::parse(execute_data, write)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::discard(write)); + }); + + cls.method(sigs::inputs, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_long writeId, targetId = 0; + bool targetIdIsNull = true; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_LONG(writeId) + Z_PARAM_LONG_OR_NULL(targetId, targetIdIsNull) + ZEND_PARSE_PARAMETERS_END(); + zval targetIdValue; + ZVAL_LONG(&targetIdValue, targetId); + PT_VF_RETURN(VariableFlow::inputs(writeId, targetIdIsNull ? NULL : &targetIdValue)); + }); + + cls.method(sigs::escape, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *name; + if (!zp::parse(execute_data, name)) RETURN_THROWS(); + zval nameValue; + ZVAL_STR(&nameValue, name); + PT_VF_RETURN(VariableFlow::escape(&nameValue)); + }); + + cls.method(sigs::mention, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *name; + if (!zp::parse(execute_data, name)) RETURN_THROWS(); + zval nameValue; + ZVAL_STR(&nameValue, name); + PT_VF_RETURN(VariableFlow::mention(&nameValue)); + }); + + cls.method(sigs::all, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *kind; + if (!zp::parse(execute_data, kind)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::all(kind)); + }); + + cls.method(sigs::exit, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_string *kind, *name = NULL; + zend_long level = 1; + if (!zp::parse, zp::Opt>(execute_data, kind, level, name)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::exit_(kind, level, name)); + }); + + cls.method(sigs::throwing, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type; + bool canContinue, canContainAnyThrowable = false; + if (!zp::parse>(execute_data, type, canContinue, canContainAnyThrowable)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::throwing(type, canContinue, canContainAnyThrowable)); + }); + + cls.method(sigs::dead, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *flow; + if (!zp::parse(execute_data, flow)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::dead(flow)); + }); + + cls.method(sigs::loop, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *condition, *body, *update; + bool atLeastOnce, canExit, canRepeat = true; + if (!zp::parse>(execute_data, condition, body, update, atLeastOnce, canExit, canRepeat)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::loop(condition, body, update, atLeastOnce, canExit, canRepeat)); + }); + + cls.method(sigs::loopStatement, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *stmt, *flow, *bindings, *ownWrites; + if (!zp::parse(execute_data, stmt, flow, bindings, ownWrites)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::loopStatement(stmt, flow, bindings, ownWrites)); + }); + + cls.method(sigs::tryCatch, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *body, *catches, *finally; + if (!zp::parse(execute_data, body, catches, finally)) RETURN_THROWS(); + PT_VF_RETURN(VariableFlow::tryCatch(body, catches, finally)); + }); + + cls.shadow(&pt_ce_variable_flow); +} + +/* }}} */ diff --git a/turbo-ext/src/VariableFlowBuilder.cpp b/turbo-ext/src/VariableFlowBuilder.cpp new file mode 100644 index 00000000000..d52fb36432d --- /dev/null +++ b/turbo-ext/src/VariableFlowBuilder.cpp @@ -0,0 +1,567 @@ +/* + * PHPStanTurbo\VariableFlowBuilder — native implementation of + * PHPStan\Analyser\VariableFlowBuilder. + * + * Composes VariableFlow fragments for assignment targets and call + * arguments: the node structure is read from the PhpParser nodes' + * properties, the flows come from the native VariableFlow factories + * (VariableFlow.cpp), stored results from the native + * ExpressionResultStorage and their flows from the native ExpressionResult + * (the methods for any other class), and the collaborators that stay PHP + * (ArgsResult, MutatingScope, VariableWriteOffset, VariableWrite) are + * called through the engine. + */ + +#include "support.h" +#include "generated/VariableFlowBuilder.h" + +namespace sigs = ptdecl::VariableFlowBuilder::sig; +#include "zv.h" +#include "TypeTraits.h" +#include "TypeOps.h" + +#include + +static zend_class_entry *pt_ce_variable_flow_builder; + +namespace { + +/* the twin's VariableWrite::KIND_LIST_ITEM */ +const zend_long PT_VFB_KIND_LIST_ITEM = 8; + +/* $node->$name, dereferenced; raw() NULL when the class has no such + * property */ +zv::Ref nodeProp(zend_object *node, const char *name, size_t len) +{ + zv::Ref value = zv::ObjRef(node).prop(name, len); + return value.raw() != NULL ? value.deref() : value; +} + +/* $node->getStartFilePos() / getEndFilePos(): the attribute, -1 without */ +zend_long filePos(zend_object *node, zend_string *attribute) +{ + zval *value = pt_node_attribute(node, attribute); + if (value == NULL || Z_TYPE_P(value) == IS_NULL) return -1; + return zval_get_long(value); +} + +/* the class entries every builder method dispatches on */ +struct NodeClasses +{ + zend_class_entry *node; + zend_class_entry *expr; + zend_class_entry *variable; + zend_class_entry *list; + zend_class_entry *array; + zend_class_entry *arrayDimFetch; + zend_class_entry *propertyFetch; + zend_class_entry *nullsafePropertyFetch; + zend_class_entry *staticPropertyFetch; + zend_class_entry *closure; + zend_class_entry *arrowFunction; + zend_class_entry *callLike; + zend_class_entry *accessFlow; + zend_class_entry *sequenceFlow; + + /* false = pending exception (an unresolvable class-map entry) */ + [[nodiscard]] bool resolve() + { + node = pt_class(PT_CLASS_NODE); + expr = pt_class(PT_CLASS_EXPR); + variable = pt_class(PT_CLASS_VARIABLE); + list = pt_class(PT_CLASS_LIST_EXPR); + array = pt_class(PT_CLASS_ARRAY_EXPR); + arrayDimFetch = pt_class(PT_CLASS_ARRAY_DIM_FETCH); + propertyFetch = pt_class(PT_CLASS_PROPERTY_FETCH); + nullsafePropertyFetch = pt_class(PT_CLASS_NULLSAFE_PROPERTY_FETCH); + staticPropertyFetch = pt_class(PT_CLASS_STATIC_PROPERTY_FETCH); + closure = pt_class(PT_CLASS_CLOSURE_EXPR); + arrowFunction = pt_class(PT_CLASS_ARROW_FUNCTION); + callLike = pt_class(PT_CLASS_CALL_LIKE); + accessFlow = pt_class(PT_CLASS_VARIABLE_ACCESS_FLOW); + sequenceFlow = pt_class(PT_CLASS_VARIABLE_SEQUENCE_FLOW); + return node != NULL && expr != NULL && variable != NULL && list != NULL && array != NULL + && arrayDimFetch != NULL && propertyFetch != NULL && nullsafePropertyFetch != NULL + && staticPropertyFetch != NULL && closure != NULL && arrowFunction != NULL && callLike != NULL + && accessFlow != NULL && sequenceFlow != NULL; + } +}; + +bool isA(zv::Ref value, zend_class_entry *ce) +{ + return value.raw() != NULL && value.isObject() && instanceof_function(Z_OBJCE_P(value.raw()), ce); +} + +/* a Variable node's string name (borrowed), NULL for a variable variable */ +zend_string *variableName(zend_object *variable) +{ + zv::Ref name = nodeProp(variable, PT_LC("name")); + return name.raw() != NULL && name.isString() ? name.asString() : NULL; +} + +/* $result !== null ? $result->getVariableFlow() : null for a stored + * ExpressionResult (owned, may be PHP null); UNDEF = pending exception */ +zv::Val resultFlow(zv::Val result) +{ + if (Z_TYPE_P(result.raw()) != IS_OBJECT) return zv::Val::null(); + return pt_expression_result_variable_flow(result.raw()); +} + +/* VariableWriteOffset::fromType($result->getType()); UNDEF = pending + * exception */ +zv::Val writeOffsetOf(zval *result) +{ + zv::Val type = pt_type_call(Z_OBJ_P(result), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + return pt_type_call_static(PT_CLASS_VARIABLE_WRITE_OFFSET, PT_LC("fromtype"), 1, type.raw()); +} + +/* new VariableWrite($variableName, $node, spl_object_id($node), $kind, + * $offsetWrite, $offset, $parentId = null, $replacesOffset) */ +zv::Val newVariableWrite(zend_string *variableName, zval *node, zend_long kind, bool offsetWrite, zval *offset, bool replacesOffset) +{ + zval argv[8]; + ZVAL_STR(&argv[0], variableName); + ZVAL_COPY_VALUE(&argv[1], node); + ZVAL_LONG(&argv[2], (zend_long) Z_OBJ_HANDLE_P(node)); + ZVAL_LONG(&argv[3], kind); + ZVAL_BOOL(&argv[4], offsetWrite); + if (offset != NULL) { + ZVAL_COPY_VALUE(&argv[5], offset); + } else { + ZVAL_NULL(&argv[5]); + } + ZVAL_NULL(&argv[6]); + ZVAL_BOOL(&argv[7], replacesOffset); + return pt_type_new(PT_CLASS_VARIABLE_WRITE, 8, argv); +} + +} // namespace + +namespace phpstanturbo { + +/* + * Mirrors PHPStan\Analyser\VariableFlowBuilder. Methods return the flow (or + * write), PHP null where the twin returns null, UNDEF for a pending + * exception. + */ +class VariableFlowBuilder +{ +public: + /* Mirrors throws(). */ + static zv::Val throws(zval *expr, HashTable *throwPoints) + { + NodeClasses classes; + if (UNEXPECTED(!classes.resolve())) return zv::Val(); + + /* a callback the callee invokes immediately throws through the call - + * its throw points are re-created on the callback argument node; + * the identity set of those arguments, keyed by object handle */ + zv::ScratchTable callbackArguments(0); + if (instanceof_function(Z_OBJCE_P(expr), classes.callLike)) { + bool isFirstClassCallable; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), isFirstClassCallable))) return zv::Val(); + if (!isFirstClassCallable) { + zv::Ref args = nodeProp(Z_OBJ_P(expr), PT_LC("args")); + if (args.raw() != NULL && args.isArray()) { + for (auto entry : zv::TableRef(args.asArrayTable())) { + zv::Ref arg = entry.value().deref(); + if (!arg.isObject()) continue; + zv::Ref value = nodeProp(arg.asObject(), PT_LC("value")); + if (!isA(value, classes.closure) && !isA(value, classes.arrowFunction)) continue; + zval marker; + ZVAL_TRUE(&marker); + zend_hash_index_update(callbackArguments.table(), Z_OBJ_HANDLE_P(value.raw()), &marker); + } + } + } + } + pt_init_strs(); + zend_long exprStart = filePos(Z_OBJ_P(expr), pt_str_start_file_pos); + zend_long exprEnd = filePos(Z_OBJ_P(expr), pt_str_end_file_pos); + zv::Arr throws = zv::Arr::create(zend_hash_num_elements(throwPoints)); + for (auto entry : zv::TableRef(throwPoints)) { + zv::Ref throwPoint = entry.value().deref(); + if (UNEXPECTED(!throwPoint.isObject())) { + zend_type_error("phpstan_turbo: expected InternalThrowPoint, got %s", zend_zval_value_name(throwPoint.raw())); + return zv::Val(); + } + zv::Val node = pt_type_call(throwPoint.asObject(), PT_LC("getnode"), 0, NULL); + if (UNEXPECTED(node.isUndef())) return zv::Val(); + zend_object *throwNode = Z_OBJ_P(node.raw()); + if (throwNode != Z_OBJ_P(expr) + && !zend_hash_index_exists(callbackArguments.table(), throwNode->handle) + && (filePos(throwNode, pt_str_start_file_pos) != exprStart || filePos(throwNode, pt_str_end_file_pos) != exprEnd)) { + continue; + } + + zv::Val type = pt_type_call(throwPoint.asObject(), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + zv::Val canContainAnyThrowable = pt_type_call(throwPoint.asObject(), PT_LC("cancontainanythrowable"), 0, NULL); + if (UNEXPECTED(canContainAnyThrowable.isUndef())) return zv::Val(); + zv::Val flow = pt_variable_flow_throwing(type.raw(), true, Z_TYPE_P(canContainAnyThrowable.raw()) == IS_TRUE); + if (UNEXPECTED(flow.isUndef())) return zv::Val(); + throws.push(std::move(flow)); + } + return pt_variable_flow_sequence_list(throws.table()); + } + + /* Mirrors arguments(). */ + static zv::Val arguments(zval *call, zval *argsResult, zval *storage) + { + zv::Ref args = nodeProp(Z_OBJ_P(call), PT_LC("args")); + if (UNEXPECTED(args.raw() == NULL || !args.isArray())) return zv::Val::null(); + zv::Arr flows = zv::Arr::create(zend_hash_num_elements(args.asArrayTable())); + for (auto entry : zv::TableRef(args.asArrayTable())) { + zv::Ref arg = entry.value().deref(); + if (UNEXPECTED(!arg.isObject())) continue; + zv::Ref value = nodeProp(arg.asObject(), PT_LC("value")); + if (UNEXPECTED(value.raw() == NULL || !value.isObject())) { + zend_type_error("phpstan_turbo: expected an Arg with an Expr value"); + return zv::Val(); + } + zv::Val result = pt_type_call(Z_OBJ_P(argsResult), PT_LC("findargresult"), 1, value.raw()); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + if (Z_TYPE_P(result.raw()) == IS_NULL) { + result = pt_expression_result_storage_find(storage, value.raw()); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + } + zv::Val flow = resultFlow(std::move(result)); + if (UNEXPECTED(flow.isUndef())) return zv::Val(); + flows.push(std::move(flow)); + zv::Ref byRef = nodeProp(arg.asObject(), PT_LC("byRef")); + if (byRef.raw() == NULL || !byRef.isTrue()) { + zv::Val passedByReference = pt_type_call(Z_OBJ_P(argsResult), PT_LC("ispassedbyreference"), 1, value.raw()); + if (UNEXPECTED(passedByReference.isUndef())) return zv::Val(); + if (Z_TYPE_P(passedByReference.raw()) != IS_TRUE) continue; + } + + zv::Val escape = escapeRoot(value.raw()); + if (UNEXPECTED(escape.isUndef())) return zv::Val(); + flows.push(std::move(escape)); + } + return pt_variable_flow_sequence_list(flows.table()); + } + + /* Mirrors child(); $node NULL for null. */ + static zv::Val child(zval *node, zval *storage) + { + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) return zv::Val(); + if (node != NULL && Z_TYPE_P(node) == IS_OBJECT && instanceof_function(Z_OBJCE_P(node), exprCe)) { + zv::Val result = pt_expression_result_storage_find(storage, node); + if (UNEXPECTED(result.isUndef())) return zv::Val(); + return resultFlow(std::move(result)); + } + return zv::Val::null(); + } + + /* Mirrors targetRead(); $targetId NULL for null. */ + static zv::Val targetRead(zval *target, zval *storage, bool read, zval *targetId) + { + NodeClasses classes; + if (UNEXPECTED(!classes.resolve())) return zv::Val(); + zend_object *targetObj = Z_OBJ_P(target); + if (instanceof_function(targetObj->ce, classes.variable)) { + zv::Ref name = nodeProp(targetObj, PT_LC("name")); + if (name.raw() != NULL && name.isString()) return read ? pt_variable_flow_read(name.asString(), targetId, false, NULL) : zv::Val::null(); + return child(name.raw(), storage); + } + if (instanceof_function(targetObj->ce, classes.list) || instanceof_function(targetObj->ce, classes.array)) return zv::Val::null(); + if (instanceof_function(targetObj->ce, classes.arrayDimFetch)) { + zv::Ref var = nodeProp(targetObj, PT_LC("var")); + zv::Ref dim = nodeProp(targetObj, PT_LC("dim")); + zval *dimNode = dim.raw() != NULL && dim.isObject() ? dim.raw() : NULL; + zv::Val dimChild = child(dimNode, storage); + if (UNEXPECTED(dimChild.isUndef())) return zv::Val(); + zv::Val first; + if (isA(var, classes.variable) && variableName(var.asObject()) != NULL) { + zv::Val offset; + if (read && dimNode != NULL) { + zv::Val dimResult = pt_expression_result_storage_find(storage, dimNode); + if (UNEXPECTED(dimResult.isUndef())) return zv::Val(); + if (Z_TYPE_P(dimResult.raw()) == IS_OBJECT) { + offset = writeOffsetOf(dimResult.raw()); + if (UNEXPECTED(offset.isUndef())) return zv::Val(); + } + } + first = pt_variable_flow_read(variableName(var.asObject()), targetId, !read, offset.isUndef() ? NULL : offset.raw()); + } else { + if (UNEXPECTED(var.raw() == NULL || !var.isObject())) return zv::Val::null(); + first = targetRead(var.raw(), storage, true, targetId); + } + if (UNEXPECTED(first.isUndef())) return zv::Val(); + zv::Args argv{first.raw(), dimChild.raw()}; + return pt_variable_flow_sequence(2, argv); + } + if (instanceof_function(targetObj->ce, classes.propertyFetch) || instanceof_function(targetObj->ce, classes.nullsafePropertyFetch)) { + return childSequence(targetObj, PT_LC("var"), PT_LC("name"), storage); + } + if (instanceof_function(targetObj->ce, classes.staticPropertyFetch)) return childSequence(targetObj, PT_LC("class"), PT_LC("name"), storage); + return child(target, storage); + } + + /* Mirrors writes(); $flow NULL for null. */ + static zv::Val writes(zval *flow) + { + zv::Arr writes = zv::Arr::empty(); + if (UNEXPECTED(!collectWrites(flow, writes))) return zv::Val(); + return zv::Val(std::move(writes)); + } + + /* Mirrors targetWrite(); $redundant NULL for null. */ + static zv::Val targetWrite(zval *target, zend_long kind, zval *scope, zval *storage, zval *redundant) + { + NodeClasses classes; + if (UNEXPECTED(!classes.resolve())) return zv::Val(); + zend_object *targetObj = Z_OBJ_P(target); + if (instanceof_function(targetObj->ce, classes.list) || instanceof_function(targetObj->ce, classes.array)) { + zv::Ref items = nodeProp(targetObj, PT_LC("items")); + if (UNEXPECTED(items.raw() == NULL || !items.isArray())) return zv::Val::null(); + zv::Arr writes = zv::Arr::create(zend_hash_num_elements(items.asArrayTable())); + for (auto entry : zv::TableRef(items.asArrayTable())) { + zv::Ref item = entry.value().deref(); + if (!item.isObject()) continue; + zv::Ref key = nodeProp(item.asObject(), PT_LC("key")); + zv::Ref value = nodeProp(item.asObject(), PT_LC("value")); + zv::Ref byRef = nodeProp(item.asObject(), PT_LC("byRef")); + if (UNEXPECTED(value.raw() == NULL || !value.isObject())) { + zend_type_error("phpstan_turbo: expected an ArrayItem with an Expr value"); + return zv::Val(); + } + zv::Val keyChild = child(key.raw() != NULL && key.isObject() ? key.raw() : NULL, storage); + if (UNEXPECTED(keyChild.isUndef())) return zv::Val(); + zv::Val valueRead = targetRead(value.raw(), storage, false, NULL); + if (UNEXPECTED(valueRead.isUndef())) return zv::Val(); + zv::Val valueWrite = targetWrite(value.raw(), PT_VFB_KIND_LIST_ITEM, scope, storage, NULL); + if (UNEXPECTED(valueWrite.isUndef())) return zv::Val(); + zv::Val escape = byRef.raw() != NULL && byRef.isTrue() ? escapeRoot(value.raw()) : zv::Val::null(); + if (UNEXPECTED(escape.isUndef())) return zv::Val(); + zv::Args argv{keyChild.raw(), valueRead.raw(), valueWrite.raw(), escape.raw()}; + zv::Val itemFlow = pt_variable_flow_sequence(4, argv); + if (UNEXPECTED(itemFlow.isUndef())) return zv::Val(); + writes.push(std::move(itemFlow)); + } + return pt_variable_flow_sequence_list(writes.table()); + } + zv::Val write = writeSite(target, kind, scope, storage); + if (UNEXPECTED(write.isUndef())) return zv::Val(); + if (Z_TYPE_P(write.raw()) == IS_NULL) return zv::Val::null(); + return pt_variable_flow_write(write.raw(), redundant); + } + + /* Mirrors writeSite(). */ + static zv::Val writeSite(zval *target, zend_long kind, zval *scope, zval *storage) + { + NodeClasses classes; + if (UNEXPECTED(!classes.resolve())) return zv::Val(); + zend_object *targetObj = Z_OBJ_P(target); + if (instanceof_function(targetObj->ce, classes.variable)) { + zend_string *name = variableName(targetObj); + if (name != NULL) { + if (zend_string_equals_literal(name, "this") || pt_is_superglobal_name(name)) return zv::Val::null(); + return newVariableWrite(name, target, kind, false, NULL, true); + } + } + if (!instanceof_function(targetObj->ce, classes.arrayDimFetch)) return zv::Val::null(); + zend_object *first = targetObj; + for (;;) { + zv::Ref var = nodeProp(first, PT_LC("var")); + if (!isA(var, classes.arrayDimFetch)) break; + first = var.asObject(); + } + zv::Ref root = nodeProp(first, PT_LC("var")); + if (!isA(root, classes.variable)) return zv::Val::null(); + zend_string *rootName = variableName(root.asObject()); + if (rootName == NULL || zend_string_equals_literal(rootName, "this") || pt_is_superglobal_name(rootName)) return zv::Val::null(); + zval rootNameValue; + ZVAL_STR(&rootNameValue, rootName); + zend_long hasVariableType = pt_type_call_trinary(Z_OBJ_P(scope), PT_LC("hasvariabletype"), 1, &rootNameValue); + if (UNEXPECTED(hasVariableType < 0)) return zv::Val(); + if (hasVariableType != PT_TRI_NO) { + zv::Val type = pt_type_call(Z_OBJ_P(scope), PT_LC("getvariabletype"), 1, &rootNameValue); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + zend_long isArray = trinaryOp(type.raw(), PT_OP_IS_ARRAY); + if (UNEXPECTED(isArray < 0)) return zv::Val(); + if (isArray != PT_TRI_YES) { + zend_long isString = trinaryOp(type.raw(), PT_OP_IS_STRING); + if (UNEXPECTED(isString < 0)) return zv::Val(); + if (isString != PT_TRI_YES) return zv::Val::null(); + } + } + zv::Ref dim = nodeProp(first, PT_LC("dim")); + zv::Val offset; + if (dim.raw() != NULL && dim.isObject()) { + zv::Val dimResult = pt_expression_result_storage_find(storage, dim.raw()); + if (UNEXPECTED(dimResult.isUndef())) return zv::Val(); + if (Z_TYPE_P(dimResult.raw()) == IS_OBJECT) { + offset = writeOffsetOf(dimResult.raw()); + if (UNEXPECTED(offset.isUndef())) return zv::Val(); + } + } + return newVariableWrite(rootName, target, kind, true, offset.isUndef() ? NULL : offset.raw(), first == targetObj); + } + + /* Mirrors escapeRoot(). */ + static zv::Val escapeRoot(zval *expr) + { + zend_class_entry *arrayDimFetchCe = pt_class(PT_CLASS_ARRAY_DIM_FETCH); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + if (UNEXPECTED(arrayDimFetchCe == NULL || variableCe == NULL)) return zv::Val(); + zend_object *node = Z_OBJ_P(expr); + while (instanceof_function(node->ce, arrayDimFetchCe)) { + zv::Ref var = nodeProp(node, PT_LC("var")); + if (UNEXPECTED(var.raw() == NULL || !var.isObject())) return zv::Val::null(); + node = var.asObject(); + } + if (!instanceof_function(node->ce, variableCe)) return zv::Val::null(); + zend_string *name = variableName(node); + return name != NULL ? pt_variable_flow_escape(name) : zv::Val::null(); + } + +private: + /* VariableFlow::sequence(self::child($node->$a, $storage), self::child($node->$b, $storage)) */ + static zv::Val childSequence(zend_object *node, const char *a, size_t aLen, const char *b, size_t bLen, zval *storage) + { + zv::Ref first = nodeProp(node, a, aLen); + zv::Ref second = nodeProp(node, b, bLen); + zv::Val firstChild = child(first.raw() != NULL && first.isObject() ? first.raw() : NULL, storage); + if (UNEXPECTED(firstChild.isUndef())) return zv::Val(); + zv::Val secondChild = child(second.raw() != NULL && second.isObject() ? second.raw() : NULL, storage); + if (UNEXPECTED(secondChild.isUndef())) return zv::Val(); + zv::Args argv{firstChild.raw(), secondChild.raw()}; + return pt_variable_flow_sequence(2, argv); + } + + /* the recursion of writes(): appends the flow's writes to $writes; + * false = pending exception */ + [[nodiscard]] static bool collectWrites(zval *flow, zv::Arr &writes) + { + if (flow == NULL || Z_TYPE_P(flow) != IS_OBJECT) return true; + zend_class_entry *accessFlowCe = pt_class(PT_CLASS_VARIABLE_ACCESS_FLOW); + zend_class_entry *sequenceFlowCe = pt_class(PT_CLASS_VARIABLE_SEQUENCE_FLOW); + if (UNEXPECTED(accessFlowCe == NULL || sequenceFlowCe == NULL)) return false; + zend_object *flowObj = Z_OBJ_P(flow); + if (instanceof_function(flowObj->ce, accessFlowCe)) { + zv::Ref write = nodeProp(flowObj, PT_LC("write")); + if (write.raw() != NULL && write.isObject()) { + writes.push(write); + } + return true; + } + if (!instanceof_function(flowObj->ce, sequenceFlowCe)) return true; + zv::Ref children = nodeProp(flowObj, PT_LC("children")); + if (children.raw() == NULL || !children.isArray()) return true; + for (auto entry : zv::TableRef(children.asArrayTable())) { + if (UNEXPECTED(!collectWrites(entry.value().deref().raw(), writes))) return false; + } + return true; + } + + /* $type->isArray() / isString() as a PT_TRI_* value; -1 = pending + * exception */ + [[nodiscard]] static zend_long trinaryOp(zval *type, pt_type_op_id op) + { + if (UNEXPECTED(Z_TYPE_P(type) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: expected a Type, got %s", zend_zval_value_name(type)); + return -1; + } + zv::Val result = pt_type_op(Z_OBJ_P(type), op, 0, NULL); + if (UNEXPECTED(result.isUndef())) return -1; + return pt_type_trinary_value(result.raw()); + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::VariableFlowBuilder; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define PT_VFB_RETURN(expr) \ + do { \ + zv::Val pt_vfb_result = (expr); \ + if (UNEXPECTED(pt_vfb_result.isUndef())) { \ + RETURN_THROWS(); \ + } \ + pt_vfb_result.intoReturnValue(return_value); \ + } while (0) + +namespace { + +} // namespace + +void pt_register_variable_flow_builder() +{ + reg::Class cls("PHPStan\\Analyser\\VariableFlowBuilder"); + ptdecl::VariableFlowBuilder::declareClass(cls); + ptdecl::VariableFlowBuilder::declareProperties(cls); + + cls.method(sigs::throws, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr; + HashTable *throwPoints; + if (!zp::parse(execute_data, expr, throwPoints)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::throws(expr, throwPoints)); + }); + + cls.method(sigs::arguments, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *call, *argsResult, *storage; + if (!zp::parse(execute_data, call, argsResult, storage)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::arguments(call, argsResult, storage)); + }); + + cls.method(sigs::child, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *node, *storage; + if (!zp::parse(execute_data, node, storage)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::child(node, storage)); + }); + + cls.method(sigs::targetRead, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *target, *storage; + bool read; + zend_long targetId = 0; + bool targetIdIsNull = true; + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT(target) + Z_PARAM_OBJECT(storage) + Z_PARAM_BOOL(read) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(targetId, targetIdIsNull) + ZEND_PARSE_PARAMETERS_END(); + zval targetIdValue; + ZVAL_LONG(&targetIdValue, targetId); + PT_VFB_RETURN(VariableFlowBuilder::targetRead(target, storage, read, targetIdIsNull ? NULL : &targetIdValue)); + }); + + cls.method(sigs::writes, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *flow; + if (!zp::parse(execute_data, flow)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::writes(flow)); + }); + + cls.method(sigs::targetWrite, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *target, *scope, *storage, *redundant = NULL; + zend_long kind; + if (!zp::parse>(execute_data, target, kind, scope, storage, redundant)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::targetWrite(target, kind, scope, storage, redundant)); + }); + + cls.method(sigs::writeSite, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *target, *scope, *storage; + zend_long kind; + if (!zp::parse(execute_data, target, kind, scope, storage)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::writeSite(target, kind, scope, storage)); + }); + + cls.method(sigs::escapeRoot, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr; + if (!zp::parse(execute_data, expr)) RETURN_THROWS(); + PT_VFB_RETURN(VariableFlowBuilder::escapeRoot(expr)); + }); + + cls.shadow(&pt_ce_variable_flow_builder); +} + +/* }}} */ diff --git a/turbo-ext/src/VariableLivenessResolver.cpp b/turbo-ext/src/VariableLivenessResolver.cpp new file mode 100644 index 00000000000..85aec9b8282 --- /dev/null +++ b/turbo-ext/src/VariableLivenessResolver.cpp @@ -0,0 +1,1544 @@ +/* + * PHPStanTurbo\VariableLivenessResolver — native implementation of + * PHPStan\Analyser\VariableLivenessResolver. + * + * The twin's only public entry point is the static resolve(): it creates a + * private instance, walks the flow tree twice (collect(), then the + * backwards liveBefore() over the compiled access keys) and hands the + * result to a VariableWritesNode. The native class keeps that instance's + * state on the C stack — PHP arrays in the twin's exact shapes, so every + * set union, key order and insertion order is the one the twin produces — + * and never instantiates the PHP class; the private methods are C++ members + * of the same names. + * + * The flow objects are the PHP flow classes (their readonly properties read + * from the slots, per class entry), the writes are VariableWrite instances + * (slots when exactly that class, the getters otherwise), the Type queries + * of the catch clauses go through the Type ops. + */ + +#include "support.h" +#include "generated/VariableLivenessResolver.h" + +namespace sigs = ptdecl::VariableLivenessResolver::sig; +#include "zv.h" +#include "TypeTraits.h" +#include "TypeOps.h" + +#include +#include +#include + +static zend_class_entry *pt_ce_variable_liveness_resolver; + +namespace { + +/* the twin's VariableWrite::KIND_PARAMETER / KIND_CLOSURE_USE */ +const zend_long PT_VLR_KIND_PARAMETER = 12; +const zend_long PT_VLR_KIND_CLOSURE_USE = 13; + +/* VariableFlow::* as an enum, PT_VLR_OTHER for any other string */ +enum FlowKind +{ + PT_VLR_SEQUENCE, + PT_VLR_CHOICE, + PT_VLR_LOOP, + PT_VLR_TRY_CATCH, + PT_VLR_SWITCH, + PT_VLR_READ, + PT_VLR_WRITE, + PT_VLR_DEFINE, + PT_VLR_DISCARD, + PT_VLR_ESCAPE, + PT_VLR_MENTION, + PT_VLR_READ_ALL, + PT_VLR_MENTION_ALL, + PT_VLR_OPAQUE, + PT_VLR_DEAD, + PT_VLR_RETURN, + PT_VLR_BREAK, + PT_VLR_CONTINUE, + PT_VLR_THROW, + PT_VLR_STOP, + PT_VLR_ARROW, + PT_VLR_LOOP_STATEMENT, + PT_VLR_OTHER, +}; + +const struct { const char *value; size_t len; FlowKind kind; } pt_vlr_kinds[] = { + { PT_LC("sequence"), PT_VLR_SEQUENCE }, + { PT_LC("choice"), PT_VLR_CHOICE }, + { PT_LC("loop"), PT_VLR_LOOP }, + { PT_LC("try"), PT_VLR_TRY_CATCH }, + { PT_LC("switch"), PT_VLR_SWITCH }, + { PT_LC("read"), PT_VLR_READ }, + { PT_LC("write"), PT_VLR_WRITE }, + { PT_LC("define"), PT_VLR_DEFINE }, + { PT_LC("discard"), PT_VLR_DISCARD }, + { PT_LC("escape"), PT_VLR_ESCAPE }, + { PT_LC("mention"), PT_VLR_MENTION }, + { PT_LC("readAll"), PT_VLR_READ_ALL }, + { PT_LC("mentionAll"), PT_VLR_MENTION_ALL }, + { PT_LC("opaque"), PT_VLR_OPAQUE }, + { PT_LC("dead"), PT_VLR_DEAD }, + { PT_LC("return"), PT_VLR_RETURN }, + { PT_LC("break"), PT_VLR_BREAK }, + { PT_LC("continue"), PT_VLR_CONTINUE }, + { PT_LC("throw"), PT_VLR_THROW }, + { PT_LC("stop"), PT_VLR_STOP }, + { PT_LC("arrow"), PT_VLR_ARROW }, + { PT_LC("loopStatement"), PT_VLR_LOOP_STATEMENT }, +}; + +FlowKind kindOf(zend_string *kind) +{ + for (size_t i = 0; i < sizeof(pt_vlr_kinds) / sizeof(pt_vlr_kinds[0]); i++) { + if (ZSTR_LEN(kind) == pt_vlr_kinds[i].len && memcmp(ZSTR_VAL(kind), pt_vlr_kinds[i].value, ZSTR_LEN(kind)) == 0) return pt_vlr_kinds[i].kind; + } + return PT_VLR_OTHER; +} + +/* the property slots of the four final PHP flow classes, resolved once per + * resolve() (their class entries come from the class map) */ +struct FlowSlots +{ + zend_class_entry *accessCe; + zend_class_entry *sequenceCe; + zend_class_entry *controlCe; + zend_class_entry *inputCe; + /* VariableFlow::$kind */ + uint32_t accessKind, sequenceKind, controlKind, inputKind; + /* VariableAccessFlow */ + uint32_t accessName, accessWrite, accessType, accessTargetId, accessContainer, accessOffset; + /* VariableSequenceFlow */ + uint32_t sequenceChildren; + /* VariableControlFlow */ + uint32_t controlChildren, controlName, controlType, controlLevel, controlAtLeastOnce, controlCanExit, controlCatches, controlArrow, controlCases, controlCanRepeat, controlCanContainAnyThrowable, controlStmt, controlBindings, controlOwnWrites; + /* VariableInputFlow */ + uint32_t inputWriteId, inputTargetId; + + static bool offsetOf(zend_class_entry *ce, const char *name, uint32_t &out) + { + int32_t offset = pt_instance_prop_offset(ce, name, strlen(name)); + if (UNEXPECTED(offset < 0)) { + zend_throw_error(NULL, "phpstan_turbo: %s has no property $%s", ZSTR_VAL(ce->name), name); + return false; + } + out = (uint32_t) offset; + return true; + } + + /* false = pending exception */ + [[nodiscard]] bool resolve() + { + accessCe = pt_class(PT_CLASS_VARIABLE_ACCESS_FLOW); + sequenceCe = pt_class(PT_CLASS_VARIABLE_SEQUENCE_FLOW); + controlCe = pt_class(PT_CLASS_VARIABLE_CONTROL_FLOW); + inputCe = pt_class(PT_CLASS_VARIABLE_INPUT_FLOW); + if (UNEXPECTED(accessCe == NULL || sequenceCe == NULL || controlCe == NULL || inputCe == NULL)) return false; + return offsetOf(accessCe, "kind", accessKind) && offsetOf(sequenceCe, "kind", sequenceKind) + && offsetOf(controlCe, "kind", controlKind) && offsetOf(inputCe, "kind", inputKind) + && offsetOf(accessCe, "name", accessName) && offsetOf(accessCe, "write", accessWrite) + && offsetOf(accessCe, "type", accessType) && offsetOf(accessCe, "targetId", accessTargetId) + && offsetOf(accessCe, "container", accessContainer) && offsetOf(accessCe, "offset", accessOffset) + && offsetOf(sequenceCe, "children", sequenceChildren) + && offsetOf(controlCe, "children", controlChildren) && offsetOf(controlCe, "name", controlName) + && offsetOf(controlCe, "type", controlType) && offsetOf(controlCe, "level", controlLevel) + && offsetOf(controlCe, "atLeastOnce", controlAtLeastOnce) && offsetOf(controlCe, "canExit", controlCanExit) + && offsetOf(controlCe, "catches", controlCatches) && offsetOf(controlCe, "arrow", controlArrow) + && offsetOf(controlCe, "cases", controlCases) && offsetOf(controlCe, "canRepeat", controlCanRepeat) + && offsetOf(controlCe, "canContainAnyThrowable", controlCanContainAnyThrowable) + && offsetOf(controlCe, "stmt", controlStmt) && offsetOf(controlCe, "bindings", controlBindings) + && offsetOf(controlCe, "ownWrites", controlOwnWrites) + && offsetOf(inputCe, "writeId", inputWriteId) && offsetOf(inputCe, "targetId", inputTargetId); + } +}; + +/* a readonly property slot, dereferenced; NULL with the engine's Error + * pending when it was never initialized */ +zval *slotOf(zend_object *object, uint32_t offset, const char *name) +{ + zval *slot = OBJ_PROP(object, offset); + ZVAL_DEREF(slot); + if (UNEXPECTED(Z_TYPE_P(slot) == IS_UNDEF)) { + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(object->ce->name), name); + return NULL; + } + return slot; +} + +/* the getters of a VariableWrite as values: the slots for an instance of + * exactly that class, the getters otherwise */ +struct WriteView +{ + zv::Val name; /* string */ + zend_string *nameStr; /* borrowed from name */ + zend_long id; + zv::Val offset; /* int|string|null */ + bool offsetIsNull; + bool offsetWrite; + bool parentIdIsNull; + zend_long parentId; + bool replacesOffset; + + /* false = pending exception */ + [[nodiscard]] bool load(zval *write) + { + bool error; + const pt_variable_write_slots *slots = pt_variable_write_slots_of(Z_OBJ_P(write), error); + zend_object *object = Z_OBJ_P(write); + zv::Val parentIdValue; + if (slots != NULL) { + name = zv::Val::copyOf(zv::Ref(OBJ_PROP(object, slots->variableName))); + id = Z_LVAL_P(OBJ_PROP(object, slots->id)); + offset = zv::Val::copyOf(zv::Ref(OBJ_PROP(object, slots->offset)).deref()); + offsetWrite = zend_is_true(OBJ_PROP(object, slots->offsetWrite)); + parentIdValue = zv::Val::copyOf(zv::Ref(OBJ_PROP(object, slots->parentId)).deref()); + replacesOffset = zend_is_true(OBJ_PROP(object, slots->replacesOffset)); + } else { + if (UNEXPECTED(error)) return false; + name = pt_type_call(object, PT_LC("getvariablename"), 0, NULL); + if (UNEXPECTED(name.isUndef())) return false; + zv::Val idValue = pt_type_call(object, PT_LC("getid"), 0, NULL); + if (UNEXPECTED(idValue.isUndef())) return false; + id = zval_get_long(idValue.raw()); + offset = pt_type_call(object, PT_LC("getoffset"), 0, NULL); + if (UNEXPECTED(offset.isUndef())) return false; + zv::Val offsetWriteValue = pt_type_call(object, PT_LC("isoffsetwrite"), 0, NULL); + if (UNEXPECTED(offsetWriteValue.isUndef())) return false; + offsetWrite = zend_is_true(offsetWriteValue.raw()); + parentIdValue = pt_type_call(object, PT_LC("getparentid"), 0, NULL); + if (UNEXPECTED(parentIdValue.isUndef())) return false; + zv::Val replacesOffsetValue = pt_type_call(object, PT_LC("replacesoffset"), 0, NULL); + if (UNEXPECTED(replacesOffsetValue.isUndef())) return false; + replacesOffset = zend_is_true(replacesOffsetValue.raw()); + } + if (UNEXPECTED(Z_TYPE_P(name.raw()) != IS_STRING)) { + zend_type_error("phpstan_turbo: VariableWrite::getVariableName() did not return a string"); + return false; + } + nameStr = Z_STR_P(name.raw()); + offsetIsNull = Z_TYPE_P(offset.raw()) == IS_NULL; + parentIdIsNull = Z_TYPE_P(parentIdValue.raw()) == IS_NULL; + parentId = parentIdIsNull ? 0 : zval_get_long(parentIdValue.raw()); + return true; + } +}; + +/* $write->getId() alone (the loop bindings and own writes) */ +bool writeId(zval *write, zend_long &out) +{ + bool error; + const pt_variable_write_slots *slots = pt_variable_write_slots_of(Z_OBJ_P(write), error); + if (slots != NULL) { + out = Z_LVAL_P(OBJ_PROP(Z_OBJ_P(write), slots->id)); + return true; + } + if (UNEXPECTED(error)) return false; + zv::Val idValue = pt_type_call(Z_OBJ_P(write), PT_LC("getid"), 0, NULL); + if (UNEXPECTED(idValue.isUndef())) return false; + out = zval_get_long(idValue.raw()); + return true; +} + +/* $write->getVariableName() alone (resolveDependencies) */ +zv::Val writeName(zval *write) +{ + bool error; + const pt_variable_write_slots *slots = pt_variable_write_slots_of(Z_OBJ_P(write), error); + if (slots != NULL) return zv::Val::copyOf(zv::Ref(OBJ_PROP(Z_OBJ_P(write), slots->variableName))); + if (UNEXPECTED(error)) return zv::Val(); + return pt_type_call(Z_OBJ_P(write), PT_LC("getvariablename"), 0, NULL); +} + +/* {{{ the twin's array / array tables as owned PHP + * arrays: every operation is the PHP array operation of the same name, so + * key order and copy-on-write behaviour are the twin's */ + +/* the empty array literal */ +zv::Val emptyArray() +{ + zval empty; + ZVAL_EMPTY_ARRAY(&empty); + return zv::Val::adopt(empty); +} + +HashTable *tableOf(const zv::Val &array) +{ + return Z_ARRVAL_P(const_cast(array).raw()); +} + +uint32_t countOf(const zv::Val &array) +{ + return zend_hash_num_elements(tableOf(array)); +} + +/* a shared (addref) copy */ +zv::Val shareArray(const zv::Val &array) +{ + return zv::Val::copyOf(zv::Ref(const_cast(array).raw())); +} + +/* $array[$key] = true / $array[$index] = true (separating a shared array) */ +void setTrue(zv::Val &array, zend_string *key) +{ + SEPARATE_ARRAY(array.raw()); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_update(Z_ARRVAL_P(array.raw()), key, &trueValue); +} + +void setTrueIndex(zv::Val &array, zend_long index) +{ + SEPARATE_ARRAY(array.raw()); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_index_update(Z_ARRVAL_P(array.raw()), (zend_ulong) index, &trueValue); +} + +/* $array[$index] = $value (borrowed, addref'd) */ +void setIndex(zv::Val &array, zend_long index, zval *value) +{ + SEPARATE_ARRAY(array.raw()); + Z_TRY_ADDREF_P(value); + zend_hash_index_update(Z_ARRVAL_P(array.raw()), (zend_ulong) index, value); +} + +/* unset($array[$key]) */ +void unsetKey(zv::Val &array, zend_string *key) +{ + SEPARATE_ARRAY(array.raw()); + zend_hash_del(Z_ARRVAL_P(array.raw()), key); +} + +/* $array[$index] ?? NULL — the inner table (borrowed), NULL when absent */ +HashTable *innerTable(const zv::Val &array, zend_long index) +{ + zval *slot = zend_hash_index_find(tableOf(array), (zend_ulong) index); + if (slot == NULL) return NULL; + ZVAL_DEREF(slot); + return Z_TYPE_P(slot) == IS_ARRAY ? Z_ARRVAL_P(slot) : NULL; +} + +HashTable *innerTableByKey(const zv::Val &array, zend_string *key) +{ + zval *slot = zend_hash_find(tableOf(array), key); + if (slot == NULL) return NULL; + ZVAL_DEREF(slot); + return Z_TYPE_P(slot) == IS_ARRAY ? Z_ARRVAL_P(slot) : NULL; +} + +/* &$array[$index] as an array, created empty when absent (the twin's + * `$this->x[$i][$j] = ...` autovivification); the returned slot is writable */ +zval *innerSlot(zv::Val &array, zend_long index) +{ + SEPARATE_ARRAY(array.raw()); + zval *slot = zend_hash_index_find(Z_ARRVAL_P(array.raw()), (zend_ulong) index); + if (slot == NULL) { + zval empty; + ZVAL_EMPTY_ARRAY(&empty); + slot = zend_hash_index_add_new(Z_ARRVAL_P(array.raw()), (zend_ulong) index, &empty); + } + SEPARATE_ARRAY(slot); + return slot; +} + +zval *innerSlotByKey(zv::Val &array, zend_string *key) +{ + SEPARATE_ARRAY(array.raw()); + zval *slot = zend_hash_find(Z_ARRVAL_P(array.raw()), key); + if (slot == NULL) { + zval empty; + ZVAL_EMPTY_ARRAY(&empty); + slot = zend_hash_add_new(Z_ARRVAL_P(array.raw()), key, &empty); + } + SEPARATE_ARRAY(slot); + return slot; +} + +/* $a + $b: a copy of $a with $b's entries under keys $a lacks */ +zv::Val unionOf(const zv::Val &a, HashTable *b) +{ + if (b == NULL || zend_hash_num_elements(b) == 0) return shareArray(a); + zval result; + ZVAL_ARR(&result, zend_array_dup(tableOf(a))); + zend_hash_merge(Z_ARRVAL(result), b, zval_add_ref, 0); + return zv::Val::adopt(result); +} + +zv::Val unionOf(const zv::Val &a, const zv::Val &b) +{ + return unionOf(a, tableOf(b)); +} + +bool isTrueAt(const zv::Val &array, zend_string *key) +{ + return zend_hash_exists(tableOf(array), key); +} + +bool isTrueAtIndex(const zv::Val &array, zend_long index) +{ + return zend_hash_index_exists(tableOf(array), (zend_ulong) index); +} + +/* }}} */ + +/* the live variables at the surrounding control-flow destinations (the + * twin's VariableFlowContext, a value object that never leaves resolve()) */ +struct Context +{ + zv::Val return_; + std::vector breaks; + std::vector continues; + std::vector> catches; /* [Type (borrowed from the flow), destination] */ + zv::Val uncaught; + + Context() : return_(emptyArray()), uncaught(emptyArray()) {} + Context(Context &&) = default; + Context &operator=(Context &&) = default; + + static std::vector shareAll(const std::vector &arrays) + { + std::vector copies; + copies.reserve(arrays.size()); + for (const zv::Val &array : arrays) { + copies.push_back(shareArray(array)); + } + return copies; + } + + static std::vector> shareCatches(const std::vector> &catches) + { + std::vector> copies; + copies.reserve(catches.size()); + for (const auto &entry : catches) { + copies.emplace_back(entry.first, shareArray(entry.second)); + } + return copies; + } + + /* [$first, ...$rest] */ + static std::vector prepend(const zv::Val &first, const std::vector &rest) + { + std::vector result; + result.reserve(rest.size() + 1); + result.push_back(shareArray(first)); + for (const zv::Val &array : rest) { + result.push_back(shareArray(array)); + } + return result; + } + +private: + Context(const Context &) = delete; +}; + +} // namespace + +namespace phpstanturbo { + +/* Mirrors PHPStan\Analyser\VariableLivenessResolver; one instance per + * resolve() call, on the C stack. A method returning bool reports a pending + * exception with false, one returning zv::Val with UNDEF. */ +class VariableLivenessResolver +{ +public: + /* Mirrors resolve(); $flow NULL for null. */ + static zv::Val resolve(zval *function, zval *flow) + { + VariableLivenessResolver self; + if (UNEXPECTED(!self.slots.resolve())) return zv::Val(); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + if (UNEXPECTED(variableCe == NULL || closureCe == NULL)) return zv::Val(); + + zv::Val returnsByRef = pt_type_call(Z_OBJ_P(function), PT_LC("returnsbyref"), 0, NULL); + if (UNEXPECTED(returnsByRef.isUndef())) return zv::Val(); + self.returnsByReference = zend_is_true(returnsByRef.raw()); + zv::Arr imports = zv::Arr::empty(); + zv::Val params = pt_type_call(Z_OBJ_P(function), PT_LC("getparams"), 0, NULL); + if (UNEXPECTED(params.isUndef())) return zv::Val(); + if (Z_TYPE_P(params.raw()) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(params.raw()))) { + zv::Ref param = entry.value().deref(); + if (!param.isObject()) continue; + zv::Ref var = zv::ObjRef(param.asObject()).prop(PT_LC("var")); + if (var.raw() == NULL) continue; + var = var.deref(); + zend_string *name = var.instanceOf(variableCe) ? variableName(var.asObject()) : NULL; + if (name == NULL) continue; + zv::Ref byRef = zv::ObjRef(param.asObject()).prop(PT_LC("byRef")); + zv::Ref flags = zv::ObjRef(param.asObject()).prop(PT_LC("flags")); + if ((byRef.raw() != NULL && zend_is_true(byRef.raw())) || (flags.raw() != NULL && zval_get_long(flags.deref().raw()) != 0)) { + setTrue(self.escapedNames, name); + continue; + } + if (UNEXPECTED(!self.import(imports, name, var.raw(), PT_VLR_KIND_PARAMETER))) return zv::Val(); + } + } + if (instanceof_function(Z_OBJCE_P(function), closureCe)) { + zv::Ref uses = zv::ObjRef(Z_OBJ_P(function)).prop(PT_LC("uses")); + if (uses.raw() != NULL && uses.deref().isArray()) { + for (auto entry : zv::TableRef(uses.deref().asArrayTable())) { + zv::Ref use = entry.value().deref(); + if (!use.isObject()) continue; + zv::Ref var = zv::ObjRef(use.asObject()).prop(PT_LC("var")); + if (var.raw() == NULL) continue; + var = var.deref(); + zend_string *name = var.isObject() ? variableName(var.asObject()) : NULL; + if (name == NULL) continue; + zv::Ref byRef = zv::ObjRef(use.asObject()).prop(PT_LC("byRef")); + if (byRef.raw() != NULL && zend_is_true(byRef.raw())) { + setTrue(self.escapedNames, name); + continue; + } + if (UNEXPECTED(!self.import(imports, name, var.raw(), PT_VLR_KIND_CLOSURE_USE))) return zv::Val(); + } + } + } + /* VariableFlow::sequence(...[...$imports, $flow]) */ + zv::Arr sequenceArgs = zv::Arr::create(zend_hash_num_elements(imports.table()) + 1); + for (auto entry : zv::TableRef(imports.table())) { + sequenceArgs.push(entry.value()); + } + if (flow != NULL) { + sequenceArgs.push(zv::Ref(flow)); + } else { + sequenceArgs.push(zv::Val::null()); + } + zv::Val body = pt_variable_flow_sequence_list(sequenceArgs.table()); + if (UNEXPECTED(body.isUndef())) return zv::Val(); + zval *bodyFlow = Z_TYPE_P(body.raw()) == IS_OBJECT ? body.raw() : NULL; + if (UNEXPECTED(!self.collect(bodyFlow, false))) return zv::Val(); + if (countOf(self.writes) != 0 && !self.opaque) { + if (UNEXPECTED(!self.compileAccesses())) return zv::Val(); + Context context; + zv::Val live = self.liveBefore(bodyFlow, emptyArray(), context); + if (UNEXPECTED(live.isUndef())) return zv::Val(); + if (UNEXPECTED(!self.resolveDependencies())) return zv::Val(); + self.resolveCoverage(); + } + + /* new VariableWritesNode(...) */ + zv::Arr writeList = zv::Arr::create(countOf(self.writes)); + for (auto entry : zv::TableRef(tableOf(self.writes))) { + writeList.push(entry.value()); + } + zv::Val readWriteIds = unionOf(self.observedIds, self.readIds); + zval argv[12]; + ZVAL_COPY_VALUE(&argv[0], function); + ZVAL_COPY_VALUE(&argv[1], writeList.raw()); + ZVAL_COPY_VALUE(&argv[2], readWriteIds.raw()); + ZVAL_COPY_VALUE(&argv[3], self.readIds.raw()); + ZVAL_COPY_VALUE(&argv[4], self.coveredIds.raw()); + ZVAL_COPY_VALUE(&argv[5], self.readNames.raw()); + ZVAL_COPY_VALUE(&argv[6], self.redundantTypes.raw()); + ZVAL_COPY_VALUE(&argv[7], self.mentionedNames.raw()); + ZVAL_COPY_VALUE(&argv[8], self.escapedNames.raw()); + ZVAL_COPY_VALUE(&argv[9], self.variableOverwritingLoops.raw()); + ZVAL_BOOL(&argv[10], self.opaque); + ZVAL_BOOL(&argv[11], self.allNamesMentioned); + return pt_type_new(PT_CLASS_VARIABLE_WRITES_NODE, 12, argv); + } + +private: + FlowSlots slots; + zv::Val writes = emptyArray(); + zv::Val readIds = emptyArray(); + zv::Val observedIds = emptyArray(); + zv::Val readNames = emptyArray(); + zv::Val mentionedNames = emptyArray(); + zv::Val escapedNames = emptyArray(); + zv::Val redundantTypes = emptyArray(); + zv::Val accesses = emptyArray(); + zv::Val readKeys = emptyArray(); + zv::Val nameKeys = emptyArray(); + zv::Val observedKeys = emptyArray(); + zv::Val killedKeys = emptyArray(); + zv::Val dependencies = emptyArray(); + zv::Val inputCopies = emptyArray(); + zv::Val inputSinks = emptyArray(); + zv::Val literalItems = emptyArray(); + zv::Val coveredIds = emptyArray(); + zv::Val allReadKeys = emptyArray(); + zv::Val loopStatements = emptyArray(); + zv::Val ownWriteIds = emptyArray(); + zv::Val variableOverwritingLoops = emptyArray(); + bool opaque = false; + bool readsAllVariables = false; + bool allNamesMentioned = false; + bool returnsByReference = false; + + VariableLivenessResolver() = default; + + /* a Variable node's string name (borrowed), NULL for a variable variable */ + static zend_string *variableName(zend_object *variable) + { + zv::Ref name = zv::ObjRef(variable).prop(PT_LC("name")); + if (name.raw() == NULL) return NULL; + name = name.deref(); + return name.isString() ? name.asString() : NULL; + } + + /* $imports[] = VariableFlow::write(new VariableWrite($name, $var, spl_object_id($var), $kind)) */ + bool import(zv::Arr &imports, zend_string *name, zval *var, zend_long kind) + { + zv::Args argv{name, var, zend_long((zend_long) Z_OBJ_HANDLE_P(var)), zend_long(kind)}; + zv::Val write = pt_type_new(PT_CLASS_VARIABLE_WRITE, 4, argv); + if (UNEXPECTED(write.isUndef())) return false; + zv::Val flow = pt_variable_flow_write(write.raw(), NULL); + if (UNEXPECTED(flow.isUndef())) return false; + imports.push(std::move(flow)); + return true; + } + + /* {{{ flow readers */ + + zval *slot(zend_object *flow, uint32_t offset, const char *name) + { + return slotOf(flow, offset, name); + } + + /* $flow->kind; PT_VLR_OTHER with an exception pending when unreadable */ + FlowKind kindOfFlow(zend_object *flow, uint32_t kindOffset) + { + zval *kind = slot(flow, kindOffset, "kind"); + if (UNEXPECTED(kind == NULL || Z_TYPE_P(kind) != IS_STRING)) { + if (kind != NULL) { + zend_throw_error(NULL, "phpstan_turbo: VariableFlow::$kind is not a string"); + } + return PT_VLR_OTHER; + } + return kindOf(Z_STR_P(kind)); + } + + bool isAccess(zend_object *flow) const { return instanceof_function(flow->ce, slots.accessCe); } + bool isSequence(zend_object *flow) const { return instanceof_function(flow->ce, slots.sequenceCe); } + bool isControl(zend_object *flow) const { return instanceof_function(flow->ce, slots.controlCe); } + bool isInput(zend_object *flow) const { return instanceof_function(flow->ce, slots.inputCe); } + + /* the kind slot of whichever flow class this is */ + uint32_t kindOffsetOf(zend_object *flow) const + { + if (isAccess(flow)) return slots.accessKind; + if (isSequence(flow)) return slots.sequenceKind; + if (isControl(flow)) return slots.controlKind; + return slots.inputKind; + } + + /* a flow-valued slot: NULL for null, the object otherwise */ + static zval *flowOf(zval *slotValue) + { + return slotValue != NULL && Z_TYPE_P(slotValue) == IS_OBJECT ? slotValue : NULL; + } + + /* $children[$i] (a list of ?VariableFlow) */ + static zval *childAt(HashTable *children, zend_long index) + { + zval *child = zend_hash_index_find(children, (zend_ulong) index); + if (child == NULL) return NULL; + ZVAL_DEREF(child); + return flowOf(child); + } + + /* }}} */ + + /* Mirrors collect(). */ + bool collect(zval *flowValue, bool dead) + { + if (flowValue == NULL) return true; + zend_object *flow = Z_OBJ_P(flowValue); + if (isInput(flow)) return true; + bool access = isAccess(flow); + FlowKind kind = kindOfFlow(flow, kindOffsetOf(flow)); + if (UNEXPECTED(EG(exception))) return false; + if (access) { + zval *name = slot(flow, slots.accessName, "name"); + if (UNEXPECTED(name == NULL)) return false; + if (Z_TYPE_P(name) == IS_STRING && !zend_string_equals_literal(Z_STR_P(name), "this") && !pt_is_superglobal_name(Z_STR_P(name))) { + setTrue(mentionedNames, Z_STR_P(name)); + zval *accessList = innerSlotByKey(accesses, Z_STR_P(name)); + Z_ADDREF_P(flowValue); + zend_hash_next_index_insert(Z_ARRVAL_P(accessList), flowValue); + if (kind == PT_VLR_READ) { + setTrue(readNames, Z_STR_P(name)); + } else if (kind == PT_VLR_ESCAPE) { + setTrue(escapedNames, Z_STR_P(name)); + } + zval *write = slot(flow, slots.accessWrite, "write"); + if (UNEXPECTED(write == NULL)) return false; + if (Z_TYPE_P(write) == IS_OBJECT && kind != PT_VLR_DISCARD) { + WriteView view; + if (UNEXPECTED(!view.load(write))) return false; + setIndex(writes, view.id, write); + if (!view.parentIdIsNull) { + zval *items = innerSlot(literalItems, view.parentId); + Z_ADDREF_P(write); + zend_hash_index_update(Z_ARRVAL_P(items), (zend_ulong) view.id, write); + } + zval *type = slot(flow, slots.accessType, "type"); + if (UNEXPECTED(type == NULL)) return false; + if (Z_TYPE_P(type) != IS_NULL) { + setIndex(redundantTypes, view.id, type); + } + if (dead) { + setTrueIndex(readIds, view.id); + } + } + } + } + if (kind == PT_VLR_READ_ALL) { + readsAllVariables = true; + } + if (kind == PT_VLR_OPAQUE) { + opaque = true; + } + if (kind == PT_VLR_READ_ALL || kind == PT_VLR_MENTION_ALL) { + allNamesMentioned = true; + } + if (access) return true; + bool control = isControl(flow); + if (!isSequence(flow) && !control) { + pt_throw_should_not_happen(); + return false; + } + zval *children = slot(flow, control ? slots.controlChildren : slots.sequenceChildren, "children"); + if (UNEXPECTED(children == NULL)) return false; + if (Z_TYPE_P(children) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(children))) { + if (UNEXPECTED(!collect(flowOf(entry.value().deref().raw()), dead || kind == PT_VLR_DEAD))) return false; + } + } + if (!control) return true; + if (kind == PT_VLR_LOOP_STATEMENT) { + zval *stmt = slot(flow, slots.controlStmt, "stmt"); + if (UNEXPECTED(stmt == NULL)) return false; + if (Z_TYPE_P(stmt) == IS_OBJECT) { + zval *ownWrites = slot(flow, slots.controlOwnWrites, "ownWrites"); + zval *bindings = slot(flow, slots.controlBindings, "bindings"); + if (UNEXPECTED(ownWrites == NULL || bindings == NULL)) return false; + zv::Val ownIds = emptyArray(); + if (Z_TYPE_P(ownWrites) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(ownWrites))) { + zend_long id; + if (UNEXPECTED(!writeId(entry.value().deref().raw(), id))) return false; + setTrueIndex(ownIds, id); + } + } + if (Z_TYPE_P(bindings) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(bindings))) { + zend_long id; + if (UNEXPECTED(!writeId(entry.value().deref().raw(), id))) return false; + setIndex(loopStatements, id, stmt); + setIndex(ownWriteIds, id, ownIds.raw()); + } + } + } + } + if (kind == PT_VLR_RETURN && returnsByReference) { + zval *name = slot(flow, slots.controlName, "name"); + if (UNEXPECTED(name == NULL)) return false; + if (Z_TYPE_P(name) == IS_STRING) { + setTrue(escapedNames, Z_STR_P(name)); + } + } + zval *cases = slot(flow, slots.controlCases, "cases"); + if (UNEXPECTED(cases == NULL)) return false; + if (Z_TYPE_P(cases) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(cases))) { + zv::Ref caseEntry = entry.value().deref(); + if (!caseEntry.isArray()) continue; + if (UNEXPECTED(!collect(childAt(caseEntry.asArrayTable(), 0), dead) || !collect(childAt(caseEntry.asArrayTable(), 1), dead))) return false; + } + } + zval *catches = slot(flow, slots.controlCatches, "catches"); + if (UNEXPECTED(catches == NULL)) return false; + if (Z_TYPE_P(catches) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(catches))) { + zv::Ref catchEntry = entry.value().deref(); + if (!catchEntry.isArray()) continue; + if (UNEXPECTED(!collect(childAt(catchEntry.asArrayTable(), 1), dead))) return false; + } + } + return true; + } + + /* Mirrors liveBefore(); $next is consumed, the result owned. */ + zv::Val liveBefore(zval *flowValue, zv::Val next, const Context &context) + { + if (flowValue == NULL) return next; + zend_object *flow = Z_OBJ_P(flowValue); + FlowKind kind = kindOfFlow(flow, kindOffsetOf(flow)); + if (UNEXPECTED(EG(exception))) return zv::Val(); + if (kind == PT_VLR_DEAD) return next; + if (isInput(flow)) { + zval *writeIdValue = slot(flow, slots.inputWriteId, "writeId"); + zval *targetId = slot(flow, slots.inputTargetId, "targetId"); + if (UNEXPECTED(writeIdValue == NULL || targetId == NULL)) return zv::Val(); + if (Z_TYPE_P(targetId) == IS_NULL) { + setTrueIndex(inputSinks, zval_get_long(writeIdValue)); + } else { + zval *copies = innerSlot(inputCopies, zval_get_long(targetId)); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_index_update(Z_ARRVAL_P(copies), (zend_ulong) zval_get_long(writeIdValue), &trueValue); + } + return next; + } + if (isAccess(flow)) { + zval *name = slot(flow, slots.accessName, "name"); + if (UNEXPECTED(name == NULL)) return zv::Val(); + if (kind == PT_VLR_READ || kind == PT_VLR_ESCAPE) { + // a by-reference capture aliases the variable - the value it + // holds at that point is observable through the alias + if (kind == PT_VLR_ESCAPE && countOf(loopStatements) != 0) { + next = passBindingProbes(std::move(next), Z_STR_P(name), NULL, false); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + } + return unionOf(next, innerTable(readKeys, (zend_long) flow->handle)); + } + zval *write = slot(flow, slots.accessWrite, "write"); + if (UNEXPECTED(write == NULL)) return zv::Val(); + if (Z_TYPE_P(write) != IS_OBJECT || kind == PT_VLR_DEFINE) return next; + WriteView view; + if (UNEXPECTED(!view.load(write))) return zv::Val(); + if (countOf(loopStatements) != 0) { + next = passBindingProbes(std::move(next), Z_STR_P(name), &view, kind == PT_VLR_DISCARD); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + } + zend_long id = view.id; + if (kind != PT_VLR_DISCARD) { + observeWrite(id, next); + HashTable *items = innerTable(literalItems, id); + if (items != NULL) { + for (auto entry : zv::TableRef(items)) { + zend_long itemId; + if (UNEXPECTED(!writeId(entry.value().deref().raw(), itemId))) return zv::Val(); + observeWrite(itemId, next); + } + } + } + HashTable *killed = innerTable(killedKeys, id); + if (killed != NULL) { + for (auto entry : zv::TableRef(killed)) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + unsetKey(next, key); + } + } + } + return next; + } + if (isSequence(flow)) { + zval *children = slot(flow, slots.sequenceChildren, "children"); + if (UNEXPECTED(children == NULL)) return zv::Val(); + HashTable *childTable = Z_TYPE_P(children) == IS_ARRAY ? Z_ARRVAL_P(children) : NULL; + if (kind == PT_VLR_SEQUENCE) { + if (childTable != NULL) { + /* array_reverse($flow->children): the list walked backwards */ + for (zend_long i = (zend_long) zend_hash_num_elements(childTable) - 1; i >= 0; i--) { + next = liveBefore(childAt(childTable, i), std::move(next), context); + if (UNEXPECTED(next.isUndef())) return zv::Val(); + } + } + return next; + } + zv::Val names = emptyArray(); + if (childTable != NULL) { + for (auto entry : zv::TableRef(childTable)) { + zv::Val childNames = liveBefore(flowOf(entry.value().deref().raw()), shareArray(next), context); + if (UNEXPECTED(childNames.isUndef())) return zv::Val(); + names = unionOf(names, childNames); + } + } + return names; + } + if (!isControl(flow)) { + pt_throw_should_not_happen(); + return zv::Val(); + } + zval *children = slot(flow, slots.controlChildren, "children"); + if (UNEXPECTED(children == NULL)) return zv::Val(); + HashTable *childTable = Z_TYPE_P(children) == IS_ARRAY ? Z_ARRVAL_P(children) : NULL; + if (kind == PT_VLR_LOOP_STATEMENT) { + // a binding reusing a variable that is read after the loop: the + // probe follows the variable backwards through the statement; + // surviving to its entry, it is armed to catch the assignment + // before the loop whose value the binding replaces + zval *bindings = slot(flow, slots.controlBindings, "bindings"); + if (UNEXPECTED(bindings == NULL)) return zv::Val(); + HashTable *bindingTable = Z_TYPE_P(bindings) == IS_ARRAY ? Z_ARRVAL_P(bindings) : NULL; + if (bindingTable != NULL) { + for (auto entry : zv::TableRef(bindingTable)) { + WriteView binding; + if (UNEXPECTED(!binding.load(entry.value().deref().raw()))) return zv::Val(); + HashTable *keys = innerTableByKey(nameKeys, binding.nameStr); + if (keys == NULL) continue; + for (auto keyEntry : zv::TableRef(keys)) { + zend_string *key = keyEntry.stringKeyOrNull(); + if (key == NULL || !isTrueAt(next, key)) continue; + zv::Str probe = bindingProbe(binding, false); + setTrue(next, probe.get()); + break; + } + } + } + zv::Val names = liveBefore(childTable != NULL ? childAt(childTable, 0) : NULL, std::move(next), context); + if (UNEXPECTED(names.isUndef())) return zv::Val(); + if (bindingTable != NULL) { + for (auto entry : zv::TableRef(bindingTable)) { + WriteView binding; + if (UNEXPECTED(!binding.load(entry.value().deref().raw()))) return zv::Val(); + zv::Str probe = bindingProbe(binding, false); + if (!isTrueAt(names, probe.get())) continue; + unsetKey(names, probe.get()); + zv::Str armed = bindingProbe(binding, true); + setTrue(names, armed.get()); + } + } + return names; + } + if (kind == PT_VLR_ARROW) { + zval *arrow = slot(flow, slots.controlArrow, "arrow"); + if (UNEXPECTED(arrow == NULL)) return zv::Val(); + if (Z_TYPE_P(arrow) == IS_OBJECT) { + Context innerContext; + zv::Val outputs = liveBefore(childTable != NULL ? childAt(childTable, 1) : NULL, emptyArray(), innerContext); + if (UNEXPECTED(outputs.isUndef())) return zv::Val(); + Context bodyContext; + bodyContext.return_ = shareArray(outputs); + bodyContext.uncaught = shareArray(outputs); + zv::Val names = liveBefore(childTable != NULL ? childAt(childTable, 0) : NULL, shareArray(outputs), bodyContext); + if (UNEXPECTED(names.isUndef())) return zv::Val(); + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + if (UNEXPECTED(variableCe == NULL)) return zv::Val(); + zv::Ref params = zv::ObjRef(Z_OBJ_P(arrow)).prop(PT_LC("params")); + if (params.raw() != NULL && params.deref().isArray()) { + for (auto entry : zv::TableRef(params.deref().asArrayTable())) { + zv::Ref param = entry.value().deref(); + if (!param.isObject()) continue; + zv::Ref var = zv::ObjRef(param.asObject()).prop(PT_LC("var")); + if (var.raw() == NULL) continue; + var = var.deref(); + zend_string *paramName = var.instanceOf(variableCe) ? variableName(var.asObject()) : NULL; + if (paramName == NULL) continue; + HashTable *keys = innerTableByKey(nameKeys, paramName); + if (keys == NULL) continue; + /* array_keys() snapshots the keys; $names is a + * different table, so walking $keys directly is the + * same walk */ + for (auto keyEntry : zv::TableRef(keys)) { + zend_string *key = keyEntry.stringKeyOrNull(); + if (key != NULL) { + unsetKey(names, key); + } + } + } + } + return unionOf(next, names); + } + } + if (kind == PT_VLR_LOOP) { + zval *canRepeat = slot(flow, slots.controlCanRepeat, "canRepeat"); + zval *canExit = slot(flow, slots.controlCanExit, "canExit"); + zval *atLeastOnce = slot(flow, slots.controlAtLeastOnce, "atLeastOnce"); + if (UNEXPECTED(canRepeat == NULL || canExit == NULL || atLeastOnce == NULL)) return zv::Val(); + zval *condition = childTable != NULL ? childAt(childTable, 0) : NULL; + zval *bodyFlow = childTable != NULL ? childAt(childTable, 1) : NULL; + zval *updateFlow = childTable != NULL ? childAt(childTable, 2) : NULL; + zv::Val head = emptyArray(); + zv::Val body; + uint32_t previousCount; + do { + previousCount = countOf(head); + zv::Val update = liveBefore(updateFlow, shareArray(head), context); + if (UNEXPECTED(update.isUndef())) return zv::Val(); + Context loopContext; + loopContext.return_ = shareArray(context.return_); + loopContext.breaks = Context::prepend(next, context.breaks); + loopContext.continues = Context::prepend(update, context.continues); + loopContext.catches = Context::shareCatches(context.catches); + loopContext.uncaught = shareArray(context.uncaught); + body = liveBefore(bodyFlow, shareArray(update), loopContext); + if (UNEXPECTED(body.isUndef())) return zv::Val(); + zv::Val afterCondition = zend_is_true(canRepeat) + ? (zend_is_true(canExit) ? unionOf(body, next) : shareArray(body)) + : shareArray(next); + head = liveBefore(condition, std::move(afterCondition), context); + if (UNEXPECTED(head.isUndef())) return zv::Val(); + } while (countOf(head) != previousCount); + + return zend_is_true(atLeastOnce) ? liveBefore(condition, std::move(body), context) : std::move(head); + } + if (kind == PT_VLR_SWITCH) { + zval *canExit = slot(flow, slots.controlCanExit, "canExit"); + zval *cases = slot(flow, slots.controlCases, "cases"); + if (UNEXPECTED(canExit == NULL || cases == NULL)) return zv::Val(); + HashTable *caseTable = Z_TYPE_P(cases) == IS_ARRAY ? Z_ARRVAL_P(cases) : NULL; + zend_long caseCount = caseTable != NULL ? (zend_long) zend_hash_num_elements(caseTable) : 0; + Context switchContext; + switchContext.return_ = shareArray(context.return_); + switchContext.breaks = Context::prepend(next, context.breaks); + switchContext.continues = Context::prepend(next, context.continues); + switchContext.catches = Context::shareCatches(context.catches); + switchContext.uncaught = shareArray(context.uncaught); + std::vector entries((size_t) caseCount); + zv::Val caseNext = shareArray(next); + zv::Val unmatched = zend_is_true(canExit) ? shareArray(next) : emptyArray(); + for (zend_long i = caseCount - 1; i >= 0; i--) { + zval *caseEntry = zend_hash_index_find(caseTable, (zend_ulong) i); + HashTable *caseParts = caseEntry != NULL && Z_TYPE_P(caseEntry) == IS_ARRAY ? Z_ARRVAL_P(caseEntry) : NULL; + zval *bodyFlow = caseParts != NULL ? childAt(caseParts, 1) : NULL; + zval *isDefault = caseParts != NULL ? zend_hash_index_find(caseParts, 2) : NULL; + caseNext = liveBefore(bodyFlow, std::move(caseNext), switchContext); + if (UNEXPECTED(caseNext.isUndef())) return zv::Val(); + entries[(size_t) i] = shareArray(caseNext); + if (isDefault == NULL || !zend_is_true(isDefault)) continue; + + unmatched = shareArray(caseNext); + } + for (zend_long i = caseCount - 1; i >= 0; i--) { + zval *caseEntry = zend_hash_index_find(caseTable, (zend_ulong) i); + HashTable *caseParts = caseEntry != NULL && Z_TYPE_P(caseEntry) == IS_ARRAY ? Z_ARRVAL_P(caseEntry) : NULL; + zval *condition = caseParts != NULL ? childAt(caseParts, 0) : NULL; + zval *isDefault = caseParts != NULL ? zend_hash_index_find(caseParts, 2) : NULL; + if (isDefault != NULL && zend_is_true(isDefault)) continue; + unmatched = liveBefore(condition, unionOf(entries[(size_t) i], unmatched), context); + if (UNEXPECTED(unmatched.isUndef())) return zv::Val(); + } + return liveBefore(childTable != NULL ? childAt(childTable, 0) : NULL, std::move(unmatched), context); + } + if (kind == PT_VLR_RETURN) return shareArray(context.return_); + if (kind == PT_VLR_BREAK || kind == PT_VLR_CONTINUE) { + zval *level = slot(flow, slots.controlLevel, "level"); + if (UNEXPECTED(level == NULL)) return zv::Val(); + const std::vector &destinations = kind == PT_VLR_BREAK ? context.breaks : context.continues; + zend_long index = zval_get_long(level) - 1; + if (index < 0 || (size_t) index >= destinations.size()) return emptyArray(); + return shareArray(destinations[(size_t) index]); + } + if (kind == PT_VLR_STOP) return emptyArray(); + if (kind == PT_VLR_THROW) { + zval *canExit = slot(flow, slots.controlCanExit, "canExit"); + zval *type = slot(flow, slots.controlType, "type"); + zval *canContainAnyThrowable = slot(flow, slots.controlCanContainAnyThrowable, "canContainAnyThrowable"); + if (UNEXPECTED(canExit == NULL || type == NULL || canContainAnyThrowable == NULL)) return zv::Val(); + zv::Val names = zend_is_true(canExit) ? std::move(next) : emptyArray(); + if (context.catches.empty()) return unionOf(names, context.uncaught); + if (Z_TYPE_P(type) != IS_OBJECT) { + pt_throw_should_not_happen(); + return zv::Val(); + } + if (zend_is_true(canContainAnyThrowable)) { + zv::Val throwable; + { + zval out; + zend_string *throwableName = zend_string_init(PT_LC("Throwable"), 0); + bool created = pt_object_type_new(&out, throwableName); + zend_string_release(throwableName); + if (UNEXPECTED(!created)) return zv::Val(); + throwable = zv::Val::adopt(out); + } + for (const auto &entry : context.catches) { + zend_long accepts = isSuperTypeOf(entry.first, throwable.raw()); + if (UNEXPECTED(accepts < 0)) return zv::Val(); + if (accepts != PT_TRI_YES) continue; + names = unionOf(names, entry.second); + break; + } + } + for (const auto &entry : context.catches) { + zend_long accepts = isSuperTypeOf(entry.first, type); + if (UNEXPECTED(accepts < 0)) return zv::Val(); + bool destinationReached = accepts != PT_TRI_NO; + if (!destinationReached) { + zend_long reverse = isSuperTypeOf(type, entry.first); + if (UNEXPECTED(reverse < 0)) return zv::Val(); + destinationReached = reverse != PT_TRI_NO; + } + if (destinationReached) { + names = unionOf(names, entry.second); + } + if (accepts == PT_TRI_YES) return names; + } + return unionOf(names, context.uncaught); + } + if (kind == PT_VLR_TRY_CATCH) { + zval *finally = childTable != NULL ? childAt(childTable, 1) : NULL; + zv::Val normal = liveBefore(finally, std::move(next), context); + if (UNEXPECTED(normal.isUndef())) return zv::Val(); + std::vector breaks; + breaks.reserve(context.breaks.size()); + for (const zv::Val &destination : context.breaks) { + zv::Val live = liveBefore(finally, shareArray(destination), context); + if (UNEXPECTED(live.isUndef())) return zv::Val(); + breaks.push_back(std::move(live)); + } + std::vector continues; + continues.reserve(context.continues.size()); + for (const zv::Val &destination : context.continues) { + zv::Val live = liveBefore(finally, shareArray(destination), context); + if (UNEXPECTED(live.isUndef())) return zv::Val(); + continues.push_back(std::move(live)); + } + std::vector> outerCatches; + outerCatches.reserve(context.catches.size()); + for (const auto &entry : context.catches) { + zv::Val live = liveBefore(finally, shareArray(entry.second), context); + if (UNEXPECTED(live.isUndef())) return zv::Val(); + outerCatches.emplace_back(entry.first, std::move(live)); + } + Context catchContext; + catchContext.return_ = liveBefore(finally, shareArray(context.return_), context); + if (UNEXPECTED(catchContext.return_.isUndef())) return zv::Val(); + catchContext.breaks = Context::shareAll(breaks); + catchContext.continues = Context::shareAll(continues); + catchContext.catches = Context::shareCatches(outerCatches); + catchContext.uncaught = liveBefore(finally, shareArray(context.uncaught), context); + if (UNEXPECTED(catchContext.uncaught.isUndef())) return zv::Val(); + zval *catches = slot(flow, slots.controlCatches, "catches"); + if (UNEXPECTED(catches == NULL)) return zv::Val(); + std::vector> allCatches; + if (Z_TYPE_P(catches) == IS_ARRAY) { + for (auto entry : zv::TableRef(Z_ARRVAL_P(catches))) { + zv::Ref catchEntry = entry.value().deref(); + if (!catchEntry.isArray()) continue; + zval *catchType = zend_hash_index_find(catchEntry.asArrayTable(), 0); + zval *catchFlow = childAt(catchEntry.asArrayTable(), 1); + if (UNEXPECTED(catchType == NULL)) continue; + ZVAL_DEREF(catchType); + zv::Val live = liveBefore(catchFlow, shareArray(normal), catchContext); + if (UNEXPECTED(live.isUndef())) return zv::Val(); + allCatches.emplace_back(catchType, std::move(live)); + } + } + for (auto &entry : outerCatches) { + allCatches.emplace_back(entry.first, std::move(entry.second)); + } + Context bodyContext; + bodyContext.return_ = shareArray(catchContext.return_); + bodyContext.breaks = std::move(breaks); + bodyContext.continues = std::move(continues); + bodyContext.catches = std::move(allCatches); + bodyContext.uncaught = shareArray(catchContext.uncaught); + return liveBefore(childTable != NULL ? childAt(childTable, 0) : NULL, std::move(normal), bodyContext); + } + if (kind == PT_VLR_READ_ALL) { + readNames = unionOf(readNames, mentionedNames); + return unionOf(next, allReadKeys); + } + return next; + } + + /* $a->isSuperTypeOf($b)->result as a PT_TRI_* value; -1 = pending exception */ + [[nodiscard]] static zend_long isSuperTypeOf(zval *a, zval *b) + { + if (UNEXPECTED(Z_TYPE_P(a) != IS_OBJECT || Z_TYPE_P(b) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: expected a Type in a catch clause"); + return -1; + } + zv::Val result = pt_type_op(Z_OBJ_P(a), PT_OP_IS_SUPER_TYPE_OF, 1, b); + if (UNEXPECTED(result.isUndef())) return -1; + if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_OBJECT)) { + zend_type_error("phpstan_turbo: isSuperTypeOf() did not return a result object"); + return -1; + } + return pt_result_value(Z_OBJ_P(result.raw())); + } + + /* + * Mirrors bindingProbe(): sprintf("\0%s\0%d%s", name, id, armed ? "\0" : "") + * — a key no read can produce. + */ + static zv::Str bindingProbe(const WriteView &binding, bool armed) + { + zend_string *name = binding.nameStr; + smart_str probe = { NULL, 0 }; + smart_str_appendc(&probe, '\0'); + smart_str_append(&probe, name); + smart_str_appendc(&probe, '\0'); + smart_str_append_long(&probe, binding.id); + if (armed) { + smart_str_appendc(&probe, '\0'); + } + smart_str_0(&probe); + return zv::Str::adopt(probe.s); + } + + /* Mirrors passBindingProbes(); $write NULL for null; UNDEF = pending exception. */ + zv::Val passBindingProbes(zv::Val next, zend_string *name, const WriteView *write, bool discard) + { + /* sprintf("\0%s\0", $name) */ + size_t prefixLen = ZSTR_LEN(name) + 2; + zv::Str prefixStr = zv::Str::adopt(zend_string_alloc(prefixLen, 0)); + char *prefix = ZSTR_VAL(prefixStr.get()); + prefix[0] = '\0'; + memcpy(prefix + 1, ZSTR_VAL(name), ZSTR_LEN(name)); + prefix[prefixLen - 1] = '\0'; + prefix[prefixLen] = '\0'; + /* array_keys($next) snapshots the keys: the table walked is the one + * before any removal (a removal separates a shared table and marks a + * bucket of an unshared one — the walk sees every original key + * either way) */ + HashTable *keys = tableOf(next); + for (auto entry : zv::TableRef(keys)) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL || ZSTR_LEN(key) < prefixLen || memcmp(ZSTR_VAL(key), prefix, prefixLen) != 0) continue; + const char *id = ZSTR_VAL(key) + prefixLen; + size_t idLen = ZSTR_LEN(key) - prefixLen; + bool armed = idLen > 0 && id[idLen - 1] == '\0'; + zend_long bindingId = ZEND_STRTOL(id, NULL, 10); + if (write != NULL) { + HashTable *ownIds = innerTable(ownWriteIds, bindingId); + if (ownIds != NULL && zend_hash_index_exists(ownIds, (zend_ulong) write->id)) continue; + } + if (armed && !discard) { + zval *statement = zend_hash_index_find(tableOf(loopStatements), (zend_ulong) bindingId); + if (statement != NULL) { + setIndex(variableOverwritingLoops, bindingId, statement); + } else { + /* the twin reads an undefined offset here: a warning and null */ + zend_error(E_WARNING, "Undefined array key " ZEND_LONG_FMT, bindingId); + zval nullValue; + ZVAL_NULL(&nullValue); + setIndex(variableOverwritingLoops, bindingId, &nullValue); + } + } + if (write == NULL || write->offsetWrite) { + // an alias or an offset write keeps the variable - the probe + // carries on to the assignment that created it + continue; + } + zv::Str held = zv::Str::copyOf(key); + unsetKey(next, held.get()); + } + + return next; + } + + /* Mirrors offsetKey(): (is_int($offset) ? 'i:' : 's:') . $offset */ + static zv::Str offsetKey(zval *offset) + { + smart_str key = { NULL, 0 }; + if (Z_TYPE_P(offset) == IS_LONG) { + smart_str_appendl(&key, "i:", 2); + smart_str_append_long(&key, Z_LVAL_P(offset)); + } else { + smart_str_appendl(&key, "s:", 2); + zend_string *str = zval_get_string(offset); + smart_str_append(&key, str); + zend_string_release(str); + } + smart_str_0(&key); + return zv::Str::adopt(key.s); + } + + /* $name . "\0" . $slot . "\0" . $targetId */ + static zv::Str accessKey(zend_string *name, zend_string *slotName, zend_long targetId) + { + smart_str key = { NULL, 0 }; + smart_str_append(&key, name); + smart_str_appendc(&key, '\0'); + smart_str_append(&key, slotName); + smart_str_appendc(&key, '\0'); + smart_str_append_long(&key, targetId); + smart_str_0(&key); + return zv::Str::adopt(key.s); + } + + /* Mirrors compileAccesses(); false = pending exception. */ + [[nodiscard]] bool compileAccesses() + { + zend_string *containerSlot = zend_string_init(PT_LC("container"), 0); + zend_string *unknownSlot = zend_string_init(PT_LC("unknown"), 0); + bool ok = compileAccessesWith(containerSlot, unknownSlot); + zend_string_release(containerSlot); + zend_string_release(unknownSlot); + return ok; + } + + bool compileAccessesWith(zend_string *containerSlot, zend_string *unknownSlot) + { + for (auto nameEntry : zv::TableRef(tableOf(accesses))) { + zend_string *name = nameEntry.stringKeyOrNull(); + zv::Ref accessList = nameEntry.value().deref(); + if (UNEXPECTED(name == NULL || !accessList.isArray())) continue; + HashTable *accessTable = accessList.asArrayTable(); + /* $slots = ['container' => true, 'unknown' => true] + every offset */ + zv::Val slotSet = emptyArray(); + setTrue(slotSet, containerSlot); + setTrue(slotSet, unknownSlot); + for (auto entry : zv::TableRef(accessTable)) { + zend_object *access = Z_OBJ_P(entry.value().deref().raw()); + zval *write = slot(access, slots.accessWrite, "write"); + if (UNEXPECTED(write == NULL)) return false; + zv::Val offset; + if (Z_TYPE_P(write) == IS_OBJECT) { + WriteView view; + if (UNEXPECTED(!view.load(write))) return false; + offset = std::move(view.offset); + } else { + zval *accessOffset = slot(access, slots.accessOffset, "offset"); + if (UNEXPECTED(accessOffset == NULL)) return false; + offset = zv::Val::copyOf(zv::Ref(accessOffset)); + } + if (Z_TYPE_P(offset.raw()) == IS_NULL) continue; + + zv::Str key = offsetKey(offset.raw()); + setTrue(slotSet, key.get()); + } + zv::Val keysBySlot = emptyArray(); + for (auto entry : zv::TableRef(accessTable)) { + zend_object *access = Z_OBJ_P(entry.value().deref().raw()); + FlowKind kind = kindOfFlow(access, slots.accessKind); + if (UNEXPECTED(EG(exception))) return false; + if (kind != PT_VLR_READ && kind != PT_VLR_ESCAPE) continue; + zval *container = slot(access, slots.accessContainer, "container"); + zval *accessOffset = slot(access, slots.accessOffset, "offset"); + zval *targetId = slot(access, slots.accessTargetId, "targetId"); + if (UNEXPECTED(container == NULL || accessOffset == NULL || targetId == NULL)) return false; + zv::Val selected; + if (zend_is_true(container)) { + selected = emptyArray(); + setTrue(selected, containerSlot); + } else if (Z_TYPE_P(accessOffset) != IS_NULL) { + selected = emptyArray(); + setTrue(selected, containerSlot); + zv::Str key = offsetKey(accessOffset); + setTrue(selected, key.get()); + } else { + selected = shareArray(slotSet); + } + zend_long targetIdValue = Z_TYPE_P(targetId) == IS_NULL ? 0 : zval_get_long(targetId); + for (auto slotEntry : zv::TableRef(tableOf(selected))) { + zend_string *slotName = slotEntry.stringKeyOrNull(); + if (slotName == NULL) continue; + zv::Str key = accessKey(name, slotName, targetIdValue); + zval *bySlot = innerSlotByKey(keysBySlot, slotName); + Z_TRY_ADDREF_P(targetId); + zend_hash_update(Z_ARRVAL_P(bySlot), key.get(), targetId); + zval *byAccess = innerSlot(readKeys, (zend_long) access->handle); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_update(Z_ARRVAL_P(byAccess), key.get(), &trueValue); + zval *byName = innerSlotByKey(nameKeys, name); + zend_hash_update(Z_ARRVAL_P(byName), key.get(), &trueValue); + } + } + // A dynamic observation sees every offset, including ones never named by a read. + if (readsAllVariables) { + for (auto slotEntry : zv::TableRef(tableOf(slotSet))) { + zend_string *slotName = slotEntry.stringKeyOrNull(); + if (slotName == NULL) continue; + zv::Str key = accessKey(name, slotName, 0); + zval *bySlot = innerSlotByKey(keysBySlot, slotName); + zval nullValue; + ZVAL_NULL(&nullValue); + zend_hash_update(Z_ARRVAL_P(bySlot), key.get(), &nullValue); + setTrue(allReadKeys, key.get()); + zval *byName = innerSlotByKey(nameKeys, name); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_update(Z_ARRVAL_P(byName), key.get(), &trueValue); + } + } + for (auto entry : zv::TableRef(accessTable)) { + zend_object *access = Z_OBJ_P(entry.value().deref().raw()); + zval *write = slot(access, slots.accessWrite, "write"); + if (UNEXPECTED(write == NULL)) return false; + if (Z_TYPE_P(write) != IS_OBJECT) continue; + WriteView view; + if (UNEXPECTED(!view.load(write))) return false; + zend_long id = view.id; + bool offsetIsNull = view.offsetIsNull; + zv::Val selectedKeys; + if (view.offsetWrite && !offsetIsNull) { + zv::Str slotName = offsetKey(view.offset.raw()); + selectedKeys = emptyArray(); + HashTable *keys = innerTableByKey(keysBySlot, slotName.get()); + zval keysValue; + if (keys != NULL) { + ZVAL_ARR(&keysValue, keys); + Z_ADDREF(keysValue); + } else { + ZVAL_EMPTY_ARRAY(&keysValue); + } + SEPARATE_ARRAY(selectedKeys.raw()); + zend_hash_update(Z_ARRVAL_P(selectedKeys.raw()), slotName.get(), &keysValue); + } else { + selectedKeys = shareArray(keysBySlot); + if (view.offsetWrite) { + unsetKey(selectedKeys, containerSlot); + } + } + bool kills = !view.offsetWrite || (!offsetIsNull && view.replacesOffset); + for (auto slotEntry : zv::TableRef(tableOf(selectedKeys))) { + zv::Ref keys = slotEntry.value().deref(); + if (!keys.isArray()) continue; + for (auto keyEntry : zv::TableRef(keys.asArrayTable())) { + zend_string *key = keyEntry.stringKeyOrNull(); + if (key == NULL) continue; + zval *observed = innerSlot(observedKeys, id); + Z_TRY_ADDREF_P(keyEntry.value().raw()); + zend_hash_update(Z_ARRVAL_P(observed), key, keyEntry.value().raw()); + if (!kills) continue; + + zval *killed = innerSlot(killedKeys, id); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_update(Z_ARRVAL_P(killed), key, &trueValue); + } + } + } + } + return true; + } + + /* Mirrors observeWrite(). */ + void observeWrite(zend_long id, const zv::Val &next) + { + HashTable *observed = innerTable(observedKeys, id); + if (observed == NULL) return; + for (auto entry : zv::TableRef(observed)) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL || !isTrueAt(next, key)) continue; + setTrueIndex(observedIds, id); + zv::Ref targetId = entry.value().deref(); + if (targetId.isNull()) { + setTrueIndex(readIds, id); + } else { + zval *dependents = innerSlot(dependencies, targetId.toLong()); + zval trueValue; + ZVAL_TRUE(&trueValue); + zend_hash_index_update(Z_ARRVAL_P(dependents), (zend_ulong) id, &trueValue); + } + } + } + + /* Mirrors resolveDependencies(); false = pending exception. */ + [[nodiscard]] bool resolveDependencies() + { + std::vector> stack; + for (auto entry : zv::TableRef(tableOf(readIds))) { + stack.emplace_back((zend_long) entry.indexKey(), false); + } + for (auto entry : zv::TableRef(tableOf(inputSinks))) { + stack.emplace_back((zend_long) entry.indexKey(), true); + } + for (auto entry : zv::TableRef(tableOf(writes))) { + zv::Val name = writeName(entry.value().deref().raw()); + if (UNEXPECTED(name.isUndef())) return false; + if (Z_TYPE_P(name.raw()) != IS_STRING || !isTrueAt(escapedNames, Z_STR_P(name.raw()))) continue; + // a write to an aliased variable is observable through the alias, + // so whatever flows into it is used; whether the write itself is + // read stays with the flow-sensitive capture read + stack.emplace_back((zend_long) entry.indexKey(), false); + stack.emplace_back((zend_long) entry.indexKey(), true); + } + /* $visited["$id:inputs"] / ["$id:value"] as (id, inputs) pairs */ + zv::ScratchTable visited(16); + while (!stack.empty()) { + std::pair top = stack.back(); + stack.pop_back(); + zend_long id = top.first; + bool inputs = top.second; + zend_ulong visitedKey = ((zend_ulong) id << 1) | (inputs ? 1 : 0); + if (zend_hash_index_exists(visited.table(), visitedKey)) continue; + zval marker; + ZVAL_TRUE(&marker); + zend_hash_index_add_new(visited.table(), visitedKey, &marker); + HashTable *dependents = innerTable(dependencies, id); + if (dependents != NULL) { + for (auto entry : zv::TableRef(dependents)) { + setTrueIndex(readIds, (zend_long) entry.indexKey()); + stack.emplace_back((zend_long) entry.indexKey(), false); + } + } + HashTable *sources = innerTable(inputCopies, id); + if (sources != NULL) { + for (auto entry : zv::TableRef(sources)) { + stack.emplace_back((zend_long) entry.indexKey(), true); + } + } + if (!inputs) continue; + HashTable *items = innerTable(literalItems, id); + if (items != NULL) { + for (auto entry : zv::TableRef(items)) { + zend_long itemId; + if (UNEXPECTED(!writeId(entry.value().deref().raw(), itemId))) return false; + stack.emplace_back(itemId, true); + } + } + } + return true; + } + + /* Mirrors resolveCoverage(). */ + void resolveCoverage() + { + std::vector stack; + for (auto entry : zv::TableRef(tableOf(writes))) { + zend_long id = (zend_long) entry.indexKey(); + if (isTrueAtIndex(observedIds, id) || isTrueAtIndex(readIds, id)) continue; + + stack.push_back(id); + } + zv::ScratchTable visited(16); + while (!stack.empty()) { + zend_long id = stack.back(); + stack.pop_back(); + if (zend_hash_index_exists(visited.table(), (zend_ulong) id)) continue; + zval marker; + ZVAL_TRUE(&marker); + zend_hash_index_add_new(visited.table(), (zend_ulong) id, &marker); + std::vector sources; + HashTable *dependents = innerTable(dependencies, id); + if (dependents != NULL) { + for (auto entry : zv::TableRef(dependents)) { + sources.push_back((zend_long) entry.indexKey()); + } + } + HashTable *copies = innerTable(inputCopies, id); + if (copies != NULL) { + for (auto entry : zv::TableRef(copies)) { + // the inputs of $copied flow into $id as well + HashTable *copiedDependents = innerTable(dependencies, (zend_long) entry.indexKey()); + if (copiedDependents == NULL) continue; + for (auto dependent : zv::TableRef(copiedDependents)) { + sources.push_back((zend_long) dependent.indexKey()); + } + } + } + for (zend_long source : sources) { + setTrueIndex(coveredIds, source); + stack.push_back(source); + } + } + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::VariableLivenessResolver; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +void pt_register_variable_liveness_resolver() +{ + reg::Class cls("PHPStan\\Analyser\\VariableLivenessResolver"); + ptdecl::VariableLivenessResolver::declareClass(cls); + + cls.method(sigs::resolve, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *function, *flow; + if (!zp::parse(execute_data, function, flow)) RETURN_THROWS(); + zv::Val result = VariableLivenessResolver::resolve(function, flow); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.shadow(&pt_ce_variable_liveness_resolver); +} + +/* }}} */ diff --git a/turbo-ext/src/VolatileExpressionHelper.cpp b/turbo-ext/src/VolatileExpressionHelper.cpp new file mode 100644 index 00000000000..4f778d90cc2 --- /dev/null +++ b/turbo-ext/src/VolatileExpressionHelper.cpp @@ -0,0 +1,381 @@ +/* + * PHPStanTurbo\VolatileExpressionHelper — native implementation of + * PHPStan\Analyser\VolatileExpressionHelper. + * + * The three static methods forget tracked entries of the two by-reference + * expression tables a MutatingScope hands in as copies of its own; like the + * twin's `unset($table[$key])` the tables are separated only when an entry + * is actually removed — the common call finds nothing tracked and must not + * duplicate the (large) tables. + */ + +#include "support.h" +#include "generated/VolatileExpressionHelper.h" + +namespace sigs = ptdecl::VolatileExpressionHelper::sig; +#include "zv.h" +#include "TypeTraits.h" +#include "TypeOps.h" + +#include + +zend_class_entry *pt_ce_volatile_expression_helper; + +namespace { + +/* the twin's private VOLATILE_FUNCTION_NAMES */ +const pt_superglobal_name pt_veh_volatile_function_names[] = { + { PT_LC("ob_get_level") }, + { PT_LC("openssl_error_string") }, +}; + +/* the twin's private EXISTENCE_CHECK_FUNCTION_NAMES */ +const pt_superglobal_name pt_veh_existence_check_function_names[] = { + { PT_LC("class_exists") }, + { PT_LC("interface_exists") }, + { PT_LC("trait_exists") }, + { PT_LC("enum_exists") }, + { PT_LC("function_exists") }, +}; + +/* the same lists as the twin's constants: persistent immutable arrays the + * engine references for the process lifetime */ +HashTable *pt_veh_persistent_list(const pt_superglobal_name *names, size_t count) +{ + HashTable *list = (HashTable *) pemalloc(sizeof(HashTable), 1); + zend_hash_init(list, (uint32_t) count, NULL, NULL, 1); + for (size_t i = 0; i < count; i++) { + zval value; + ZVAL_INTERNED_STR(&value, zend_string_init_interned(names[i].name, names[i].len, 1)); + zend_hash_next_index_insert(list, &value); + } + GC_ADD_FLAGS(list, IS_ARRAY_IMMUTABLE); + GC_SET_REFCOUNT(list, 2); + return list; +} + +void pt_veh_volatile_function_names_constant(zval *out) +{ + ZVAL_ARR(out, pt_veh_persistent_list(pt_veh_volatile_function_names, sizeof(pt_veh_volatile_function_names) / sizeof(pt_veh_volatile_function_names[0]))); + Z_TYPE_INFO_P(out) = IS_ARRAY; +} + +void pt_veh_existence_check_function_names_constant(zval *out) +{ + ZVAL_ARR(out, pt_veh_persistent_list(pt_veh_existence_check_function_names, sizeof(pt_veh_existence_check_function_names) / sizeof(pt_veh_existence_check_function_names[0]))); + Z_TYPE_INFO_P(out) = IS_ARRAY; +} + +/* unset($table[$key]) on the caller's array: separates a shared table + * first, exactly when the twin's unset would */ +void pt_veh_unset(zval *table, const char *key, size_t len) +{ + SEPARATE_ARRAY(table); + zend_hash_str_del(Z_ARRVAL_P(table), key, len); +} + +/* unset($expressionTypes[$key]); unset($nativeExpressionTypes[$key]) for the + * key of a bucket of one of the two tables: the first removal may release + * the last reference to that bucket's key string, so the key is held for the + * second */ +void pt_veh_unset_both(zval *expressionTypes, zval *nativeExpressionTypes, zend_string *key, zend_ulong idx) +{ + zv::Str held = key != NULL ? zv::Str::copyOf(key) : zv::Str(); + SEPARATE_ARRAY(expressionTypes); + pt_ht_del(Z_ARRVAL_P(expressionTypes), key, idx); + SEPARATE_ARRAY(nativeExpressionTypes); + pt_ht_del(Z_ARRVAL_P(nativeExpressionTypes), key, idx); +} + +/* a by-reference `array &$x` argument: the reference's inner array (the + * caller's variable), or NULL with the twin's TypeError pending */ +zval *pt_veh_array_ref(zval *arg, uint32_t argNum) +{ + ZVAL_DEREF(arg); + if (UNEXPECTED(Z_TYPE_P(arg) != IS_ARRAY)) { + zend_argument_type_error(argNum, "must be of type array, %s given", zend_zval_value_name(arg)); + return NULL; + } + return arg; +} + +/* ltrim(strtolower($name), '\\') as a borrowed byte range of an owned + * lowercase copy */ +zend_string *pt_veh_lower_symbol(zend_string *name, const char **start, size_t *len) +{ + zend_string *lower = zend_string_tolower(name); + const char *p = ZSTR_VAL(lower); + size_t n = ZSTR_LEN(lower); + while (n > 0 && *p == '\\') { + p++; + n--; + } + *start = p; + *len = n; + return lower; +} + +} // namespace + +namespace phpstanturbo { + +/* Mirrors PHPStan\Analyser\VolatileExpressionHelper. The table arguments + * are the by-reference arrays' inner zvals (never references). */ +class VolatileExpressionHelper +{ +public: + /* Mirrors invalidateVolatileFunctionCalls(). */ + static bool invalidateVolatileFunctionCalls(zval *expressionTypes, zval *nativeExpressionTypes) + { + bool changed = false; + char key[64]; + for (size_t i = 0; i < sizeof(pt_veh_volatile_function_names) / sizeof(pt_veh_volatile_function_names[0]); i++) { + const pt_superglobal_name &functionName = pt_veh_volatile_function_names[i]; + for (int leading = 0; leading < 2; leading++) { + size_t len = 0; + if (leading == 1) { + key[len++] = '\\'; + } + memcpy(key + len, functionName.name, functionName.len); + len += functionName.len; + key[len++] = '('; + key[len++] = ')'; + if (!zend_hash_str_exists(Z_ARRVAL_P(expressionTypes), key, len) + && !zend_hash_str_exists(Z_ARRVAL_P(nativeExpressionTypes), key, len)) { + continue; + } + + pt_veh_unset(expressionTypes, key, len); + pt_veh_unset(nativeExpressionTypes, key, len); + changed = true; + } + } + + return changed; + } + + /* Mirrors invalidateSuperglobals(). */ + static bool invalidateSuperglobals(zval *expressionTypes, zval *nativeExpressionTypes) + { + size_t superglobalCount; + const pt_superglobal_name *superglobals = pt_superglobal_names(&superglobalCount); + bool hasTrackedSuperglobal = false; + char variableString[16]; + for (size_t i = 0; i < superglobalCount; i++) { + variableString[0] = '$'; + memcpy(variableString + 1, superglobals[i].name, superglobals[i].len); + size_t len = superglobals[i].len + 1; + if (!zend_hash_str_exists(Z_ARRVAL_P(expressionTypes), variableString, len) + && !zend_hash_str_exists(Z_ARRVAL_P(nativeExpressionTypes), variableString, len)) { + continue; + } + + hasTrackedSuperglobal = true; + break; + } + + if (!hasTrackedSuperglobal) return false; + + /* the keys of $expressionTypes + $nativeExpressionTypes: both tables' + * keys, each table iterated as it was before any removal (a removal + * separates a shared table, and marks a bucket of an unshared one + * — the walk sees every original key either way) */ + bool changed = false; + zv::TableRef tables[2] = { zv::TableRef(Z_ARRVAL_P(expressionTypes)), zv::TableRef(Z_ARRVAL_P(nativeExpressionTypes)) }; + for (int t = 0; t < 2; t++) { + for (auto entry : tables[t]) { + zend_string *exprString = entry.stringKeyOrNull(); + if (exprString == NULL || !isSuperglobalExprString(exprString, superglobals, superglobalCount)) continue; + + pt_veh_unset_both(expressionTypes, nativeExpressionTypes, exprString, 0); + changed = true; + } + } + + return changed; + } + + /* + * Mirrors invalidateNegativeExistenceChecks(). $functionNames NULL stands + * for the twin's EXISTENCE_CHECK_FUNCTION_NAMES default, $declaredSymbolName + * NULL for null; UNDEF = pending exception. + */ + static zv::Val invalidateNegativeExistenceChecks(zval *scope, zval *expressionTypes, zval *nativeExpressionTypes, HashTable *functionNames, zend_string *declaredSymbolName) + { + zend_class_entry *funcCallCe = pt_class(PT_CLASS_FUNC_CALL); + zend_class_entry *nameCe = pt_class(PT_CLASS_NAME); + if (UNEXPECTED(funcCallCe == NULL || nameCe == NULL)) return zv::Val(); + + bool changed = false; + /* foreach by value walks the table as it was before any removal */ + for (auto entry : zv::TableRef(Z_ARRVAL_P(expressionTypes))) { + zv::Ref exprTypeHolder = entry.value().deref(); + if (UNEXPECTED(!pt_check_holder(exprTypeHolder.raw()))) return zv::Val(); + zend_object *expr = zv::ObjRef(exprTypeHolder.asObject()).propAt(PT_ETH_PROP_EXPR).asObject(); + + if (!instanceof_function(expr->ce, funcCallCe)) continue; + bool isFirstClassCallable; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, isFirstClassCallable))) return zv::Val(); + if (isFirstClassCallable) continue; + zv::Ref name = zv::ObjRef(expr).prop(PT_LC("name")); + if (UNEXPECTED(name.raw() == NULL)) continue; + name = name.deref(); + if (!name.instanceOf(nameCe)) continue; + /* $expr->name->toLowerString() */ + zv::Ref nameString = zv::ObjRef(name.asObject()).prop(PT_LC("name")); + if (UNEXPECTED(nameString.raw() == NULL)) continue; + nameString = nameString.deref(); + if (!nameString.isString()) continue; + zv::Str lowerName = zv::Str::adopt(zend_string_tolower(nameString.asString())); + if (!inFunctionNames(lowerName.get(), functionNames)) continue; + /* $exprTypeHolder->getType()->isTrue()->yes() */ + zend_long isTrue = pt_type_call_trinary(zv::ObjRef(exprTypeHolder.asObject()).propAt(PT_ETH_PROP_TYPE).asObject(), PT_LC("istrue"), 0, NULL); + if (UNEXPECTED(isTrue < 0)) return zv::Val(); + if (isTrue == PT_TRI_YES) continue; + + /* keep a check whose single constant-string argument names a symbol + * other than the declared one */ + zv::Ref args = declaredSymbolName != NULL ? zv::ObjRef(expr).prop(PT_LC("args")) : zv::Ref(NULL); + if (args.raw() != NULL && args.deref().isArray()) { + zval *firstArg = zend_hash_index_find(args.deref().asArrayTable(), 0); + if (firstArg != NULL && Z_TYPE_P(firstArg) != IS_NULL) { + bool keep = false; + if (UNEXPECTED(!namesAnotherSymbol(scope, zv::Ref(firstArg).deref(), declaredSymbolName, keep))) return zv::Val(); + if (keep) continue; + } + } + + pt_veh_unset_both(expressionTypes, nativeExpressionTypes, entry.stringKeyOrNull(), entry.indexKey()); + changed = true; + } + + return zv::Val::boolean(changed); + } + +private: + /* $exprString === '$' . $name || str_starts_with($exprString, '$' . $name . '[') */ + static bool isSuperglobalExprString(zend_string *exprString, const pt_superglobal_name *superglobals, size_t superglobalCount) + { + const char *s = ZSTR_VAL(exprString); + size_t len = ZSTR_LEN(exprString); + if (len < 2 || s[0] != '$') return false; + for (size_t i = 0; i < superglobalCount; i++) { + size_t nameLen = superglobals[i].len; + if (len < nameLen + 1 || memcmp(s + 1, superglobals[i].name, nameLen) != 0) continue; + if (len == nameLen + 1 || s[nameLen + 1] == '[') return true; + } + return false; + } + + /* in_array($lowerName, $functionNames, true) — the default list when + * $functionNames is NULL */ + static bool inFunctionNames(zend_string *lowerName, HashTable *functionNames) + { + if (functionNames == NULL) { + return pt_veh_in_list(lowerName, pt_veh_existence_check_function_names, sizeof(pt_veh_existence_check_function_names) / sizeof(pt_veh_existence_check_function_names[0])); + } + for (auto entry : zv::TableRef(functionNames)) { + zv::Ref value = entry.value().deref(); + if (value.isString() && zend_string_equals(value.asString(), lowerName)) return true; + } + return false; + } + + static bool pt_veh_in_list(zend_string *name, const pt_superglobal_name *names, size_t count) + { + for (size_t i = 0; i < count; i++) { + if (ZSTR_LEN(name) == names[i].len && memcmp(ZSTR_VAL(name), names[i].name, names[i].len) == 0) return true; + } + return false; + } + + /* + * The twin's keep condition: $scope->getType($arg->value) has exactly one + * constant string, and it names (case-insensitively, leading backslashes + * aside) a symbol other than $declaredSymbolName; false = pending + * exception. + */ + [[nodiscard]] static bool namesAnotherSymbol(zval *scope, zv::Ref arg, zend_string *declaredSymbolName, bool &keep) + { + keep = false; + if (!arg.isObject()) return true; + zv::Ref argValue = zv::ObjRef(arg.asObject()).prop(PT_LC("value")); + if (argValue.raw() == NULL) return true; + argValue = argValue.deref(); + if (!argValue.isObject()) return true; + zv::Val type = pt_type_call(Z_OBJ_P(scope), PT_LC("gettype"), 1, argValue.raw()); + if (UNEXPECTED(type.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(type.raw()) != IS_OBJECT)) return true; + zv::Val constantStrings = pt_type_call(Z_OBJ_P(type.raw()), PT_LC("getconstantstrings"), 0, NULL); + if (UNEXPECTED(constantStrings.isUndef())) return false; + if (Z_TYPE_P(constantStrings.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(constantStrings.raw())) != 1) return true; + zval *constantString = zend_hash_index_find(Z_ARRVAL_P(constantStrings.raw()), 0); + if (constantString == NULL || Z_TYPE_P(constantString) != IS_OBJECT) return true; + zv::Val value = pt_type_op(Z_OBJ_P(constantString), PT_OP_GET_VALUE, 0, NULL); + if (UNEXPECTED(value.isUndef())) return false; + if (Z_TYPE_P(value.raw()) != IS_STRING) return true; + const char *a; + size_t aLen; + zv::Str lowerValue = zv::Str::adopt(pt_veh_lower_symbol(Z_STR_P(value.raw()), &a, &aLen)); + const char *b; + size_t bLen; + zv::Str lowerDeclared = zv::Str::adopt(pt_veh_lower_symbol(declaredSymbolName, &b, &bLen)); + keep = aLen != bLen || memcmp(a, b, aLen) != 0; + return true; + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::VolatileExpressionHelper; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +void pt_register_volatile_expression_helper() +{ + reg::Class cls("PHPStan\\Analyser\\VolatileExpressionHelper"); + ptdecl::VolatileExpressionHelper::declareClass(cls); + ptdecl::VolatileExpressionHelper::declareProperties(cls); + cls.privateClassConstantValue("VOLATILE_FUNCTION_NAMES", pt_veh_volatile_function_names_constant); + cls.privateClassConstantValue("EXISTENCE_CHECK_FUNCTION_NAMES", pt_veh_existence_check_function_names_constant); + + cls.method(sigs::invalidateVolatileFunctionCalls, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expressionTypes, *nativeExpressionTypes; + if (!zp::parse(execute_data, expressionTypes, nativeExpressionTypes)) RETURN_THROWS(); + expressionTypes = pt_veh_array_ref(expressionTypes, 1); + if (UNEXPECTED(expressionTypes == NULL)) RETURN_THROWS(); + nativeExpressionTypes = pt_veh_array_ref(nativeExpressionTypes, 2); + if (UNEXPECTED(nativeExpressionTypes == NULL)) RETURN_THROWS(); + RETURN_BOOL(VolatileExpressionHelper::invalidateVolatileFunctionCalls(expressionTypes, nativeExpressionTypes)); + }); + + cls.method(sigs::invalidateSuperglobals, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expressionTypes, *nativeExpressionTypes; + if (!zp::parse(execute_data, expressionTypes, nativeExpressionTypes)) RETURN_THROWS(); + expressionTypes = pt_veh_array_ref(expressionTypes, 1); + if (UNEXPECTED(expressionTypes == NULL)) RETURN_THROWS(); + nativeExpressionTypes = pt_veh_array_ref(nativeExpressionTypes, 2); + if (UNEXPECTED(nativeExpressionTypes == NULL)) RETURN_THROWS(); + RETURN_BOOL(VolatileExpressionHelper::invalidateSuperglobals(expressionTypes, nativeExpressionTypes)); + }); + + cls.method(sigs::invalidateNegativeExistenceChecks, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *expressionTypes, *nativeExpressionTypes; + HashTable *functionNames = NULL; + zend_string *declaredSymbolName = NULL; + if (!zp::parse, zp::Opt>(execute_data, scope, expressionTypes, nativeExpressionTypes, functionNames, declaredSymbolName)) RETURN_THROWS(); + expressionTypes = pt_veh_array_ref(expressionTypes, 2); + if (UNEXPECTED(expressionTypes == NULL)) RETURN_THROWS(); + nativeExpressionTypes = pt_veh_array_ref(nativeExpressionTypes, 3); + if (UNEXPECTED(nativeExpressionTypes == NULL)) RETURN_THROWS(); + zv::Val result = VolatileExpressionHelper::invalidateNegativeExistenceChecks(scope, expressionTypes, nativeExpressionTypes, functionNames, declaredSymbolName); + if (UNEXPECTED(result.isUndef())) RETURN_THROWS(); + result.intoReturnValue(return_value); + }); + + cls.shadow(&pt_ce_volatile_expression_helper); +} + +/* }}} */ diff --git a/turbo-ext/src/generated/ClassReflection.h b/turbo-ext/src/generated/ClassReflection.h new file mode 100644 index 00000000000..188549312b1 --- /dev/null +++ b/turbo-ext/src/generated/ClassReflection.h @@ -0,0 +1,407 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Reflection/ClassReflection.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_CLASS_REFLECTION_H +#define PHPSTANTURBO_GENERATED_CLASS_REFLECTION_H + +#include "../reg.h" + +namespace ptdecl::ClassReflection { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t methods = 0; +inline constexpr uint32_t properties = 1; +inline constexpr uint32_t instanceProperties = 2; +inline constexpr uint32_t staticProperties = 3; +inline constexpr uint32_t constants = 4; +inline constexpr uint32_t enumCases = 5; +inline constexpr uint32_t classHierarchyDistances = 6; +inline constexpr uint32_t deprecatedDescription = 7; +inline constexpr uint32_t isDeprecated = 8; +inline constexpr uint32_t allowedSubTypes = 9; +inline constexpr uint32_t allowedSubTypesResolved = 10; +inline constexpr uint32_t isGeneric = 11; +inline constexpr uint32_t isInternal = 12; +inline constexpr uint32_t isFinal = 13; +inline constexpr uint32_t isImmutable = 14; +inline constexpr uint32_t hasConsistentConstructor = 15; +inline constexpr uint32_t acceptsNamedArguments = 16; +inline constexpr uint32_t templateTypeMap = 17; +inline constexpr uint32_t activeTemplateTypeMap = 18; +inline constexpr uint32_t defaultCallSiteVarianceMap = 19; +inline constexpr uint32_t callSiteVarianceMap = 20; +inline constexpr uint32_t ancestors = 21; +inline constexpr uint32_t cacheKey = 22; +inline constexpr uint32_t subclasses = 23; +inline constexpr uint32_t filename = 24; +inline constexpr uint32_t reflectionDocComment = 25; +inline constexpr uint32_t stubPhpDocBlock = 26; +inline constexpr uint32_t resolvedPhpDocBlock = 27; +inline constexpr uint32_t traitContextResolvedPhpDocBlock = 28; +inline constexpr uint32_t cachedInterfaces = 29; +inline constexpr uint32_t cachedParentClass = 30; +inline constexpr uint32_t circularParentClassName = 31; +inline constexpr uint32_t typeAliases = 32; +inline constexpr uint32_t hasMethodCache = 33; +inline constexpr uint32_t hasPropertyCache = 34; +inline constexpr uint32_t hasInstancePropertyCache = 35; +inline constexpr uint32_t hasStaticPropertyCache = 36; +inline constexpr uint32_t name = 37; +inline constexpr uint32_t classReflectionFactory = 38; +inline constexpr uint32_t reflectionProvider = 39; +inline constexpr uint32_t initializerExprTypeResolver = 40; +inline constexpr uint32_t fileTypeMapper = 41; +inline constexpr uint32_t stubPhpDocProvider = 42; +inline constexpr uint32_t phpDocInheritanceResolver = 43; +inline constexpr uint32_t phpVersion = 44; +inline constexpr uint32_t signatureMapProvider = 45; +inline constexpr uint32_t deprecationProvider = 46; +inline constexpr uint32_t attributeReflectionFactory = 47; +inline constexpr uint32_t classReflectionExtensionRegistryProvider = 48; +inline constexpr uint32_t displayName = 49; +inline constexpr uint32_t reflection = 50; +inline constexpr uint32_t anonymousFilename = 51; +inline constexpr uint32_t resolvedTemplateTypeMap = 52; +inline constexpr uint32_t stubPhpDocBlockCallback = 53; +inline constexpr uint32_t extraCacheKey = 54; +inline constexpr uint32_t resolvedCallSiteVarianceMap = 55; +inline constexpr uint32_t finalByKeywordOverride = 56; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("methods", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("properties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("instanceProperties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("staticProperties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("constants", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("enumCases", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); + cls.property("classHierarchyDistances", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); + cls.property("deprecatedDescription", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_STRING); + cls.property("isDeprecated", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("allowedSubTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); + cls.property("allowedSubTypesResolved", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); + cls.property("isGeneric", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("isInternal", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("isFinal", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("isImmutable", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("hasConsistentConstructor", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("acceptsNamedArguments", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_BOOL); + cls.property("templateTypeMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeMap"); + cls.property("activeTemplateTypeMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeMap"); + cls.property("defaultCallSiteVarianceMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"); + cls.property("callSiteVarianceMap", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"); + cls.property("ancestors", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); + cls.property("cacheKey", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_STRING); + cls.property("subclasses", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("filename", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_NULL | MAY_BE_FALSE | MAY_BE_STRING); + cls.property("reflectionDocComment", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_NULL | MAY_BE_FALSE | MAY_BE_STRING); + cls.property("stubPhpDocBlock", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_NULL | MAY_BE_FALSE, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); + cls.property("resolvedPhpDocBlock", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_FALSE, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); + cls.property("traitContextResolvedPhpDocBlock", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_FALSE, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); + cls.property("cachedInterfaces", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); + cls.property("cachedParentClass", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_NULL | MAY_BE_FALSE, "PHPStan\\Reflection\\ClassReflection"); + cls.property("circularParentClassName", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedFalse, MAY_BE_NULL | MAY_BE_FALSE | MAY_BE_STRING); + cls.property("typeAliases", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_ARRAY); + cls.property("resolvingTypeAliasImports", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasMethodCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasPropertyCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasInstancePropertyCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("hasStaticPropertyCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("name", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL | MAY_BE_STRING); + cls.property("classReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\ClassReflectionFactory"); + cls.property("reflectionProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\ReflectionProvider"); + cls.property("initializerExprTypeResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\InitializerExprTypeResolver"); + cls.property("fileTypeMapper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Type\\FileTypeMapper"); + cls.property("stubPhpDocProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\PhpDoc\\StubPhpDocProvider"); + cls.property("phpDocInheritanceResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\PhpDoc\\PhpDocInheritanceResolver"); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Php\\PhpVersion"); + cls.property("signatureMapProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\SignatureMap\\SignatureMapProvider"); + cls.property("deprecationProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\Deprecation\\DeprecationProvider"); + cls.property("attributeReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\AttributeReflectionFactory"); + cls.property("classReflectionExtensionRegistryProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\DependencyInjection\\Reflection\\ClassReflectionExtensionRegistryProvider"); + cls.property("displayName", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING); + cls.property("reflection", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "ReflectionClass"); + cls.property("anonymousFilename", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_STRING); + cls.property("resolvedTemplateTypeMap", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeMap"); + cls.property("stubPhpDocBlockCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "Closure"); + cls.property("extraCacheKey", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_STRING); + cls.property("resolvedCallSiteVarianceMap", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"); + cls.property("finalByKeywordOverride", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_BOOL); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg __construct_args[] = { reg::typed("classReflectionFactory", 0, "PHPStan\\Reflection\\ClassReflectionFactory"), reg::typed("reflectionProvider", 0, "PHPStan\\Reflection\\ReflectionProvider"), reg::typed("initializerExprTypeResolver", 0, "PHPStan\\Reflection\\InitializerExprTypeResolver"), reg::typed("fileTypeMapper", 0, "PHPStan\\Type\\FileTypeMapper"), reg::typed("stubPhpDocProvider", 0, "PHPStan\\PhpDoc\\StubPhpDocProvider"), reg::typed("phpDocInheritanceResolver", 0, "PHPStan\\PhpDoc\\PhpDocInheritanceResolver"), reg::typed("phpVersion", 0, "PHPStan\\Php\\PhpVersion"), reg::typed("signatureMapProvider", 0, "PHPStan\\Reflection\\SignatureMap\\SignatureMapProvider"), reg::typed("deprecationProvider", 0, "PHPStan\\Reflection\\Deprecation\\DeprecationProvider"), reg::typed("attributeReflectionFactory", 0, "PHPStan\\Reflection\\AttributeReflectionFactory"), reg::typed("classReflectionExtensionRegistryProvider", 0, "PHPStan\\DependencyInjection\\Reflection\\ClassReflectionExtensionRegistryProvider"), reg::typed("displayName", MAY_BE_STRING), reg::typed("reflection", 0, "ReflectionClass"), reg::typed("anonymousFilename", MAY_BE_NULL | MAY_BE_STRING), reg::typed("resolvedTemplateTypeMap", MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeMap"), reg::typed("stubPhpDocBlockCallback", MAY_BE_NULL, "Closure"), reg::typed("extraCacheKey", MAY_BE_NULL | MAY_BE_STRING, nullptr, false, false, "null"), reg::typed("resolvedCallSiteVarianceMap", MAY_BE_NULL, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap", false, false, "null"), reg::typed("finalByKeywordOverride", MAY_BE_NULL | MAY_BE_BOOL, nullptr, false, false, "null") }; +inline constexpr reg::Sig __construct = { "__construct", ZEND_ACC_PUBLIC, 16, __construct_args, 19, nullptr }; +inline constexpr reg::Arg getNativeReflection_return = reg::typed("", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClass|PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionEnum"); +inline constexpr reg::Sig getNativeReflection = { "getNativeReflection", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getNativeReflection_return }; +inline constexpr reg::Arg getFileName_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig getFileName = { "getFileName", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFileName_return }; +inline constexpr reg::Arg getParentClass_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig getParentClass = { "getParentClass", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getParentClass_return }; +inline constexpr reg::Arg findCircularParentClassName_args[] = { reg::typed("parentClassName", MAY_BE_STRING) }; +inline constexpr reg::Arg findCircularParentClassName_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig findCircularParentClassName = { "findCircularParentClassName", ZEND_ACC_PRIVATE, 1, findCircularParentClassName_args, 1, &findCircularParentClassName_return }; +inline constexpr reg::Arg getName_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getName = { "getName", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getName_return }; +inline constexpr reg::Arg getDisplayName_args[] = { reg::typed("withTemplateTypes", MAY_BE_BOOL, nullptr, false, false, "true") }; +inline constexpr reg::Arg getDisplayName_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getDisplayName = { "getDisplayName", ZEND_ACC_PUBLIC, 0, getDisplayName_args, 1, &getDisplayName_return }; +inline constexpr reg::Arg getCacheKey_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getCacheKey = { "getCacheKey", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getCacheKey_return }; +inline constexpr reg::Arg getClassHierarchyDistances_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getClassHierarchyDistances = { "getClassHierarchyDistances", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getClassHierarchyDistances_return }; +inline constexpr reg::Arg collectTraits_args[] = { reg::typed("class", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClass|PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionEnum") }; +inline constexpr reg::Arg collectTraits_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig collectTraits = { "collectTraits", ZEND_ACC_PRIVATE, 1, collectTraits_args, 1, &collectTraits_return }; +inline constexpr reg::Arg allowsDynamicProperties_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig allowsDynamicProperties = { "allowsDynamicProperties", ZEND_ACC_PUBLIC, 0, nullptr, 0, &allowsDynamicProperties_return }; +inline constexpr reg::Arg hasProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasProperty = { "hasProperty", ZEND_ACC_PUBLIC, 1, hasProperty_args, 1, &hasProperty_return }; +inline constexpr reg::Arg hasInstanceProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasInstanceProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasInstanceProperty = { "hasInstanceProperty", ZEND_ACC_PUBLIC, 1, hasInstanceProperty_args, 1, &hasInstanceProperty_return }; +inline constexpr reg::Arg hasStaticProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasStaticProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasStaticProperty = { "hasStaticProperty", ZEND_ACC_PUBLIC, 1, hasStaticProperty_args, 1, &hasStaticProperty_return }; +inline constexpr reg::Arg hasMethod_args[] = { reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasMethod_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasMethod = { "hasMethod", ZEND_ACC_PUBLIC, 1, hasMethod_args, 1, &hasMethod_return }; +inline constexpr reg::Arg getMethod_args[] = { reg::typed("methodName", MAY_BE_STRING), reg::typed("scope", 0, "PHPStan\\Reflection\\ClassMemberAccessAnswerer") }; +inline constexpr reg::Arg getMethod_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getMethod = { "getMethod", ZEND_ACC_PUBLIC, 2, getMethod_args, 2, &getMethod_return }; +inline constexpr reg::Arg wrapExtendedMethod_args[] = { reg::typed("method", 0, "PHPStan\\Reflection\\MethodReflection") }; +inline constexpr reg::Arg wrapExtendedMethod_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig wrapExtendedMethod = { "wrapExtendedMethod", ZEND_ACC_PRIVATE, 1, wrapExtendedMethod_args, 1, &wrapExtendedMethod_return }; +inline constexpr reg::Arg wrapExtendedProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING), reg::typed("method", 0, "PHPStan\\Reflection\\PropertyReflection") }; +inline constexpr reg::Arg wrapExtendedProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig wrapExtendedProperty = { "wrapExtendedProperty", ZEND_ACC_PRIVATE, 2, wrapExtendedProperty_args, 2, &wrapExtendedProperty_return }; +inline constexpr reg::Arg hasNativeMethod_args[] = { reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasNativeMethod_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasNativeMethod = { "hasNativeMethod", ZEND_ACC_PUBLIC, 1, hasNativeMethod_args, 1, &hasNativeMethod_return }; +inline constexpr reg::Arg getNativeMethod_args[] = { reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg getNativeMethod_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getNativeMethod = { "getNativeMethod", ZEND_ACC_PUBLIC, 1, getNativeMethod_args, 1, &getNativeMethod_return }; +inline constexpr reg::Arg hasConstructor_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasConstructor = { "hasConstructor", ZEND_ACC_PUBLIC, 0, nullptr, 0, &hasConstructor_return }; +inline constexpr reg::Arg getConstructor_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getConstructor = { "getConstructor", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getConstructor_return }; +inline constexpr reg::Arg findConstructor_return = reg::typed("", MAY_BE_NULL, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod"); +inline constexpr reg::Sig findConstructor = { "findConstructor", ZEND_ACC_PRIVATE, 0, nullptr, 0, &findConstructor_return }; +inline constexpr reg::Arg evictPrivateSymbols_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig evictPrivateSymbols = { "evictPrivateSymbols", ZEND_ACC_PUBLIC, 0, nullptr, 0, &evictPrivateSymbols_return }; +inline constexpr reg::Arg getProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING), reg::typed("scope", 0, "PHPStan\\Reflection\\ClassMemberAccessAnswerer") }; +inline constexpr reg::Arg getProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig getProperty = { "getProperty", ZEND_ACC_PUBLIC, 2, getProperty_args, 2, &getProperty_return }; +inline constexpr reg::Arg getInstanceProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING), reg::typed("scope", 0, "PHPStan\\Reflection\\ClassMemberAccessAnswerer") }; +inline constexpr reg::Arg getInstanceProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig getInstanceProperty = { "getInstanceProperty", ZEND_ACC_PUBLIC, 2, getInstanceProperty_args, 2, &getInstanceProperty_return }; +inline constexpr reg::Arg getStaticProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg getStaticProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig getStaticProperty = { "getStaticProperty", ZEND_ACC_PUBLIC, 1, getStaticProperty_args, 1, &getStaticProperty_return }; +inline constexpr reg::Arg hasNativeProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasNativeProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasNativeProperty = { "hasNativeProperty", ZEND_ACC_PUBLIC, 1, hasNativeProperty_args, 1, &hasNativeProperty_return }; +inline constexpr reg::Arg getNativeProperty_args[] = { reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg getNativeProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\Php\\PhpPropertyReflection"); +inline constexpr reg::Sig getNativeProperty = { "getNativeProperty", ZEND_ACC_PUBLIC, 1, getNativeProperty_args, 1, &getNativeProperty_return }; +inline constexpr reg::Arg isAbstract_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isAbstract = { "isAbstract", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isAbstract_return }; +inline constexpr reg::Arg isInterface_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInterface = { "isInterface", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInterface_return }; +inline constexpr reg::Arg isTrait_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isTrait = { "isTrait", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isTrait_return }; +inline constexpr reg::Arg isEnum_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isEnum = { "isEnum", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isEnum_return }; +inline constexpr reg::Arg getClassTypeDescription_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getClassTypeDescription = { "getClassTypeDescription", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getClassTypeDescription_return }; +inline constexpr reg::Arg isReadOnly_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isReadOnly = { "isReadOnly", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isReadOnly_return }; +inline constexpr reg::Arg isBackedEnum_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isBackedEnum = { "isBackedEnum", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isBackedEnum_return }; +inline constexpr reg::Arg getBackedEnumType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getBackedEnumType = { "getBackedEnumType", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getBackedEnumType_return }; +inline constexpr reg::Arg hasEnumCase_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg hasEnumCase_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasEnumCase = { "hasEnumCase", ZEND_ACC_PUBLIC, 1, hasEnumCase_args, 1, &hasEnumCase_return }; +inline constexpr reg::Arg getEnumCases_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getEnumCases = { "getEnumCases", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getEnumCases_return }; +inline constexpr reg::Arg getEnumCase_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg getEnumCase_return = reg::typed("", 0, "PHPStan\\Reflection\\EnumCaseReflection"); +inline constexpr reg::Sig getEnumCase = { "getEnumCase", ZEND_ACC_PUBLIC, 1, getEnumCase_args, 1, &getEnumCase_return }; +inline constexpr reg::Arg isClass_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isClass = { "isClass", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isClass_return }; +inline constexpr reg::Arg isAnonymous_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isAnonymous = { "isAnonymous", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isAnonymous_return }; +inline constexpr reg::Arg is_args[] = { reg::typed("className", MAY_BE_STRING) }; +inline constexpr reg::Arg is_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig is = { "is", ZEND_ACC_PUBLIC, 1, is_args, 1, &is_return }; +inline constexpr reg::Arg isSubclassOf_args[] = { reg::typed("className", MAY_BE_STRING) }; +inline constexpr reg::Arg isSubclassOf_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isSubclassOf = { "isSubclassOf", ZEND_ACC_PUBLIC, 1, isSubclassOf_args, 1, &isSubclassOf_return }; +inline constexpr reg::Arg isSubclassOfClass_args[] = { reg::typed("class", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg isSubclassOfClass_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isSubclassOfClass = { "isSubclassOfClass", ZEND_ACC_PUBLIC, 1, isSubclassOfClass_args, 1, &isSubclassOfClass_return }; +inline constexpr reg::Arg implementsInterface_args[] = { reg::typed("className", MAY_BE_STRING) }; +inline constexpr reg::Arg implementsInterface_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig implementsInterface = { "implementsInterface", ZEND_ACC_PUBLIC, 1, implementsInterface_args, 1, &implementsInterface_return }; +inline constexpr reg::Arg getParents_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getParents = { "getParents", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getParents_return }; +inline constexpr reg::Arg getInterfaces_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getInterfaces = { "getInterfaces", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getInterfaces_return }; +inline constexpr reg::Arg collectInterfaces_args[] = { reg::typed("interface", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg collectInterfaces_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig collectInterfaces = { "collectInterfaces", ZEND_ACC_PRIVATE, 1, collectInterfaces_args, 1, &collectInterfaces_return }; +inline constexpr reg::Arg getImmediateInterfaces_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getImmediateInterfaces = { "getImmediateInterfaces", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getImmediateInterfaces_return }; +inline constexpr reg::Arg getTraits_args[] = { reg::typed("recursive", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg getTraits_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getTraits = { "getTraits", ZEND_ACC_PUBLIC, 0, getTraits_args, 1, &getTraits_return }; +inline constexpr reg::Arg getParentClassesNames_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getParentClassesNames = { "getParentClassesNames", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getParentClassesNames_return }; +inline constexpr reg::Arg hasConstant_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg hasConstant_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasConstant = { "hasConstant", ZEND_ACC_PUBLIC, 1, hasConstant_args, 1, &hasConstant_return }; +inline constexpr reg::Arg getConstant_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg getConstant_return = reg::typed("", 0, "PHPStan\\Reflection\\ClassConstantReflection"); +inline constexpr reg::Sig getConstant = { "getConstant", ZEND_ACC_PUBLIC, 1, getConstant_args, 1, &getConstant_return }; +inline constexpr reg::Arg getConstantPhpDocType_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg getConstantPhpDocType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getConstantPhpDocType = { "getConstantPhpDocType", ZEND_ACC_PUBLIC, 1, getConstantPhpDocType_args, 1, &getConstantPhpDocType_return }; +inline constexpr reg::Arg findConstantResolvedPhpDoc_args[] = { reg::typed("reflectionConstant", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClassConstant") }; +inline constexpr reg::Arg findConstantResolvedPhpDoc_return = reg::typed("", MAY_BE_NULL, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); +inline constexpr reg::Sig findConstantResolvedPhpDoc = { "findConstantResolvedPhpDoc", ZEND_ACC_PRIVATE, 1, findConstantResolvedPhpDoc_args, 1, &findConstantResolvedPhpDoc_return }; +inline constexpr reg::Arg resolveConstantVarPhpDocType_args[] = { reg::typed("resolvedPhpDoc", 0, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"), reg::typed("nativeType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("declaringClass", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg resolveConstantVarPhpDocType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveConstantVarPhpDocType = { "resolveConstantVarPhpDocType", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 3, resolveConstantVarPhpDocType_args, 3, &resolveConstantVarPhpDocType_return }; +inline constexpr reg::Arg hasTraitUse_args[] = { reg::typed("traitName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasTraitUse_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasTraitUse = { "hasTraitUse", ZEND_ACC_PUBLIC, 1, hasTraitUse_args, 1, &hasTraitUse_return }; +inline constexpr reg::Arg getTraitNames_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getTraitNames = { "getTraitNames", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getTraitNames_return }; +inline constexpr reg::Arg getTypeAliases_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getTypeAliases = { "getTypeAliases", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getTypeAliases_return }; +inline constexpr reg::Arg getDeprecatedDescription_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig getDeprecatedDescription = { "getDeprecatedDescription", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getDeprecatedDescription_return }; +inline constexpr reg::Arg isDeprecated_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isDeprecated = { "isDeprecated", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isDeprecated_return }; +inline constexpr reg::Arg resolveDeprecation_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig resolveDeprecation = { "resolveDeprecation", ZEND_ACC_PRIVATE, 0, nullptr, 0, &resolveDeprecation_return }; +inline constexpr reg::Arg isBuiltin_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isBuiltin = { "isBuiltin", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isBuiltin_return }; +inline constexpr reg::Arg isInternal_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInternal = { "isInternal", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInternal_return }; +inline constexpr reg::Arg isFinal_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isFinal = { "isFinal", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isFinal_return }; +inline constexpr reg::Arg isImmutable_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isImmutable = { "isImmutable", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isImmutable_return }; +inline constexpr reg::Arg hasConsistentConstructor_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasConsistentConstructor = { "hasConsistentConstructor", ZEND_ACC_PUBLIC, 0, nullptr, 0, &hasConsistentConstructor_return }; +inline constexpr reg::Arg acceptsNamedArguments_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig acceptsNamedArguments = { "acceptsNamedArguments", ZEND_ACC_PUBLIC, 0, nullptr, 0, &acceptsNamedArguments_return }; +inline constexpr reg::Arg hasFinalByKeywordOverride_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasFinalByKeywordOverride = { "hasFinalByKeywordOverride", ZEND_ACC_PUBLIC, 0, nullptr, 0, &hasFinalByKeywordOverride_return }; +inline constexpr reg::Arg isFinalByKeyword_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isFinalByKeyword = { "isFinalByKeyword", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isFinalByKeyword_return }; +inline constexpr reg::Arg isAttributeClass_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isAttributeClass = { "isAttributeClass", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isAttributeClass_return }; +inline constexpr reg::Arg findAttributeFlags_return = reg::typed("", MAY_BE_NULL | MAY_BE_LONG); +inline constexpr reg::Sig findAttributeFlags = { "findAttributeFlags", ZEND_ACC_PRIVATE, 0, nullptr, 0, &findAttributeFlags_return }; +inline constexpr reg::Arg getAttributes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getAttributes = { "getAttributes", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getAttributes_return }; +inline constexpr reg::Arg getAttributeClassFlags_return = reg::typed("", MAY_BE_LONG); +inline constexpr reg::Sig getAttributeClassFlags = { "getAttributeClassFlags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getAttributeClassFlags_return }; +inline constexpr reg::Arg getObjectType_return = reg::typed("", 0, "PHPStan\\Type\\ObjectType"); +inline constexpr reg::Sig getObjectType = { "getObjectType", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getObjectType_return }; +inline constexpr reg::Arg getTemplateTypeMap_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"); +inline constexpr reg::Sig getTemplateTypeMap = { "getTemplateTypeMap", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getTemplateTypeMap_return }; +inline constexpr reg::Arg getActiveTemplateTypeMap_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"); +inline constexpr reg::Sig getActiveTemplateTypeMap = { "getActiveTemplateTypeMap", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getActiveTemplateTypeMap_return }; +inline constexpr reg::Arg getPossiblyIncompleteActiveTemplateTypeMap_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"); +inline constexpr reg::Sig getPossiblyIncompleteActiveTemplateTypeMap = { "getPossiblyIncompleteActiveTemplateTypeMap", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getPossiblyIncompleteActiveTemplateTypeMap_return }; +inline constexpr reg::Arg getActiveTemplateTypeMapForAncestorResolution_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"); +inline constexpr reg::Sig getActiveTemplateTypeMapForAncestorResolution = { "getActiveTemplateTypeMapForAncestorResolution", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getActiveTemplateTypeMapForAncestorResolution_return }; +inline constexpr reg::Arg getDefaultCallSiteVarianceMap_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"); +inline constexpr reg::Sig getDefaultCallSiteVarianceMap = { "getDefaultCallSiteVarianceMap", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getDefaultCallSiteVarianceMap_return }; +inline constexpr reg::Arg getCallSiteVarianceMap_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"); +inline constexpr reg::Sig getCallSiteVarianceMap = { "getCallSiteVarianceMap", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getCallSiteVarianceMap_return }; +inline constexpr reg::Arg isGeneric_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isGeneric = { "isGeneric", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isGeneric_return }; +inline constexpr reg::Arg typeMapFromList_args[] = { reg::typed("types", MAY_BE_ARRAY) }; +inline constexpr reg::Arg typeMapFromList_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"); +inline constexpr reg::Sig typeMapFromList = { "typeMapFromList", ZEND_ACC_PUBLIC, 1, typeMapFromList_args, 1, &typeMapFromList_return }; +inline constexpr reg::Arg varianceMapFromList_args[] = { reg::typed("variances", MAY_BE_ARRAY) }; +inline constexpr reg::Arg varianceMapFromList_return = reg::typed("", 0, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap"); +inline constexpr reg::Sig varianceMapFromList = { "varianceMapFromList", ZEND_ACC_PUBLIC, 1, varianceMapFromList_args, 1, &varianceMapFromList_return }; +inline constexpr reg::Arg typeMapToList_args[] = { reg::typed("typeMap", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap") }; +inline constexpr reg::Arg typeMapToList_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig typeMapToList = { "typeMapToList", ZEND_ACC_PUBLIC, 1, typeMapToList_args, 1, &typeMapToList_return }; +inline constexpr reg::Arg varianceMapToList_args[] = { reg::typed("varianceMap", 0, "PHPStan\\Type\\Generic\\TemplateTypeVarianceMap") }; +inline constexpr reg::Arg varianceMapToList_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig varianceMapToList = { "varianceMapToList", ZEND_ACC_PUBLIC, 1, varianceMapToList_args, 1, &varianceMapToList_return }; +inline constexpr reg::Arg withTypes_args[] = { reg::typed("types", MAY_BE_ARRAY) }; +inline constexpr reg::Arg withTypes_return = reg::typed("", 0, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig withTypes = { "withTypes", ZEND_ACC_PUBLIC, 1, withTypes_args, 1, &withTypes_return }; +inline constexpr reg::Arg withVariances_args[] = { reg::typed("variances", MAY_BE_ARRAY) }; +inline constexpr reg::Arg withVariances_return = reg::typed("", 0, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig withVariances = { "withVariances", ZEND_ACC_PUBLIC, 1, withVariances_args, 1, &withVariances_return }; +inline constexpr reg::Arg asFinal_return = reg::typed("", 0, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig asFinal = { "asFinal", ZEND_ACC_PUBLIC, 0, nullptr, 0, &asFinal_return }; +inline constexpr reg::Arg withoutFinalByKeywordOverride_return = reg::typed("", 0, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig withoutFinalByKeywordOverride = { "withoutFinalByKeywordOverride", ZEND_ACC_PUBLIC, 0, nullptr, 0, &withoutFinalByKeywordOverride_return }; +inline constexpr reg::Arg removeFinalKeywordOverride_return = reg::typed("", 0, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig removeFinalKeywordOverride = { "removeFinalKeywordOverride", ZEND_ACC_PUBLIC, 0, nullptr, 0, &removeFinalKeywordOverride_return }; +inline constexpr reg::Arg getResolvedPhpDoc_return = reg::typed("", MAY_BE_NULL, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); +inline constexpr reg::Sig getResolvedPhpDoc = { "getResolvedPhpDoc", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getResolvedPhpDoc_return }; +inline constexpr reg::Arg getTraitContextResolvedPhpDoc_args[] = { reg::typed("implementingClass", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg getTraitContextResolvedPhpDoc_return = reg::typed("", MAY_BE_NULL, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"); +inline constexpr reg::Sig getTraitContextResolvedPhpDoc = { "getTraitContextResolvedPhpDoc", ZEND_ACC_PUBLIC, 1, getTraitContextResolvedPhpDoc_args, 1, &getTraitContextResolvedPhpDoc_return }; +inline constexpr reg::Arg getFirstExtendsTag_return = reg::typed("", MAY_BE_NULL, "PHPStan\\PhpDoc\\Tag\\ExtendsTag"); +inline constexpr reg::Sig getFirstExtendsTag = { "getFirstExtendsTag", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getFirstExtendsTag_return }; +inline constexpr reg::Arg getExtendsTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getExtendsTags = { "getExtendsTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getExtendsTags_return }; +inline constexpr reg::Arg getImplementsTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getImplementsTags = { "getImplementsTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getImplementsTags_return }; +inline constexpr reg::Arg getTemplateTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getTemplateTags = { "getTemplateTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getTemplateTags_return }; +inline constexpr reg::Arg getAncestors_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getAncestors = { "getAncestors", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getAncestors_return }; +inline constexpr reg::Arg collectAncestors_args[] = { reg::typed("ancestors", MAY_BE_ARRAY, nullptr, true, false) }; +inline constexpr reg::Arg collectAncestors_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig collectAncestors = { "collectAncestors", ZEND_ACC_PRIVATE, 1, collectAncestors_args, 1, &collectAncestors_return }; +inline constexpr reg::Arg getAncestorWithClassName_args[] = { reg::typed("className", MAY_BE_STRING) }; +inline constexpr reg::Arg getAncestorWithClassName_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig getAncestorWithClassName = { "getAncestorWithClassName", ZEND_ACC_PUBLIC, 1, getAncestorWithClassName_args, 1, &getAncestorWithClassName_return }; +inline constexpr reg::Arg isValidAncestorType_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("ancestorClasses", MAY_BE_ARRAY) }; +inline constexpr reg::Arg isValidAncestorType_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isValidAncestorType = { "isValidAncestorType", ZEND_ACC_PRIVATE, 2, isValidAncestorType_args, 2, &isValidAncestorType_return }; +inline constexpr reg::Arg getMixinTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getMixinTags = { "getMixinTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getMixinTags_return }; +inline constexpr reg::Arg getRequireExtendsTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getRequireExtendsTags = { "getRequireExtendsTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getRequireExtendsTags_return }; +inline constexpr reg::Arg getRequireImplementsTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getRequireImplementsTags = { "getRequireImplementsTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getRequireImplementsTags_return }; +inline constexpr reg::Arg getSealedTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getSealedTags = { "getSealedTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getSealedTags_return }; +inline constexpr reg::Arg getPropertyTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getPropertyTags = { "getPropertyTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getPropertyTags_return }; +inline constexpr reg::Arg getMethodTags_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getMethodTags = { "getMethodTags", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getMethodTags_return }; +inline constexpr reg::Arg getResolvedMixinTypes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getResolvedMixinTypes = { "getResolvedMixinTypes", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getResolvedMixinTypes_return }; +inline constexpr reg::Arg getAllowedSubTypes_return = reg::typed("", MAY_BE_NULL | MAY_BE_ARRAY); +inline constexpr reg::Sig getAllowedSubTypes = { "getAllowedSubTypes", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getAllowedSubTypes_return }; +} // namespace sig + +} // namespace ptdecl::ClassReflection + +#endif diff --git a/turbo-ext/src/generated/ExpressionResult.h b/turbo-ext/src/generated/ExpressionResult.h new file mode 100644 index 00000000000..96fa6d14d87 --- /dev/null +++ b/turbo-ext/src/generated/ExpressionResult.h @@ -0,0 +1,197 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/ExpressionResult.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_EXPRESSION_RESULT_H +#define PHPSTANTURBO_GENERATED_EXPRESSION_RESULT_H + +#include "../reg.h" + +namespace ptdecl::ExpressionResult { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t typeCallback = 0; +inline constexpr uint32_t specifyTypesCallback = 1; +inline constexpr uint32_t createTypesCallback = 2; +inline constexpr uint32_t truthyScope = 3; +inline constexpr uint32_t falseyScope = 4; +inline constexpr uint32_t extensionsDeclined = 5; +inline constexpr uint32_t expressionTypeResolverExtensions = 6; +inline constexpr uint32_t defaultNarrowingHelper = 7; +inline constexpr uint32_t scope = 8; +inline constexpr uint32_t beforeScope = 9; +inline constexpr uint32_t expr = 10; +inline constexpr uint32_t hasYield = 11; +inline constexpr uint32_t isAlwaysTerminating = 12; +inline constexpr uint32_t throwPoints = 13; +inline constexpr uint32_t impurePoints = 14; +inline constexpr uint32_t containsNullsafe = 15; +inline constexpr uint32_t issetabilityDescriptor = 16; +inline constexpr uint32_t truthyScopeOverrideResult = 17; +inline constexpr uint32_t falseyScopeOverrideResult = 18; +inline constexpr uint32_t type = 19; +inline constexpr uint32_t nativeType = 20; +inline constexpr uint32_t argsResult = 21; +inline constexpr uint32_t variableFlow = 22; +inline constexpr uint32_t specifiedTypes = 23; +inline constexpr uint32_t cachedType = 24; +inline constexpr uint32_t cachedNativeType = 25; +inline constexpr uint32_t resolvedType = 26; +inline constexpr uint32_t resolvedNativeType = 27; +inline constexpr uint32_t projectedType = 28; +inline constexpr uint32_t projectedNativeType = 29; +inline constexpr uint32_t readVariableNames = 30; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("typeCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Null, 0); + cls.property("specifyTypesCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Null, 0); + cls.property("createTypesCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Null, 0); + cls.property("truthyScope", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("falseyScope", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("extensionsDeclined", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); + cls.property("expressionTypeResolverExtensions", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\DependencyInjection\\ExtensionsCollection"); + cls.property("defaultNarrowingHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper"); + cls.property("scope", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\MutatingScope"); + cls.property("beforeScope", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\MutatingScope"); + cls.property("expr", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PhpParser\\Node\\Expr"); + cls.property("hasYield", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("isAlwaysTerminating", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("throwPoints", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("impurePoints", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("containsNullsafe", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("issetabilityDescriptor", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\IssetabilityDescriptor"); + cls.property("truthyScopeOverrideResult", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResult"); + cls.property("falseyScopeOverrideResult", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResult"); + cls.property("type", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("nativeType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("argsResult", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\ArgsResult"); + cls.property("variableFlow", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); + cls.property("specifiedTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("cachedType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("cachedNativeType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("resolvedType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("resolvedNativeType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("projectedType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("projectedNativeType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); + cls.property("readVariableNames", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_ARRAY); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg __construct_args[] = { reg::typed("expressionTypeResolverExtensions", 0, "PHPStan\\DependencyInjection\\ExtensionsCollection"), reg::typed("defaultNarrowingHelper", 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper"), reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("beforeScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("hasYield", MAY_BE_BOOL), reg::typed("isAlwaysTerminating", MAY_BE_BOOL), reg::typed("throwPoints", MAY_BE_ARRAY), reg::typed("impurePoints", MAY_BE_ARRAY), reg::typed("typeCallback", MAY_BE_NULL | MAY_BE_CALLABLE), reg::typed("specifyTypesCallback", MAY_BE_CALLABLE), reg::typed("containsNullsafe", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("issetabilityDescriptor", MAY_BE_NULL, "PHPStan\\Analyser\\IssetabilityDescriptor", false, false, "null"), reg::typed("truthyScopeOverrideResult", MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResult", false, false, "null"), reg::typed("falseyScopeOverrideResult", MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResult", false, false, "null"), reg::typed("createTypesCallback", MAY_BE_NULL | MAY_BE_CALLABLE, nullptr, false, false, "null"), reg::typed("type", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("nativeType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("argsResult", MAY_BE_NULL, "PHPStan\\Analyser\\ArgsResult", false, false, "null"), reg::typed("variableFlow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow", false, false, "null"), reg::typed("specifiedTypes", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("cachedType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("extensionsDeclined", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("cachedNativeType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("resolvedType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("resolvedNativeType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("projectedType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("projectedNativeType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("readVariableNames", MAY_BE_NULL | MAY_BE_ARRAY, nullptr, false, false, "null") }; +inline constexpr reg::Sig __construct = { "__construct", ZEND_ACC_PUBLIC, 11, __construct_args, 29, nullptr }; +inline constexpr reg::Arg finalize_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("hasYield", MAY_BE_BOOL), reg::typed("isAlwaysTerminating", MAY_BE_BOOL), reg::typed("throwPoints", MAY_BE_ARRAY), reg::typed("impurePoints", MAY_BE_ARRAY), reg::typed("variableFlow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow") }; +inline constexpr reg::Arg finalize_return = reg::typed("", 0, "PHPStan\\Analyser\\ExpressionResult"); +inline constexpr reg::Sig finalize = { "finalize", ZEND_ACC_PUBLIC, 6, finalize_args, 6, &finalize_return }; +inline constexpr reg::Arg getScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig getScope = { "getScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getScope_return }; +inline constexpr reg::Arg getVariableFlow_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig getVariableFlow = { "getVariableFlow", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getVariableFlow_return }; +inline constexpr reg::Arg withScope_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg withScope_return = reg::typed("", 0, "PHPStan\\Analyser\\ExpressionResult"); +inline constexpr reg::Sig withScope = { "withScope", ZEND_ACC_PUBLIC, 1, withScope_args, 1, &withScope_return }; +inline constexpr reg::Arg getBeforeScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig getBeforeScope = { "getBeforeScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getBeforeScope_return }; +inline constexpr reg::Arg getExpr_return = reg::typed("", 0, "PhpParser\\Node\\Expr"); +inline constexpr reg::Sig getExpr = { "getExpr", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getExpr_return }; +inline constexpr reg::Arg getArgsResult_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\ArgsResult"); +inline constexpr reg::Sig getArgsResult = { "getArgsResult", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getArgsResult_return }; +inline constexpr reg::Arg hasYield_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasYield = { "hasYield", ZEND_ACC_PUBLIC, 0, nullptr, 0, &hasYield_return }; +inline constexpr reg::Arg containsNullsafe_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig containsNullsafe = { "containsNullsafe", ZEND_ACC_PUBLIC, 0, nullptr, 0, &containsNullsafe_return }; +inline constexpr reg::Arg getIssetabilityResolution_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("useNativeTypes", MAY_BE_BOOL), reg::typed("reprocessUntrackedLinks", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg getIssetabilityResolution_return = reg::typed("", 0, "PHPStan\\Analyser\\IssetabilityResolution"); +inline constexpr reg::Sig getIssetabilityResolution = { "getIssetabilityResolution", ZEND_ACC_PUBLIC, 2, getIssetabilityResolution_args, 3, &getIssetabilityResolution_return }; +inline constexpr reg::Arg getThrowPoints_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getThrowPoints = { "getThrowPoints", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getThrowPoints_return }; +inline constexpr reg::Arg getImpurePoints_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getImpurePoints = { "getImpurePoints", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getImpurePoints_return }; +inline constexpr reg::Arg getTruthyScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig getTruthyScope = { "getTruthyScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getTruthyScope_return }; +inline constexpr reg::Arg getFalseyScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig getFalseyScope = { "getFalseyScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFalseyScope_return }; +inline constexpr reg::Arg withEqualityCheckResult_args[] = { reg::typed("specifiedTypes", 0, "PHPStan\\Analyser\\SpecifiedTypes"), reg::typed("value", MAY_BE_BOOL) }; +inline constexpr reg::Arg withEqualityCheckResult_return = reg::typed("", 0, "PHPStan\\Analyser\\SpecifiedTypes"); +inline constexpr reg::Sig withEqualityCheckResult = { "withEqualityCheckResult", ZEND_ACC_PRIVATE, 2, withEqualityCheckResult_args, 2, &withEqualityCheckResult_return }; +inline constexpr reg::Arg isAlwaysTerminating_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isAlwaysTerminating = { "isAlwaysTerminating", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isAlwaysTerminating_return }; +inline constexpr reg::Arg getType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getType = { "getType", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getType_return }; +inline constexpr reg::Arg getNativeType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getNativeType = { "getNativeType", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getNativeType_return }; +inline constexpr reg::Arg consultExpressionTypeResolverExtensions_args[] = { reg::typed("readScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg consultExpressionTypeResolverExtensions_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig consultExpressionTypeResolverExtensions = { "consultExpressionTypeResolverExtensions", ZEND_ACC_PRIVATE, 1, consultExpressionTypeResolverExtensions_args, 1, &consultExpressionTypeResolverExtensions_return }; +inline constexpr reg::Arg resolveOwnRawType_args[] = { reg::typed("nativeTypesPromoted", MAY_BE_BOOL) }; +inline constexpr reg::Arg resolveOwnRawType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveOwnRawType = { "resolveOwnRawType", ZEND_ACC_PRIVATE, 1, resolveOwnRawType_args, 1, &resolveOwnRawType_return }; +inline constexpr reg::Arg releaseTypeCallbackIfResolved_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig releaseTypeCallbackIfResolved = { "releaseTypeCallbackIfResolved", ZEND_ACC_PRIVATE, 0, nullptr, 0, &releaseTypeCallbackIfResolved_return }; +inline constexpr reg::Arg resolveOwnType_args[] = { reg::typed("nativeTypesPromoted", MAY_BE_BOOL) }; +inline constexpr reg::Arg resolveOwnType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveOwnType = { "resolveOwnType", ZEND_ACC_PRIVATE, 1, resolveOwnType_args, 1, &resolveOwnType_return }; +inline constexpr reg::Arg projectVoidToNull_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("nativeTypesPromoted", MAY_BE_BOOL) }; +inline constexpr reg::Arg projectVoidToNull_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig projectVoidToNull = { "projectVoidToNull", ZEND_ACC_PRIVATE, 2, projectVoidToNull_args, 2, &projectVoidToNull_return }; +inline constexpr reg::Arg projectsVoidToNull_args[] = { reg::typed("nativeTypesPromoted", MAY_BE_BOOL) }; +inline constexpr reg::Arg projectsVoidToNull_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig projectsVoidToNull = { "projectsVoidToNull", ZEND_ACC_PRIVATE, 1, projectsVoidToNull_args, 1, &projectsVoidToNull_return }; +inline constexpr reg::Arg getKeepVoidType_args[] = { reg::typed("nativeTypesPromoted", MAY_BE_BOOL) }; +inline constexpr reg::Arg getKeepVoidType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getKeepVoidType = { "getKeepVoidType", ZEND_ACC_PUBLIC, 1, getKeepVoidType_args, 1, &getKeepVoidType_return }; +inline constexpr reg::Arg hasTrackedExpressionType_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg hasTrackedExpressionType_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasTrackedExpressionType = { "hasTrackedExpressionType", ZEND_ACC_PRIVATE, 1, hasTrackedExpressionType_args, 1, &hasTrackedExpressionType_return }; +inline constexpr reg::Arg canResolveOwnType_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canResolveOwnType = { "canResolveOwnType", ZEND_ACC_PUBLIC, 0, nullptr, 0, &canResolveOwnType_return }; +inline constexpr reg::Arg hasOwnLazyResolution_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasOwnLazyResolution = { "hasOwnLazyResolution", ZEND_ACC_PRIVATE, 0, nullptr, 0, &hasOwnLazyResolution_return }; +inline constexpr reg::Arg getSpecifiedTypesForScope_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("context", 0, "PHPStan\\Analyser\\TypeSpecifierContext") }; +inline constexpr reg::Arg getSpecifiedTypesForScope_return = reg::typed("", 0, "PHPStan\\Analyser\\SpecifiedTypes"); +inline constexpr reg::Sig getSpecifiedTypesForScope = { "getSpecifiedTypesForScope", ZEND_ACC_PUBLIC, 2, getSpecifiedTypesForScope_args, 2, &getSpecifiedTypesForScope_return }; +inline constexpr reg::Arg getSpecifiedTypes_args[] = { reg::typed("context", 0, "PHPStan\\Analyser\\TypeSpecifierContext"), reg::typed("nativeTypesPromoted", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg getSpecifiedTypes_return = reg::typed("", 0, "PHPStan\\Analyser\\SpecifiedTypes"); +inline constexpr reg::Sig getSpecifiedTypes = { "getSpecifiedTypes", ZEND_ACC_PUBLIC, 1, getSpecifiedTypes_args, 2, &getSpecifiedTypes_return }; +inline constexpr reg::Arg getCreatedTypesForScope_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("context", 0, "PHPStan\\Analyser\\TypeSpecifierContext") }; +inline constexpr reg::Arg getCreatedTypesForScope_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\SpecifiedTypes"); +inline constexpr reg::Sig getCreatedTypesForScope = { "getCreatedTypesForScope", ZEND_ACC_PUBLIC, 3, getCreatedTypesForScope_args, 3, &getCreatedTypesForScope_return }; +inline constexpr reg::Arg getCreatedTypes_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("context", 0, "PHPStan\\Analyser\\TypeSpecifierContext"), reg::typed("nativeTypesPromoted", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg getCreatedTypes_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\SpecifiedTypes"); +inline constexpr reg::Sig getCreatedTypes = { "getCreatedTypes", ZEND_ACC_PUBLIC, 2, getCreatedTypes_args, 3, &getCreatedTypes_return }; +inline constexpr reg::Arg getTypeOnScope_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("useNativeTypes", MAY_BE_BOOL) }; +inline constexpr reg::Arg getTypeOnScope_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getTypeOnScope = { "getTypeOnScope", ZEND_ACC_PUBLIC, 2, getTypeOnScope_args, 2, &getTypeOnScope_return }; +inline constexpr reg::Arg answersOnScope_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("useNativeTypes", MAY_BE_BOOL) }; +inline constexpr reg::Arg answersOnScope_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig answersOnScope = { "answersOnScope", ZEND_ACC_PUBLIC, 2, answersOnScope_args, 2, &answersOnScope_return }; +inline constexpr reg::Arg isScopeAuthoritative_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg isScopeAuthoritative_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isScopeAuthoritative = { "isScopeAuthoritative", ZEND_ACC_PRIVATE, 1, isScopeAuthoritative_args, 1, &isScopeAuthoritative_return }; +inline constexpr reg::Arg askScopeVariableStateMatches_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("useNativeTypes", MAY_BE_BOOL), reg::typed("ruleFacingAsk", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg askScopeVariableStateMatches_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig askScopeVariableStateMatches = { "askScopeVariableStateMatches", ZEND_ACC_PUBLIC, 2, askScopeVariableStateMatches_args, 3, &askScopeVariableStateMatches_return }; +inline constexpr reg::Arg atAskPosition_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg atAskPosition_return = reg::typed("", 0, "PHPStan\\Analyser\\ExpressionResult"); +inline constexpr reg::Sig atAskPosition = { "atAskPosition", ZEND_ACC_PUBLIC, 1, atAskPosition_args, 1, &atAskPosition_return }; +inline constexpr reg::Arg onNonNullabilityDevicedScopes_args[] = { reg::typed("beforeScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg onNonNullabilityDevicedScopes_return = reg::typed("", 0, "PHPStan\\Analyser\\ExpressionResult"); +inline constexpr reg::Sig onNonNullabilityDevicedScopes = { "onNonNullabilityDevicedScopes", ZEND_ACC_PUBLIC, 2, onNonNullabilityDevicedScopes_args, 2, &onNonNullabilityDevicedScopes_return }; +inline constexpr reg::Arg getReadVariableNames_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getReadVariableNames = { "getReadVariableNames", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getReadVariableNames_return }; +inline constexpr reg::Arg collectReadVariableNames_args[] = { reg::typed("node", 0, "PhpParser\\Node") }; +inline constexpr reg::Arg collectReadVariableNames_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig collectReadVariableNames = { "collectReadVariableNames", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 1, collectReadVariableNames_args, 1, &collectReadVariableNames_return }; +} // namespace sig + +} // namespace ptdecl::ExpressionResult + +#endif diff --git a/turbo-ext/src/generated/ExpressionResultStorageStack.h b/turbo-ext/src/generated/ExpressionResultStorageStack.h new file mode 100644 index 00000000000..37faaae7e18 --- /dev/null +++ b/turbo-ext/src/generated/ExpressionResultStorageStack.h @@ -0,0 +1,40 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/ExpressionResultStorageStack.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_EXPRESSION_RESULT_STORAGE_STACK_H +#define PHPSTANTURBO_GENERATED_EXPRESSION_RESULT_STORAGE_STACK_H + +#include "../reg.h" + +namespace ptdecl::ExpressionResultStorageStack { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t stack = 0; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("stack", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg push_args[] = { reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage") }; +inline constexpr reg::Arg push_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig push = { "push", ZEND_ACC_PUBLIC, 1, push_args, 1, &push_return }; +inline constexpr reg::Arg pop_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig pop = { "pop", ZEND_ACC_PUBLIC, 0, nullptr, 0, &pop_return }; +inline constexpr reg::Arg getCurrent_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResultStorage"); +inline constexpr reg::Sig getCurrent = { "getCurrent", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getCurrent_return }; +} // namespace sig + +} // namespace ptdecl::ExpressionResultStorageStack + +#endif diff --git a/turbo-ext/src/generated/MutatingScope.h b/turbo-ext/src/generated/MutatingScope.h new file mode 100644 index 00000000000..59ec61fa945 --- /dev/null +++ b/turbo-ext/src/generated/MutatingScope.h @@ -0,0 +1,640 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/MutatingScope.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_MUTATING_SCOPE_H +#define PHPSTANTURBO_GENERATED_MUTATING_SCOPE_H + +#include "../reg.h" + +namespace ptdecl::MutatingScope { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t resolvedTypes = 0; +inline constexpr uint32_t nodeCallbackScope = 1; +inline constexpr uint32_t namespace_ = 2; +inline constexpr uint32_t scopeOutOfFirstLevelStatement = 3; +inline constexpr uint32_t scopeWithPromotedNativeTypes = 4; +inline constexpr uint32_t container = 5; +inline constexpr uint32_t scopeFactory = 6; +inline constexpr uint32_t reflectionProvider = 7; +inline constexpr uint32_t initializerExprTypeResolver = 8; +inline constexpr uint32_t expressionTypeResolverExtensions = 9; +inline constexpr uint32_t exprPrinter = 10; +inline constexpr uint32_t typeSpecifier = 11; +inline constexpr uint32_t propertyReflectionFinder = 12; +inline constexpr uint32_t parser = 13; +inline constexpr uint32_t constantResolver = 14; +inline constexpr uint32_t expressionResultStorageStack = 15; +inline constexpr uint32_t context = 16; +inline constexpr uint32_t phpVersion = 17; +inline constexpr uint32_t attributeReflectionFactory = 18; +inline constexpr uint32_t configuredPhpVersionRangeHelper = 19; +inline constexpr uint32_t nodeCallback = 20; +inline constexpr uint32_t declareStrictTypes = 21; +inline constexpr uint32_t function = 22; +inline constexpr uint32_t expressionTypes = 23; +inline constexpr uint32_t nativeExpressionTypes = 24; +inline constexpr uint32_t conditionalExpressions = 25; +inline constexpr uint32_t inClosureBindScopeClasses = 26; +inline constexpr uint32_t anonymousFunctionReflection = 27; +inline constexpr uint32_t inFirstLevelStatement = 28; +inline constexpr uint32_t currentlyAssignedExpressions = 29; +inline constexpr uint32_t currentlyAllowedUndefinedExpressions = 30; +inline constexpr uint32_t inFunctionCallsStack = 31; +inline constexpr uint32_t afterExtractCall = 32; +inline constexpr uint32_t parentScope = 33; +inline constexpr uint32_t nativeTypesPromoted = 34; +inline constexpr uint32_t templateArgumentFrame = 35; +inline constexpr uint32_t templateArgumentConstraints = 36; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.implements({ "PHPStan\\Analyser\\Scope", "PHPStan\\Analyser\\NodeCallbackInvoker", "PHPStan\\Analyser\\CollectedDataEmitter" }); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("globalConstantFetchKeys", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("resolvedTypes", ZEND_ACC_PUBLIC, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("nodeCallbackScope", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("namespace", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_STRING); + cls.property("scopeOutOfFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("scopeWithPromotedNativeTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("container", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\DependencyInjection\\Container"); + cls.property("scopeFactory", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\InternalScopeFactory"); + cls.property("reflectionProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\ReflectionProvider"); + cls.property("initializerExprTypeResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\InitializerExprTypeResolver"); + cls.property("expressionTypeResolverExtensions", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\DependencyInjection\\ExtensionsCollection"); + cls.property("exprPrinter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Node\\Printer\\ExprPrinter"); + cls.property("typeSpecifier", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\TypeSpecifier"); + cls.property("propertyReflectionFinder", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Rules\\Properties\\PropertyReflectionFinder"); + cls.property("parser", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Parser\\Parser"); + cls.property("constantResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ConstantResolver"); + cls.property("expressionResultStorageStack", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExpressionResultStorageStack"); + cls.property("context", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ScopeContext"); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Php\\PhpVersion"); + cls.property("attributeReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\AttributeReflectionFactory"); + cls.property("configuredPhpVersionRangeHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Php\\ConfiguredPhpVersionRangeHelper"); + cls.property("nodeCallback", ZEND_ACC_PRIVATE, reg::PropertyKind::Null, 0); + cls.property("declareStrictTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("function", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection"); + cls.property("expressionTypes", ZEND_ACC_PUBLIC, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("nativeExpressionTypes", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("conditionalExpressions", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("inClosureBindScopeClasses", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("anonymousFunctionReflection", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\ClosureType"); + cls.property("inFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("currentlyAssignedExpressions", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("currentlyAllowedUndefinedExpressions", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("inFunctionCallsStack", ZEND_ACC_PUBLIC, reg::PropertyKind::Typed, MAY_BE_ARRAY); + cls.property("afterExtractCall", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("parentScope", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("nativeTypesPromoted", ZEND_ACC_PUBLIC, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("templateArgumentFrame", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame"); + cls.property("templateArgumentConstraints", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints"); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg __construct_args[] = { reg::typed("container", 0, "PHPStan\\DependencyInjection\\Container"), reg::typed("scopeFactory", 0, "PHPStan\\Analyser\\InternalScopeFactory"), reg::typed("reflectionProvider", 0, "PHPStan\\Reflection\\ReflectionProvider"), reg::typed("initializerExprTypeResolver", 0, "PHPStan\\Reflection\\InitializerExprTypeResolver"), reg::typed("expressionTypeResolverExtensions", 0, "PHPStan\\DependencyInjection\\ExtensionsCollection"), reg::typed("exprPrinter", 0, "PHPStan\\Node\\Printer\\ExprPrinter"), reg::typed("typeSpecifier", 0, "PHPStan\\Analyser\\TypeSpecifier"), reg::typed("propertyReflectionFinder", 0, "PHPStan\\Rules\\Properties\\PropertyReflectionFinder"), reg::typed("parser", 0, "PHPStan\\Parser\\Parser"), reg::typed("constantResolver", 0, "PHPStan\\Analyser\\ConstantResolver"), reg::typed("expressionResultStorageStack", 0, "PHPStan\\Analyser\\ExpressionResultStorageStack"), reg::typed("context", 0, "PHPStan\\Analyser\\ScopeContext"), reg::typed("phpVersion", 0, "PHPStan\\Php\\PhpVersion"), reg::typed("attributeReflectionFactory", 0, "PHPStan\\Reflection\\AttributeReflectionFactory"), reg::typed("configuredPhpVersionRangeHelper", 0, "PHPStan\\Php\\ConfiguredPhpVersionRangeHelper"), reg::typed("nodeCallback", 0, nullptr, false, false, "null"), reg::typed("declareStrictTypes", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("function", MAY_BE_NULL, "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection", false, false, "null"), reg::typed("namespace", MAY_BE_NULL | MAY_BE_STRING, nullptr, false, false, "null"), reg::typed("expressionTypes", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("nativeExpressionTypes", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("conditionalExpressions", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("inClosureBindScopeClasses", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("anonymousFunctionReflection", MAY_BE_NULL, "PHPStan\\Type\\ClosureType", false, false, "null"), reg::typed("inFirstLevelStatement", MAY_BE_BOOL, nullptr, false, false, "true"), reg::typed("currentlyAssignedExpressions", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("currentlyAllowedUndefinedExpressions", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("inFunctionCallsStack", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("afterExtractCall", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("parentScope", MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope", false, false, "null"), reg::typed("nativeTypesPromoted", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("templateArgumentFrame", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame", false, false, "null"), reg::typed("templateArgumentConstraints", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints", false, false, "null") }; +inline constexpr reg::Sig __construct = { "__construct", ZEND_ACC_PUBLIC, 15, __construct_args, 33, nullptr }; +inline constexpr reg::Arg toNodeCallbackScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig toNodeCallbackScope = { "toNodeCallbackScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &toNodeCallbackScope_return }; +inline constexpr reg::Arg toWalkScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig toWalkScope = { "toWalkScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &toWalkScope_return }; +inline constexpr reg::Arg toMutatingScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig toMutatingScope = { "toMutatingScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &toMutatingScope_return }; +inline constexpr reg::Arg getFile_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getFile = { "getFile", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFile_return }; +inline constexpr reg::Arg getFileDescription_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getFileDescription = { "getFileDescription", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFileDescription_return }; +inline constexpr reg::Arg isDeclareStrictTypes_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isDeclareStrictTypes = { "isDeclareStrictTypes", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isDeclareStrictTypes_return }; +inline constexpr reg::Arg enterDeclareStrictTypes_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterDeclareStrictTypes = { "enterDeclareStrictTypes", ZEND_ACC_PUBLIC, 0, nullptr, 0, &enterDeclareStrictTypes_return }; +inline constexpr reg::Arg rememberConstructorExpressions_args[] = { reg::typed("currentExpressionTypes", MAY_BE_ARRAY) }; +inline constexpr reg::Arg rememberConstructorExpressions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig rememberConstructorExpressions = { "rememberConstructorExpressions", ZEND_ACC_PRIVATE, 1, rememberConstructorExpressions_args, 1, &rememberConstructorExpressions_return }; +inline constexpr reg::Arg classHasCustomSerialization_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig classHasCustomSerialization = { "classHasCustomSerialization", ZEND_ACC_PRIVATE, 0, nullptr, 0, &classHasCustomSerialization_return }; +inline constexpr reg::Arg rememberConstructorScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig rememberConstructorScope = { "rememberConstructorScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &rememberConstructorScope_return }; +inline constexpr reg::Arg isReadonlyPropertyFetch_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr\\PropertyFetch"), reg::typed("allowOnlyOnThis", MAY_BE_BOOL) }; +inline constexpr reg::Arg isReadonlyPropertyFetch_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isReadonlyPropertyFetch = { "isReadonlyPropertyFetch", ZEND_ACC_PUBLIC, 2, isReadonlyPropertyFetch_args, 2, &isReadonlyPropertyFetch_return }; +inline constexpr reg::Arg isInClass_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInClass = { "isInClass", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInClass_return }; +inline constexpr reg::Arg isInTrait_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInTrait = { "isInTrait", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInTrait_return }; +inline constexpr reg::Arg getClassReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig getClassReflection = { "getClassReflection", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getClassReflection_return }; +inline constexpr reg::Arg getTraitReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ClassReflection"); +inline constexpr reg::Sig getTraitReflection = { "getTraitReflection", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getTraitReflection_return }; +inline constexpr reg::Arg getFunction_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection"); +inline constexpr reg::Sig getFunction = { "getFunction", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFunction_return }; +inline constexpr reg::Arg getFunctionName_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig getFunctionName = { "getFunctionName", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFunctionName_return }; +inline constexpr reg::Arg getNamespace_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig getNamespace = { "getNamespace", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getNamespace_return }; +inline constexpr reg::Arg getParentScope_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig getParentScope = { "getParentScope", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getParentScope_return }; +inline constexpr reg::Arg canAnyVariableExist_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canAnyVariableExist = { "canAnyVariableExist", ZEND_ACC_PUBLIC, 0, nullptr, 0, &canAnyVariableExist_return }; +inline constexpr reg::Arg afterExtractCall_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig afterExtractCall = { "afterExtractCall", ZEND_ACC_PUBLIC, 0, nullptr, 0, &afterExtractCall_return }; +inline constexpr reg::Arg afterClearstatcacheCall_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig afterClearstatcacheCall = { "afterClearstatcacheCall", ZEND_ACC_PUBLIC, 0, nullptr, 0, &afterClearstatcacheCall_return }; +inline constexpr reg::Arg afterOpenSslCall_args[] = { reg::typed("openSslFunctionName", MAY_BE_STRING) }; +inline constexpr reg::Arg afterOpenSslCall_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig afterOpenSslCall = { "afterOpenSslCall", ZEND_ACC_PUBLIC, 1, afterOpenSslCall_args, 1, &afterOpenSslCall_return }; +inline constexpr reg::Arg invalidateVolatileExpressions_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig invalidateVolatileExpressions = { "invalidateVolatileExpressions", ZEND_ACC_PUBLIC, 0, nullptr, 0, &invalidateVolatileExpressions_return }; +inline constexpr reg::Arg invalidateExistenceCheckExpressions_args[] = { reg::typed("functionNames", MAY_BE_ARRAY), reg::typed("declaredSymbolName", MAY_BE_NULL | MAY_BE_STRING) }; +inline constexpr reg::Arg invalidateExistenceCheckExpressions_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig invalidateExistenceCheckExpressions = { "invalidateExistenceCheckExpressions", ZEND_ACC_PUBLIC, 2, invalidateExistenceCheckExpressions_args, 2, &invalidateExistenceCheckExpressions_return }; +inline constexpr reg::Arg hasVariableType_args[] = { reg::typed("variableName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasVariableType_return = reg::typed("", 0, "PHPStan\\TrinaryLogic"); +inline constexpr reg::Sig hasVariableType = { "hasVariableType", ZEND_ACC_PUBLIC, 1, hasVariableType_args, 1, &hasVariableType_return }; +inline constexpr reg::Arg getVariableType_args[] = { reg::typed("variableName", MAY_BE_STRING) }; +inline constexpr reg::Arg getVariableType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getVariableType = { "getVariableType", ZEND_ACC_PUBLIC, 1, getVariableType_args, 1, &getVariableType_return }; +inline constexpr reg::Arg getDefinedVariables_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getDefinedVariables = { "getDefinedVariables", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getDefinedVariables_return }; +inline constexpr reg::Arg getMaybeDefinedVariables_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getMaybeDefinedVariables = { "getMaybeDefinedVariables", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getMaybeDefinedVariables_return }; +inline constexpr reg::Arg findPossiblyImpureCallDescriptions_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg findPossiblyImpureCallDescriptions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig findPossiblyImpureCallDescriptions = { "findPossiblyImpureCallDescriptions", ZEND_ACC_PUBLIC, 1, findPossiblyImpureCallDescriptions_args, 1, &findPossiblyImpureCallDescriptions_return }; +inline constexpr reg::Arg isGlobalVariable_args[] = { reg::typed("variableName", MAY_BE_STRING) }; +inline constexpr reg::Arg isGlobalVariable_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isGlobalVariable = { "isGlobalVariable", ZEND_ACC_PRIVATE, 1, isGlobalVariable_args, 1, &isGlobalVariable_return }; +inline constexpr reg::Arg hasConstant_args[] = { reg::typed("name", 0, "PhpParser\\Node\\Name") }; +inline constexpr reg::Arg hasConstant_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasConstant = { "hasConstant", ZEND_ACC_PUBLIC, 1, hasConstant_args, 1, &hasConstant_return }; +inline constexpr reg::Arg fileHasCompilerHaltStatementCalls_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig fileHasCompilerHaltStatementCalls = { "fileHasCompilerHaltStatementCalls", ZEND_ACC_PRIVATE, 0, nullptr, 0, &fileHasCompilerHaltStatementCalls_return }; +inline constexpr reg::Arg isInAnonymousFunction_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInAnonymousFunction = { "isInAnonymousFunction", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInAnonymousFunction_return }; +inline constexpr reg::Arg getAnonymousFunctionReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\ClosureType"); +inline constexpr reg::Sig getAnonymousFunctionReflection = { "getAnonymousFunctionReflection", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getAnonymousFunctionReflection_return }; +inline constexpr reg::Arg getAnonymousFunctionReturnType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getAnonymousFunctionReturnType = { "getAnonymousFunctionReturnType", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getAnonymousFunctionReturnType_return }; +inline constexpr reg::Arg withAnonymousFunctionReflection_args[] = { reg::typed("anonymousFunctionReflection", 0, "PHPStan\\Type\\ClosureType") }; +inline constexpr reg::Arg withAnonymousFunctionReflection_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig withAnonymousFunctionReflection = { "withAnonymousFunctionReflection", ZEND_ACC_PUBLIC, 1, withAnonymousFunctionReflection_args, 1, &withAnonymousFunctionReflection_return }; +inline constexpr reg::Arg getType_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getType = { "getType", ZEND_ACC_PUBLIC, 1, getType_args, 1, &getType_return }; +inline constexpr reg::Arg getScopeType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getScopeType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getScopeType = { "getScopeType", ZEND_ACC_PUBLIC, 1, getScopeType_args, 1, &getScopeType_return }; +inline constexpr reg::Arg getScopeNativeType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getScopeNativeType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getScopeNativeType = { "getScopeNativeType", ZEND_ACC_PUBLIC, 1, getScopeNativeType_args, 1, &getScopeNativeType_return }; +inline constexpr reg::Arg getNodeKey_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getNodeKey_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getNodeKey = { "getNodeKey", ZEND_ACC_PUBLIC, 1, getNodeKey_args, 1, &getNodeKey_return }; +inline constexpr reg::Arg getExprPrinter_return = reg::typed("", 0, "PHPStan\\Node\\Printer\\ExprPrinter"); +inline constexpr reg::Sig getExprPrinter = { "getExprPrinter", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getExprPrinter_return }; +inline constexpr reg::Arg duplicateWith_args[] = { reg::typed("expressionTypes", MAY_BE_ARRAY), reg::typed("nativeExpressionTypes", MAY_BE_ARRAY), reg::typed("conditionalExpressions", MAY_BE_ARRAY), reg::typed("currentlyAssignedExpressions", MAY_BE_ARRAY), reg::typed("currentlyAllowedUndefinedExpressions", MAY_BE_ARRAY), reg::typed("inFunctionCallsStack", MAY_BE_ARRAY), reg::typed("inFirstLevelStatement", MAY_BE_BOOL), reg::typed("afterExtractCall", MAY_BE_BOOL) }; +inline constexpr reg::Arg duplicateWith_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig duplicateWith = { "duplicateWith", ZEND_ACC_PUBLIC, 8, duplicateWith_args, 8, &duplicateWith_return }; +inline constexpr reg::Arg getClosureScopeCacheKey_args[] = { reg::typed("relevantRoots", MAY_BE_NULL | MAY_BE_ARRAY, nullptr, false, false, "null") }; +inline constexpr reg::Arg getClosureScopeCacheKey_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig getClosureScopeCacheKey = { "getClosureScopeCacheKey", ZEND_ACC_PUBLIC, 0, getClosureScopeCacheKey_args, 1, &getClosureScopeCacheKey_return }; +inline constexpr reg::Arg exprStringIsRootedIn_args[] = { reg::typed("exprString", MAY_BE_STRING), reg::typed("roots", MAY_BE_ARRAY) }; +inline constexpr reg::Arg exprStringIsRootedIn_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig exprStringIsRootedIn = { "exprStringIsRootedIn", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 2, exprStringIsRootedIn_args, 2, &exprStringIsRootedIn_return }; +inline constexpr reg::Arg resolveType_args[] = { reg::typed("exprString", MAY_BE_STRING), reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg resolveType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveType = { "resolveType", ZEND_ACC_PRIVATE, 2, resolveType_args, 2, &resolveType_return }; +inline constexpr reg::Arg resolveTypeOfNewWorldHandlerNode_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg resolveTypeOfNewWorldHandlerNode_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveTypeOfNewWorldHandlerNode = { "resolveTypeOfNewWorldHandlerNode", ZEND_ACC_PRIVATE, 1, resolveTypeOfNewWorldHandlerNode_args, 1, &resolveTypeOfNewWorldHandlerNode_return }; +inline constexpr reg::Arg getCurrentTypesOfSpecifiedExpr_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getCurrentTypesOfSpecifiedExpr_return = reg::typed("", MAY_BE_NULL | MAY_BE_ARRAY); +inline constexpr reg::Sig getCurrentTypesOfSpecifiedExpr = { "getCurrentTypesOfSpecifiedExpr", ZEND_ACC_PRIVATE, 1, getCurrentTypesOfSpecifiedExpr_args, 1, &getCurrentTypesOfSpecifiedExpr_return }; +inline constexpr reg::Arg specifyTypesOfNewWorldHandlerNode_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr"), reg::typed("context", 0, "PHPStan\\Analyser\\TypeSpecifierContext") }; +inline constexpr reg::Arg specifyTypesOfNewWorldHandlerNode_return = reg::typed("", 0, "PHPStan\\Analyser\\SpecifiedTypes"); +inline constexpr reg::Sig specifyTypesOfNewWorldHandlerNode = { "specifyTypesOfNewWorldHandlerNode", ZEND_ACC_PUBLIC, 2, specifyTypesOfNewWorldHandlerNode_args, 2, &specifyTypesOfNewWorldHandlerNode_return }; +inline constexpr reg::Arg obtainResultForNode_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg obtainResultForNode_return = reg::typed("", 0, "PHPStan\\Analyser\\ExpressionResult"); +inline constexpr reg::Sig obtainResultForNode = { "obtainResultForNode", ZEND_ACC_PUBLIC, 1, obtainResultForNode_args, 1, &obtainResultForNode_return }; +inline constexpr reg::Arg pushExpressionResultStorage_args[] = { reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage") }; +inline constexpr reg::Arg pushExpressionResultStorage_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig pushExpressionResultStorage = { "pushExpressionResultStorage", ZEND_ACC_PUBLIC, 1, pushExpressionResultStorage_args, 1, &pushExpressionResultStorage_return }; +inline constexpr reg::Arg popExpressionResultStorage_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig popExpressionResultStorage = { "popExpressionResultStorage", ZEND_ACC_PUBLIC, 0, nullptr, 0, &popExpressionResultStorage_return }; +inline constexpr reg::Arg findSettledStoredResult_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg findSettledStoredResult_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResult"); +inline constexpr reg::Sig findSettledStoredResult = { "findSettledStoredResult", ZEND_ACC_PROTECTED, 1, findSettledStoredResult_args, 1, &findSettledStoredResult_return }; +inline constexpr reg::Arg getCurrentExpressionResultStorage_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResultStorage"); +inline constexpr reg::Sig getCurrentExpressionResultStorage = { "getCurrentExpressionResultStorage", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getCurrentExpressionResultStorage_return }; +inline constexpr reg::Arg withTemplateArgumentFrame_args[] = { reg::typed("frame", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame") }; +inline constexpr reg::Arg withTemplateArgumentFrame_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig withTemplateArgumentFrame = { "withTemplateArgumentFrame", ZEND_ACC_PUBLIC, 1, withTemplateArgumentFrame_args, 1, &withTemplateArgumentFrame_return }; +inline constexpr reg::Arg getCurrentTemplateArgumentFrame_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame"); +inline constexpr reg::Sig getCurrentTemplateArgumentFrame = { "getCurrentTemplateArgumentFrame", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getCurrentTemplateArgumentFrame_return }; +inline constexpr reg::Arg getTemplateArgumentConstraints_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints"); +inline constexpr reg::Sig getTemplateArgumentConstraints = { "getTemplateArgumentConstraints", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getTemplateArgumentConstraints_return }; +inline constexpr reg::Arg withTemplateArgumentConstraints_args[] = { reg::typed("constraints", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints") }; +inline constexpr reg::Arg withTemplateArgumentConstraints_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig withTemplateArgumentConstraints = { "withTemplateArgumentConstraints", ZEND_ACC_PUBLIC, 1, withTemplateArgumentConstraints_args, 1, &withTemplateArgumentConstraints_return }; +inline constexpr reg::Arg addTemplateArgumentConstraints_args[] = { reg::typed("constraints", MAY_BE_NULL, "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints") }; +inline constexpr reg::Arg addTemplateArgumentConstraints_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig addTemplateArgumentConstraints = { "addTemplateArgumentConstraints", ZEND_ACC_PUBLIC, 1, addTemplateArgumentConstraints_args, 1, &addTemplateArgumentConstraints_return }; +inline constexpr reg::Arg withoutMemoizedTypes_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig withoutMemoizedTypes = { "withoutMemoizedTypes", ZEND_ACC_PUBLIC, 0, nullptr, 0, &withoutMemoizedTypes_return }; +inline constexpr reg::Arg getDifferingVariableRoots_args[] = { reg::typed("other", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg getDifferingVariableRoots_return = reg::typed("", MAY_BE_NULL | MAY_BE_ARRAY); +inline constexpr reg::Sig getDifferingVariableRoots = { "getDifferingVariableRoots", ZEND_ACC_PUBLIC, 1, getDifferingVariableRoots_args, 1, &getDifferingVariableRoots_return }; +inline constexpr reg::Arg getVariableRootOfExpressionKey_args[] = { reg::typed("key", MAY_BE_STRING) }; +inline constexpr reg::Arg getVariableRootOfExpressionKey_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig getVariableRootOfExpressionKey = { "getVariableRootOfExpressionKey", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 1, getVariableRootOfExpressionKey_args, 1, &getVariableRootOfExpressionKey_return }; +inline constexpr reg::Arg withRecordedStatementDelta_args[] = { reg::typed("recordedEntry", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("recordedExit", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg withRecordedStatementDelta_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig withRecordedStatementDelta = { "withRecordedStatementDelta", ZEND_ACC_PUBLIC, 2, withRecordedStatementDelta_args, 2, &withRecordedStatementDelta_return }; +inline constexpr reg::Arg applyRecordedHolderDelta_args[] = { reg::typed("current", MAY_BE_ARRAY), reg::typed("recordedEntry", MAY_BE_ARRAY), reg::typed("recordedExit", MAY_BE_ARRAY) }; +inline constexpr reg::Arg applyRecordedHolderDelta_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig applyRecordedHolderDelta = { "applyRecordedHolderDelta", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 3, applyRecordedHolderDelta_args, 3, &applyRecordedHolderDelta_return }; +inline constexpr reg::Arg getNativeType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getNativeType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getNativeType = { "getNativeType", ZEND_ACC_PUBLIC, 1, getNativeType_args, 1, &getNativeType_return }; +inline constexpr reg::Arg getKeepVoidType_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getKeepVoidType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getKeepVoidType = { "getKeepVoidType", ZEND_ACC_PUBLIC, 1, getKeepVoidType_args, 1, &getKeepVoidType_return }; +inline constexpr reg::Arg doNotTreatPhpDocTypesAsCertain_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig doNotTreatPhpDocTypesAsCertain = { "doNotTreatPhpDocTypesAsCertain", ZEND_ACC_PUBLIC, 0, nullptr, 0, &doNotTreatPhpDocTypesAsCertain_return }; +inline constexpr reg::Arg promoteNativeTypes_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig promoteNativeTypes = { "promoteNativeTypes", ZEND_ACC_PRIVATE, 0, nullptr, 0, &promoteNativeTypes_return }; +inline constexpr reg::Arg resolveName_args[] = { reg::typed("name", 0, "PhpParser\\Node\\Name") }; +inline constexpr reg::Arg resolveName_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig resolveName = { "resolveName", ZEND_ACC_PUBLIC, 1, resolveName_args, 1, &resolveName_return }; +inline constexpr reg::Arg resolveTypeByName_args[] = { reg::typed("name", 0, "PhpParser\\Node\\Name") }; +inline constexpr reg::Arg resolveTypeByName_return = reg::typed("", 0, "PHPStan\\Type\\TypeWithClassName"); +inline constexpr reg::Sig resolveTypeByName = { "resolveTypeByName", ZEND_ACC_PUBLIC, 1, resolveTypeByName_args, 1, &resolveTypeByName_return }; +inline constexpr reg::Arg getTypeFromValue_args[] = { reg::typed("value", 0) }; +inline constexpr reg::Arg getTypeFromValue_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getTypeFromValue = { "getTypeFromValue", ZEND_ACC_PUBLIC, 1, getTypeFromValue_args, 1, &getTypeFromValue_return }; +inline constexpr reg::Arg hasExpressionType_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg hasExpressionType_return = reg::typed("", 0, "PHPStan\\TrinaryLogic"); +inline constexpr reg::Sig hasExpressionType = { "hasExpressionType", ZEND_ACC_PUBLIC, 1, hasExpressionType_args, 1, &hasExpressionType_return }; +inline constexpr reg::Arg getTrackedExpressionType_args[] = { reg::typed("node", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getTrackedExpressionType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getTrackedExpressionType = { "getTrackedExpressionType", ZEND_ACC_PUBLIC, 1, getTrackedExpressionType_args, 1, &getTrackedExpressionType_return }; +inline constexpr reg::Arg pushInFunctionCall_args[] = { reg::typed("reflection", 0), reg::typed("parameter", MAY_BE_NULL, "PHPStan\\Reflection\\ParameterReflection"), reg::typed("rememberTypes", MAY_BE_BOOL) }; +inline constexpr reg::Arg pushInFunctionCall_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig pushInFunctionCall = { "pushInFunctionCall", ZEND_ACC_PUBLIC, 3, pushInFunctionCall_args, 3, &pushInFunctionCall_return }; +inline constexpr reg::Arg popInFunctionCall_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig popInFunctionCall = { "popInFunctionCall", ZEND_ACC_PUBLIC, 0, nullptr, 0, &popInFunctionCall_return }; +inline constexpr reg::Arg isInClassExists_args[] = { reg::typed("className", MAY_BE_STRING) }; +inline constexpr reg::Arg isInClassExists_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInClassExists = { "isInClassExists", ZEND_ACC_PUBLIC, 1, isInClassExists_args, 1, &isInClassExists_return }; +inline constexpr reg::Arg getFunctionCallStack_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getFunctionCallStack = { "getFunctionCallStack", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFunctionCallStack_return }; +inline constexpr reg::Arg getFunctionCallStackWithParameters_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getFunctionCallStackWithParameters = { "getFunctionCallStackWithParameters", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getFunctionCallStackWithParameters_return }; +inline constexpr reg::Arg isInFunctionExists_args[] = { reg::typed("functionName", MAY_BE_STRING) }; +inline constexpr reg::Arg isInFunctionExists_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInFunctionExists = { "isInFunctionExists", ZEND_ACC_PUBLIC, 1, isInFunctionExists_args, 1, &isInFunctionExists_return }; +inline constexpr reg::Arg enterClass_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg enterClass_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterClass = { "enterClass", ZEND_ACC_PUBLIC, 1, enterClass_args, 1, &enterClass_return }; +inline constexpr reg::Arg enterTrait_args[] = { reg::typed("traitReflection", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg enterTrait_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterTrait = { "enterTrait", ZEND_ACC_PUBLIC, 1, enterTrait_args, 1, &enterTrait_return }; +inline constexpr reg::Arg enterClassMethod_args[] = { reg::typed("classMethod", 0, "PhpParser\\Node\\Stmt\\ClassMethod"), reg::typed("templateTypeMap", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"), reg::typed("phpDocParameterTypes", MAY_BE_ARRAY), reg::typed("phpDocReturnType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("throwType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("deprecatedDescription", MAY_BE_NULL | MAY_BE_STRING), reg::typed("isDeprecated", MAY_BE_BOOL), reg::typed("isInternal", MAY_BE_BOOL), reg::typed("isFinal", MAY_BE_BOOL), reg::typed("isPure", MAY_BE_NULL | MAY_BE_BOOL, nullptr, false, false, "null"), reg::typed("acceptsNamedArguments", MAY_BE_BOOL, nullptr, false, false, "true"), reg::typed("asserts", MAY_BE_NULL, "PHPStan\\Reflection\\Assertions", false, false, "null"), reg::typed("selfOutType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null"), reg::typed("phpDocComment", MAY_BE_NULL | MAY_BE_STRING, nullptr, false, false, "null"), reg::typed("parameterOutTypes", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("immediatelyInvokedCallableParameters", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("phpDocClosureThisTypeParameters", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("isConstructor", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("resolvedPhpDocBlock", MAY_BE_NULL, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock", false, false, "null"), reg::typed("phpDocPureUnlessCallableIsImpureParameters", MAY_BE_ARRAY, nullptr, false, false, "[]") }; +inline constexpr reg::Arg enterClassMethod_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterClassMethod = { "enterClassMethod", ZEND_ACC_PUBLIC, 9, enterClassMethod_args, 20, &enterClassMethod_return }; +inline constexpr reg::Arg enterPropertyHook_args[] = { reg::typed("hook", 0, "PhpParser\\Node\\PropertyHook"), reg::typed("propertyName", MAY_BE_STRING), reg::typed("nativePropertyTypeNode", MAY_BE_NULL, "PhpParser\\Node\\Identifier|PhpParser\\Node\\Name|PhpParser\\Node\\ComplexType"), reg::typed("phpDocPropertyType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("phpDocParameterTypes", MAY_BE_ARRAY), reg::typed("throwType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("deprecatedDescription", MAY_BE_NULL | MAY_BE_STRING), reg::typed("isDeprecated", MAY_BE_BOOL), reg::typed("isPure", MAY_BE_NULL | MAY_BE_BOOL), reg::typed("phpDocComment", MAY_BE_NULL | MAY_BE_STRING), reg::typed("resolvedPhpDocBlock", MAY_BE_NULL, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock", false, false, "null") }; +inline constexpr reg::Arg enterPropertyHook_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterPropertyHook = { "enterPropertyHook", ZEND_ACC_PUBLIC, 10, enterPropertyHook_args, 11, &enterPropertyHook_return }; +inline constexpr reg::Arg transformStaticType_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg transformStaticType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig transformStaticType = { "transformStaticType", ZEND_ACC_PRIVATE, 1, transformStaticType_args, 1, &transformStaticType_return }; +inline constexpr reg::Arg getRealParameterTypes_args[] = { reg::typed("functionLike", 0, "PhpParser\\Node\\FunctionLike") }; +inline constexpr reg::Arg getRealParameterTypes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getRealParameterTypes = { "getRealParameterTypes", ZEND_ACC_PRIVATE, 1, getRealParameterTypes_args, 1, &getRealParameterTypes_return }; +inline constexpr reg::Arg getRealParameterDefaultValues_args[] = { reg::typed("functionLike", 0, "PhpParser\\Node\\FunctionLike") }; +inline constexpr reg::Arg getRealParameterDefaultValues_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getRealParameterDefaultValues = { "getRealParameterDefaultValues", ZEND_ACC_PRIVATE, 1, getRealParameterDefaultValues_args, 1, &getRealParameterDefaultValues_return }; +inline constexpr reg::Arg getParameterAttributes_args[] = { reg::typed("functionLike", 0, "PhpParser\\Node\\Stmt\\ClassMethod|PhpParser\\Node\\Stmt\\Function_|PhpParser\\Node\\PropertyHook") }; +inline constexpr reg::Arg getParameterAttributes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getParameterAttributes = { "getParameterAttributes", ZEND_ACC_PRIVATE, 1, getParameterAttributes_args, 1, &getParameterAttributes_return }; +inline constexpr reg::Arg enterFunction_args[] = { reg::typed("function", 0, "PhpParser\\Node\\Stmt\\Function_"), reg::typed("templateTypeMap", 0, "PHPStan\\Type\\Generic\\TemplateTypeMap"), reg::typed("phpDocParameterTypes", MAY_BE_ARRAY), reg::typed("phpDocReturnType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("throwType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("deprecatedDescription", MAY_BE_NULL | MAY_BE_STRING), reg::typed("isDeprecated", MAY_BE_BOOL), reg::typed("isInternal", MAY_BE_BOOL), reg::typed("isPure", MAY_BE_NULL | MAY_BE_BOOL, nullptr, false, false, "null"), reg::typed("acceptsNamedArguments", MAY_BE_BOOL, nullptr, false, false, "true"), reg::typed("asserts", MAY_BE_NULL, "PHPStan\\Reflection\\Assertions", false, false, "null"), reg::typed("phpDocComment", MAY_BE_NULL | MAY_BE_STRING, nullptr, false, false, "null"), reg::typed("parameterOutTypes", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("immediatelyInvokedCallableParameters", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("phpDocClosureThisTypeParameters", MAY_BE_ARRAY, nullptr, false, false, "[]"), reg::typed("pureUnlessCallableIsImpureParameters", MAY_BE_ARRAY, nullptr, false, false, "[]") }; +inline constexpr reg::Arg enterFunction_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterFunction = { "enterFunction", ZEND_ACC_PUBLIC, 8, enterFunction_args, 16, &enterFunction_return }; +inline constexpr reg::Arg enterFunctionLike_args[] = { reg::typed("functionReflection", 0, "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection"), reg::typed("preserveConstructorScope", MAY_BE_BOOL) }; +inline constexpr reg::Arg enterFunctionLike_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterFunctionLike = { "enterFunctionLike", ZEND_ACC_PRIVATE, 2, enterFunctionLike_args, 2, &enterFunctionLike_return }; +inline constexpr reg::Arg enterNamespace_args[] = { reg::typed("namespaceName", MAY_BE_STRING) }; +inline constexpr reg::Arg enterNamespace_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterNamespace = { "enterNamespace", ZEND_ACC_PUBLIC, 1, enterNamespace_args, 1, &enterNamespace_return }; +inline constexpr reg::Arg enterClosureBind_args[] = { reg::typed("thisType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("nativeThisType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("scopeClasses", MAY_BE_ARRAY) }; +inline constexpr reg::Arg enterClosureBind_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterClosureBind = { "enterClosureBind", ZEND_ACC_PUBLIC, 3, enterClosureBind_args, 3, &enterClosureBind_return }; +inline constexpr reg::Arg restoreOriginalScopeAfterClosureBind_args[] = { reg::typed("originalScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg restoreOriginalScopeAfterClosureBind_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig restoreOriginalScopeAfterClosureBind = { "restoreOriginalScopeAfterClosureBind", ZEND_ACC_PUBLIC, 1, restoreOriginalScopeAfterClosureBind_args, 1, &restoreOriginalScopeAfterClosureBind_return }; +inline constexpr reg::Arg restoreThis_args[] = { reg::typed("restoreThisScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg restoreThis_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig restoreThis = { "restoreThis", ZEND_ACC_PUBLIC, 1, restoreThis_args, 1, &restoreThis_return }; +inline constexpr reg::Arg enterClosureCall_args[] = { reg::typed("thisType", 0, "PHPStan\\Type\\Type"), reg::typed("nativeThisType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg enterClosureCall_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterClosureCall = { "enterClosureCall", ZEND_ACC_PUBLIC, 2, enterClosureCall_args, 2, &enterClosureCall_return }; +inline constexpr reg::Arg isInClosureBind_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInClosureBind = { "isInClosureBind", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInClosureBind_return }; +inline constexpr reg::Arg withClosureBindScopeClasses_args[] = { reg::typed("scopeClasses", MAY_BE_ARRAY) }; +inline constexpr reg::Arg withClosureBindScopeClasses_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig withClosureBindScopeClasses = { "withClosureBindScopeClasses", ZEND_ACC_PUBLIC, 1, withClosureBindScopeClasses_args, 1, &withClosureBindScopeClasses_return }; +inline constexpr reg::Arg enterAnonymousFunction_args[] = { reg::typed("closure", 0, "PhpParser\\Node\\Expr\\Closure"), reg::typed("callableParameters", MAY_BE_NULL | MAY_BE_ARRAY), reg::typed("nativeCallableParameters", MAY_BE_NULL | MAY_BE_ARRAY, nullptr, false, false, "null") }; +inline constexpr reg::Arg enterAnonymousFunction_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterAnonymousFunction = { "enterAnonymousFunction", ZEND_ACC_PUBLIC, 2, enterAnonymousFunction_args, 3, &enterAnonymousFunction_return }; +inline constexpr reg::Arg enterAnonymousFunctionWithoutReflection_args[] = { reg::typed("closure", 0, "PhpParser\\Node\\Expr\\Closure"), reg::typed("callableParameters", MAY_BE_NULL | MAY_BE_ARRAY), reg::typed("nativeCallableParameters", MAY_BE_NULL | MAY_BE_ARRAY) }; +inline constexpr reg::Arg enterAnonymousFunctionWithoutReflection_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterAnonymousFunctionWithoutReflection = { "enterAnonymousFunctionWithoutReflection", ZEND_ACC_PUBLIC, 3, enterAnonymousFunctionWithoutReflection_args, 3, &enterAnonymousFunctionWithoutReflection_return }; +inline constexpr reg::Arg expressionTypeIsUnchangeable_args[] = { reg::typed("typeHolder", 0, "PHPStan\\Analyser\\ExpressionTypeHolder") }; +inline constexpr reg::Arg expressionTypeIsUnchangeable_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig expressionTypeIsUnchangeable = { "expressionTypeIsUnchangeable", ZEND_ACC_PRIVATE, 1, expressionTypeIsUnchangeable_args, 1, &expressionTypeIsUnchangeable_return }; +inline constexpr reg::Arg invalidateStaticExpressions_args[] = { reg::typed("expressionTypes", MAY_BE_ARRAY) }; +inline constexpr reg::Arg invalidateStaticExpressions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig invalidateStaticExpressions = { "invalidateStaticExpressions", ZEND_ACC_PRIVATE, 1, invalidateStaticExpressions_args, 1, &invalidateStaticExpressions_return }; +inline constexpr reg::Arg enterArrowFunction_args[] = { reg::typed("arrowFunction", 0, "PhpParser\\Node\\Expr\\ArrowFunction"), reg::typed("callableParameters", MAY_BE_NULL | MAY_BE_ARRAY), reg::typed("nativeCallableParameters", MAY_BE_NULL | MAY_BE_ARRAY, nullptr, false, false, "null") }; +inline constexpr reg::Arg enterArrowFunction_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterArrowFunction = { "enterArrowFunction", ZEND_ACC_PUBLIC, 2, enterArrowFunction_args, 3, &enterArrowFunction_return }; +inline constexpr reg::Arg enterArrowFunctionWithoutReflection_args[] = { reg::typed("arrowFunction", 0, "PhpParser\\Node\\Expr\\ArrowFunction"), reg::typed("callableParameters", MAY_BE_NULL | MAY_BE_ARRAY), reg::typed("nativeCallableParameters", MAY_BE_NULL | MAY_BE_ARRAY) }; +inline constexpr reg::Arg enterArrowFunctionWithoutReflection_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterArrowFunctionWithoutReflection = { "enterArrowFunctionWithoutReflection", ZEND_ACC_PUBLIC, 3, enterArrowFunctionWithoutReflection_args, 3, &enterArrowFunctionWithoutReflection_return }; +inline constexpr reg::Arg isParameterValueNullable_args[] = { reg::typed("parameter", 0, "PhpParser\\Node\\Param") }; +inline constexpr reg::Arg isParameterValueNullable_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isParameterValueNullable = { "isParameterValueNullable", ZEND_ACC_PUBLIC, 1, isParameterValueNullable_args, 1, &isParameterValueNullable_return }; +inline constexpr reg::Arg getFunctionType_args[] = { reg::typed("type", 0), reg::typed("isNullable", MAY_BE_BOOL), reg::typed("isVariadic", MAY_BE_BOOL) }; +inline constexpr reg::Arg getFunctionType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getFunctionType = { "getFunctionType", ZEND_ACC_PUBLIC, 3, getFunctionType_args, 3, &getFunctionType_return }; +inline constexpr reg::Arg getCallableParameterType_args[] = { reg::typed("parameter", 0, "PhpParser\\Node\\Param"), reg::typed("callableParameters", MAY_BE_ARRAY), reg::typed("index", MAY_BE_LONG) }; +inline constexpr reg::Arg getCallableParameterType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getCallableParameterType = { "getCallableParameterType", ZEND_ACC_PRIVATE, 3, getCallableParameterType_args, 3, &getCallableParameterType_return }; +inline constexpr reg::Arg buildVariadicArrayTypeFromCallableParameters_args[] = { reg::typed("callableParameters", MAY_BE_ARRAY), reg::typed("startIndex", MAY_BE_LONG) }; +inline constexpr reg::Arg buildVariadicArrayTypeFromCallableParameters_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig buildVariadicArrayTypeFromCallableParameters = { "buildVariadicArrayTypeFromCallableParameters", ZEND_ACC_PRIVATE, 2, buildVariadicArrayTypeFromCallableParameters_args, 2, &buildVariadicArrayTypeFromCallableParameters_return }; +inline constexpr reg::Arg intersectButNotNever_args[] = { reg::typed("nativeType", 0, "PHPStan\\Type\\Type"), reg::typed("inferredType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg intersectButNotNever_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig intersectButNotNever = { "intersectButNotNever", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, intersectButNotNever_args, 2, &intersectButNotNever_return }; +inline constexpr reg::Arg enterMatch_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr\\Match_"), reg::typed("condType", 0, "PHPStan\\Type\\Type"), reg::typed("condNativeType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg enterMatch_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterMatch = { "enterMatch", ZEND_ACC_PUBLIC, 3, enterMatch_args, 3, &enterMatch_return }; +inline constexpr reg::Arg enterForeach_args[] = { reg::typed("originalScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("iteratee", 0, "PhpParser\\Node\\Expr"), reg::typed("iterateeType", 0, "PHPStan\\Type\\Type"), reg::typed("nativeIterateeType", 0, "PHPStan\\Type\\Type"), reg::typed("valueName", MAY_BE_STRING), reg::typed("keyName", MAY_BE_NULL | MAY_BE_STRING), reg::typed("valueByRef", MAY_BE_BOOL) }; +inline constexpr reg::Arg enterForeach_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterForeach = { "enterForeach", ZEND_ACC_PUBLIC, 7, enterForeach_args, 7, &enterForeach_return }; +inline constexpr reg::Arg enterForeachKey_args[] = { reg::typed("originalScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("iteratee", 0, "PhpParser\\Node\\Expr"), reg::typed("iterateeType", 0, "PHPStan\\Type\\Type"), reg::typed("nativeIterateeType", 0, "PHPStan\\Type\\Type"), reg::typed("keyName", MAY_BE_STRING) }; +inline constexpr reg::Arg enterForeachKey_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterForeachKey = { "enterForeachKey", ZEND_ACC_PUBLIC, 5, enterForeachKey_args, 5, &enterForeachKey_return }; +inline constexpr reg::Arg enterCatchType_args[] = { reg::typed("catchType", 0, "PHPStan\\Type\\Type"), reg::typed("variableName", MAY_BE_NULL | MAY_BE_STRING) }; +inline constexpr reg::Arg enterCatchType_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterCatchType = { "enterCatchType", ZEND_ACC_PUBLIC, 2, enterCatchType_args, 2, &enterCatchType_return }; +inline constexpr reg::Arg enterExpressionAssign_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("isPlainWrite", MAY_BE_BOOL, nullptr, false, false, "true") }; +inline constexpr reg::Arg enterExpressionAssign_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig enterExpressionAssign = { "enterExpressionAssign", ZEND_ACC_PUBLIC, 1, enterExpressionAssign_args, 2, &enterExpressionAssign_return }; +inline constexpr reg::Arg exitExpressionAssign_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg exitExpressionAssign_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig exitExpressionAssign = { "exitExpressionAssign", ZEND_ACC_PUBLIC, 1, exitExpressionAssign_args, 1, &exitExpressionAssign_return }; +inline constexpr reg::Arg isInExpressionAssign_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg isInExpressionAssign_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInExpressionAssign = { "isInExpressionAssign", ZEND_ACC_PUBLIC, 1, isInExpressionAssign_args, 1, &isInExpressionAssign_return }; +inline constexpr reg::Arg isInWriteExpressionAssign_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg isInWriteExpressionAssign_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInWriteExpressionAssign = { "isInWriteExpressionAssign", ZEND_ACC_PUBLIC, 1, isInWriteExpressionAssign_args, 1, &isInWriteExpressionAssign_return }; +inline constexpr reg::Arg setAllowedUndefinedExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg setAllowedUndefinedExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig setAllowedUndefinedExpression = { "setAllowedUndefinedExpression", ZEND_ACC_PUBLIC, 1, setAllowedUndefinedExpression_args, 1, &setAllowedUndefinedExpression_return }; +inline constexpr reg::Arg unsetAllowedUndefinedExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg unsetAllowedUndefinedExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig unsetAllowedUndefinedExpression = { "unsetAllowedUndefinedExpression", ZEND_ACC_PUBLIC, 1, unsetAllowedUndefinedExpression_args, 1, &unsetAllowedUndefinedExpression_return }; +inline constexpr reg::Arg isUndefinedExpressionAllowed_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg isUndefinedExpressionAllowed_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isUndefinedExpressionAllowed = { "isUndefinedExpressionAllowed", ZEND_ACC_PUBLIC, 1, isUndefinedExpressionAllowed_args, 1, &isUndefinedExpressionAllowed_return }; +inline constexpr reg::Arg assignVariable_args[] = { reg::typed("variableName", MAY_BE_STRING), reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("nativeType", 0, "PHPStan\\Type\\Type"), reg::typed("certainty", 0, "PHPStan\\TrinaryLogic"), reg::typed("intertwinedPropagatedFrom", MAY_BE_ARRAY, nullptr, false, false, "[]") }; +inline constexpr reg::Arg assignVariable_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig assignVariable = { "assignVariable", ZEND_ACC_PUBLIC, 4, assignVariable_args, 5, &assignVariable_return }; +inline constexpr reg::Arg overwriteExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("nativeType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg overwriteExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig overwriteExpression = { "overwriteExpression", ZEND_ACC_PRIVATE, 3, overwriteExpression_args, 3, &overwriteExpression_return }; +inline constexpr reg::Arg resolveIntertwinedAssignedType_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("rootType", 0, "PHPStan\\Type\\Type"), reg::typed("assignedExpr", 0, "PhpParser\\Node\\Expr"), reg::typed("rootVariableName", MAY_BE_STRING), reg::typed("native", MAY_BE_BOOL) }; +inline constexpr reg::Arg resolveIntertwinedAssignedType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveIntertwinedAssignedType = { "resolveIntertwinedAssignedType", ZEND_ACC_PRIVATE, 5, resolveIntertwinedAssignedType_args, 5, &resolveIntertwinedAssignedType_return }; +inline constexpr reg::Arg isDimFetchPathReachable_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("dimFetch", 0, "PhpParser\\Node\\Expr\\ArrayDimFetch") }; +inline constexpr reg::Arg isDimFetchPathReachable_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isDimFetchPathReachable = { "isDimFetchPathReachable", ZEND_ACC_PRIVATE, 2, isDimFetchPathReachable_args, 2, &isDimFetchPathReachable_return }; +inline constexpr reg::Arg unsetExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg unsetExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig unsetExpression = { "unsetExpression", ZEND_ACC_PRIVATE, 1, unsetExpression_args, 1, &unsetExpression_return }; +inline constexpr reg::Arg getStateType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getStateType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getStateType = { "getStateType", ZEND_ACC_PUBLIC, 1, getStateType_args, 1, &getStateType_return }; +inline constexpr reg::Arg getScopeStateType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getScopeStateType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getScopeStateType = { "getScopeStateType", ZEND_ACC_PRIVATE, 1, getScopeStateType_args, 1, &getScopeStateType_return }; +inline constexpr reg::Arg getScopeStateNativeType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg getScopeStateNativeType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getScopeStateNativeType = { "getScopeStateNativeType", ZEND_ACC_PRIVATE, 1, getScopeStateNativeType_args, 1, &getScopeStateNativeType_return }; +inline constexpr reg::Arg resolveScopeStateType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("native", MAY_BE_BOOL) }; +inline constexpr reg::Arg resolveScopeStateType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig resolveScopeStateType = { "resolveScopeStateType", ZEND_ACC_PRIVATE, 2, resolveScopeStateType_args, 2, &resolveScopeStateType_return }; +inline constexpr reg::Arg specifyExpressionType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("nativeType", 0, "PHPStan\\Type\\Type"), reg::typed("certainty", 0, "PHPStan\\TrinaryLogic") }; +inline constexpr reg::Arg specifyExpressionType_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig specifyExpressionType = { "specifyExpressionType", ZEND_ACC_PUBLIC, 4, specifyExpressionType_args, 4, &specifyExpressionType_return }; +inline constexpr reg::Arg openSpecificationScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig openSpecificationScope = { "openSpecificationScope", ZEND_ACC_PRIVATE, 0, nullptr, 0, &openSpecificationScope_return }; +inline constexpr reg::Arg isSpecifyExpressionTypeNoop_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg isSpecifyExpressionTypeNoop_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isSpecifyExpressionTypeNoop = { "isSpecifyExpressionTypeNoop", ZEND_ACC_PRIVATE, 2, isSpecifyExpressionTypeNoop_args, 2, &isSpecifyExpressionTypeNoop_return }; +inline constexpr reg::Arg specifyExpressionTypeInPlace_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("nativeType", 0, "PHPStan\\Type\\Type"), reg::typed("certainty", 0, "PHPStan\\TrinaryLogic") }; +inline constexpr reg::Arg specifyExpressionTypeInPlace_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig specifyExpressionTypeInPlace = { "specifyExpressionTypeInPlace", ZEND_ACC_PRIVATE, 4, specifyExpressionTypeInPlace_args, 4, &specifyExpressionTypeInPlace_return }; +inline constexpr reg::Arg assignExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("nativeType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg assignExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig assignExpression = { "assignExpression", ZEND_ACC_PUBLIC, 3, assignExpression_args, 3, &assignExpression_return }; +inline constexpr reg::Arg assignInitializedProperty_args[] = { reg::typed("fetchedOnType", 0, "PHPStan\\Type\\Type"), reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg assignInitializedProperty_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig assignInitializedProperty = { "assignInitializedProperty", ZEND_ACC_PUBLIC, 2, assignInitializedProperty_args, 2, &assignInitializedProperty_return }; +inline constexpr reg::Arg invalidateExpression_args[] = { reg::typed("expressionToInvalidate", 0, "PhpParser\\Node\\Expr"), reg::typed("requireMoreCharacters", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("invalidatingClass", MAY_BE_NULL, "PHPStan\\Reflection\\ClassReflection", false, false, "null"), reg::typed("keepPropertyFetches", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg invalidateExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig invalidateExpression = { "invalidateExpression", ZEND_ACC_PUBLIC, 1, invalidateExpression_args, 4, &invalidateExpression_return }; +inline constexpr reg::Arg isPrivatePropertyOfDifferentClass_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("invalidatingClass", 0, "PHPStan\\Reflection\\ClassReflection") }; +inline constexpr reg::Arg isPrivatePropertyOfDifferentClass_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isPrivatePropertyOfDifferentClass = { "isPrivatePropertyOfDifferentClass", ZEND_ACC_PUBLIC, 2, isPrivatePropertyOfDifferentClass_args, 2, &isPrivatePropertyOfDifferentClass_return }; +inline constexpr reg::Arg invalidateMethodsOnExpression_args[] = { reg::typed("expressionToInvalidate", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg invalidateMethodsOnExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig invalidateMethodsOnExpression = { "invalidateMethodsOnExpression", ZEND_ACC_PRIVATE, 1, invalidateMethodsOnExpression_args, 1, &invalidateMethodsOnExpression_return }; +inline constexpr reg::Arg setExpressionCertaintyKeepingType_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("certainty", 0, "PHPStan\\TrinaryLogic") }; +inline constexpr reg::Arg setExpressionCertaintyKeepingType_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig setExpressionCertaintyKeepingType = { "setExpressionCertaintyKeepingType", ZEND_ACC_PRIVATE, 2, setExpressionCertaintyKeepingType_args, 2, &setExpressionCertaintyKeepingType_return }; +inline constexpr reg::Arg isComplexUnionType_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg isComplexUnionType_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isComplexUnionType = { "isComplexUnionType", ZEND_ACC_PRIVATE, 1, isComplexUnionType_args, 1, &isComplexUnionType_return }; +inline constexpr reg::Arg addTypeToExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg addTypeToExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig addTypeToExpression = { "addTypeToExpression", ZEND_ACC_PUBLIC, 2, addTypeToExpression_args, 2, &addTypeToExpression_return }; +inline constexpr reg::Arg removeTypeFromExpression_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("typeToRemove", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg removeTypeFromExpression_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig removeTypeFromExpression = { "removeTypeFromExpression", ZEND_ACC_PUBLIC, 2, removeTypeFromExpression_args, 2, &removeTypeFromExpression_return }; +inline constexpr reg::Arg filterByTruthyValue_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg filterByTruthyValue_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig filterByTruthyValue = { "filterByTruthyValue", ZEND_ACC_PUBLIC, 1, filterByTruthyValue_args, 1, &filterByTruthyValue_return }; +inline constexpr reg::Arg filterByFalseyValue_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg filterByFalseyValue_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig filterByFalseyValue = { "filterByFalseyValue", ZEND_ACC_PUBLIC, 1, filterByFalseyValue_args, 1, &filterByFalseyValue_return }; +inline constexpr reg::Arg applySpecifiedTypes_args[] = { reg::typed("specifiedTypes", 0, "PHPStan\\Analyser\\SpecifiedTypes") }; +inline constexpr reg::Arg applySpecifiedTypes_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig applySpecifiedTypes = { "applySpecifiedTypes", ZEND_ACC_PUBLIC, 1, applySpecifiedTypes_args, 1, &applySpecifiedTypes_return }; +inline constexpr reg::Arg processConditionalExpressionsAfterSpecifying_args[] = { reg::typed("specifiedExpressions", MAY_BE_ARRAY) }; +inline constexpr reg::Arg processConditionalExpressionsAfterSpecifying_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig processConditionalExpressionsAfterSpecifying = { "processConditionalExpressionsAfterSpecifying", ZEND_ACC_PRIVATE, 1, processConditionalExpressionsAfterSpecifying_args, 1, &processConditionalExpressionsAfterSpecifying_return }; +inline constexpr reg::Arg getConditionalExpressions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getConditionalExpressions = { "getConditionalExpressions", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getConditionalExpressions_return }; +inline constexpr reg::Arg addConditionalExpressions_args[] = { reg::typed("exprString", MAY_BE_STRING), reg::typed("conditionalExpressionHolders", MAY_BE_ARRAY) }; +inline constexpr reg::Arg addConditionalExpressions_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig addConditionalExpressions = { "addConditionalExpressions", ZEND_ACC_PUBLIC, 2, addConditionalExpressions_args, 2, &addConditionalExpressions_return }; +inline constexpr reg::Arg exitFirstLevelStatements_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig exitFirstLevelStatements = { "exitFirstLevelStatements", ZEND_ACC_PUBLIC, 0, nullptr, 0, &exitFirstLevelStatements_return }; +inline constexpr reg::Arg isInFirstLevelStatement_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isInFirstLevelStatement = { "isInFirstLevelStatement", ZEND_ACC_PUBLIC, 0, nullptr, 0, &isInFirstLevelStatement_return }; +inline constexpr reg::Arg mergeWith_args[] = { reg::typed("otherScope", MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"), reg::typed("preserveVacuousConditionals", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg mergeWith_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig mergeWith = { "mergeWith", ZEND_ACC_PUBLIC, 1, mergeWith_args, 2, &mergeWith_return }; +inline constexpr reg::Arg mergeWithVariableState_args[] = { reg::typed("otherScope", MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"), reg::typed("preserveVacuousConditionals", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg mergeWithVariableState_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig mergeWithVariableState = { "mergeWithVariableState", ZEND_ACC_PRIVATE, 1, mergeWithVariableState_args, 2, &mergeWithVariableState_return }; +inline constexpr reg::Arg withoutPreciseClassConstantFetches_args[] = { reg::typed("differingExpressionKeys", MAY_BE_ARRAY), reg::typed("ourExpressionTypes", MAY_BE_ARRAY), reg::typed("theirExpressionTypes", MAY_BE_ARRAY) }; +inline constexpr reg::Arg withoutPreciseClassConstantFetches_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig withoutPreciseClassConstantFetches = { "withoutPreciseClassConstantFetches", ZEND_ACC_PRIVATE, 3, withoutPreciseClassConstantFetches_args, 3, &withoutPreciseClassConstantFetches_return }; +inline constexpr reg::Arg preserveVacuousConditionalExpressions_args[] = { reg::typed("currentConditionalExpressions", MAY_BE_ARRAY), reg::typed("sourceConditionalExpressions", MAY_BE_ARRAY), reg::typed("otherExpressionTypes", MAY_BE_ARRAY) }; +inline constexpr reg::Arg preserveVacuousConditionalExpressions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig preserveVacuousConditionalExpressions = { "preserveVacuousConditionalExpressions", ZEND_ACC_PRIVATE, 3, preserveVacuousConditionalExpressions_args, 3, &preserveVacuousConditionalExpressions_return }; +inline constexpr reg::Arg mergeSameGuardConditionalExpressions_args[] = { reg::typed("currentConditionalExpressions", MAY_BE_ARRAY), reg::typed("ourConditionalExpressions", MAY_BE_ARRAY), reg::typed("theirConditionalExpressions", MAY_BE_ARRAY) }; +inline constexpr reg::Arg mergeSameGuardConditionalExpressions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig mergeSameGuardConditionalExpressions = { "mergeSameGuardConditionalExpressions", ZEND_ACC_PRIVATE, 3, mergeSameGuardConditionalExpressions_args, 3, &mergeSameGuardConditionalExpressions_return }; +inline constexpr reg::Arg mergeConditionalExpressions_args[] = { reg::typed("newConditionalExpressions", MAY_BE_ARRAY), reg::typed("existingConditionalExpressions", MAY_BE_ARRAY) }; +inline constexpr reg::Arg mergeConditionalExpressions_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig mergeConditionalExpressions = { "mergeConditionalExpressions", ZEND_ACC_PRIVATE, 2, mergeConditionalExpressions_args, 2, &mergeConditionalExpressions_return }; +inline constexpr reg::Arg mergeInitializedProperties_args[] = { reg::typed("calledMethodScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg mergeInitializedProperties_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig mergeInitializedProperties = { "mergeInitializedProperties", ZEND_ACC_PUBLIC, 1, mergeInitializedProperties_args, 1, &mergeInitializedProperties_return }; +inline constexpr reg::Arg processFinallyScope_args[] = { reg::typed("finallyScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("originalFinallyScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg processFinallyScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig processFinallyScope = { "processFinallyScope", ZEND_ACC_PUBLIC, 2, processFinallyScope_args, 2, &processFinallyScope_return }; +inline constexpr reg::Arg processFinallyScopeVariableTypeHolders_args[] = { reg::typed("ourVariableTypeHolders", MAY_BE_ARRAY), reg::typed("finallyVariableTypeHolders", MAY_BE_ARRAY), reg::typed("originalVariableTypeHolders", MAY_BE_ARRAY) }; +inline constexpr reg::Arg processFinallyScopeVariableTypeHolders_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig processFinallyScopeVariableTypeHolders = { "processFinallyScopeVariableTypeHolders", ZEND_ACC_PRIVATE, 3, processFinallyScopeVariableTypeHolders_args, 3, &processFinallyScopeVariableTypeHolders_return }; +inline constexpr reg::Arg processClosureScope_args[] = { reg::typed("closureScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("prevScope", MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"), reg::typed("byRefUses", MAY_BE_ARRAY) }; +inline constexpr reg::Arg processClosureScope_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig processClosureScope = { "processClosureScope", ZEND_ACC_PUBLIC, 3, processClosureScope_args, 3, &processClosureScope_return }; +inline constexpr reg::Arg processAlwaysIterableForeachScopeWithoutPollute_args[] = { reg::typed("finalScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg processAlwaysIterableForeachScopeWithoutPollute_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig processAlwaysIterableForeachScopeWithoutPollute = { "processAlwaysIterableForeachScopeWithoutPollute", ZEND_ACC_PUBLIC, 1, processAlwaysIterableForeachScopeWithoutPollute_args, 1, &processAlwaysIterableForeachScopeWithoutPollute_return }; +inline constexpr reg::Arg generalizeWith_args[] = { reg::typed("otherScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("writableVariableNames", MAY_BE_NULL | MAY_BE_ARRAY, nullptr, false, false, "null") }; +inline constexpr reg::Arg generalizeWith_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig generalizeWith = { "generalizeWith", ZEND_ACC_PUBLIC, 1, generalizeWith_args, 2, &generalizeWith_return }; +inline constexpr reg::Arg generalizeWithVariableState_args[] = { reg::typed("otherScope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("writableVariableNames", MAY_BE_NULL | MAY_BE_ARRAY) }; +inline constexpr reg::Arg generalizeWithVariableState_return = reg::typed("", 0, "PHPStan\\Analyser\\MutatingScope"); +inline constexpr reg::Sig generalizeWithVariableState = { "generalizeWithVariableState", ZEND_ACC_PRIVATE, 2, generalizeWithVariableState_args, 2, &generalizeWithVariableState_return }; +inline constexpr reg::Arg generalizeVariableTypeHolders_args[] = { reg::typed("variableTypeHolders", MAY_BE_ARRAY), reg::typed("otherVariableTypeHolders", MAY_BE_ARRAY), reg::typed("writableVariableNames", MAY_BE_NULL | MAY_BE_ARRAY) }; +inline constexpr reg::Arg generalizeVariableTypeHolders_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig generalizeVariableTypeHolders = { "generalizeVariableTypeHolders", ZEND_ACC_PRIVATE, 3, generalizeVariableTypeHolders_args, 3, &generalizeVariableTypeHolders_return }; +inline constexpr reg::Arg flattenUnionForGeneralization_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg flattenUnionForGeneralization_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig flattenUnionForGeneralization = { "flattenUnionForGeneralization", ZEND_ACC_PRIVATE, 1, flattenUnionForGeneralization_args, 1, &flattenUnionForGeneralization_return }; +inline constexpr reg::Arg generalizeType_args[] = { reg::typed("a", 0, "PHPStan\\Type\\Type"), reg::typed("b", 0, "PHPStan\\Type\\Type"), reg::typed("depth", MAY_BE_LONG) }; +inline constexpr reg::Arg generalizeType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig generalizeType = { "generalizeType", ZEND_ACC_PRIVATE, 3, generalizeType_args, 3, &generalizeType_return }; +inline constexpr reg::Arg getArrayDepth_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg getArrayDepth_return = reg::typed("", MAY_BE_LONG); +inline constexpr reg::Sig getArrayDepth = { "getArrayDepth", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 1, getArrayDepth_args, 1, &getArrayDepth_return }; +inline constexpr reg::Arg equals_args[] = { reg::typed("otherScope", 0, "PHPStan\\Analyser\\MutatingScope") }; +inline constexpr reg::Arg equals_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig equals = { "equals", ZEND_ACC_PUBLIC, 1, equals_args, 1, &equals_return }; +inline constexpr reg::Arg compareConditionalExpressions_args[] = { reg::typed("conditionalExpressions", MAY_BE_ARRAY), reg::typed("otherConditionalExpressions", MAY_BE_ARRAY) }; +inline constexpr reg::Arg compareConditionalExpressions_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig compareConditionalExpressions = { "compareConditionalExpressions", ZEND_ACC_PRIVATE, 2, compareConditionalExpressions_args, 2, &compareConditionalExpressions_return }; +inline constexpr reg::Arg compareVariableTypeHolders_args[] = { reg::typed("variableTypeHolders", MAY_BE_ARRAY), reg::typed("otherVariableTypeHolders", MAY_BE_ARRAY) }; +inline constexpr reg::Arg compareVariableTypeHolders_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig compareVariableTypeHolders = { "compareVariableTypeHolders", ZEND_ACC_PRIVATE, 2, compareVariableTypeHolders_args, 2, &compareVariableTypeHolders_return }; +inline constexpr reg::Arg canAccessProperty_args[] = { reg::typed("propertyReflection", 0, "PHPStan\\Reflection\\PropertyReflection") }; +inline constexpr reg::Arg canAccessProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canAccessProperty = { "canAccessProperty", ZEND_ACC_PUBLIC, 1, canAccessProperty_args, 1, &canAccessProperty_return }; +inline constexpr reg::Arg canReadProperty_args[] = { reg::typed("propertyReflection", 0, "PHPStan\\Reflection\\ExtendedPropertyReflection") }; +inline constexpr reg::Arg canReadProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canReadProperty = { "canReadProperty", ZEND_ACC_PUBLIC, 1, canReadProperty_args, 1, &canReadProperty_return }; +inline constexpr reg::Arg canWriteProperty_args[] = { reg::typed("propertyReflection", 0, "PHPStan\\Reflection\\ExtendedPropertyReflection") }; +inline constexpr reg::Arg canWriteProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canWriteProperty = { "canWriteProperty", ZEND_ACC_PUBLIC, 1, canWriteProperty_args, 1, &canWriteProperty_return }; +inline constexpr reg::Arg canCallMethod_args[] = { reg::typed("methodReflection", 0, "PHPStan\\Reflection\\MethodReflection") }; +inline constexpr reg::Arg canCallMethod_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canCallMethod = { "canCallMethod", ZEND_ACC_PUBLIC, 1, canCallMethod_args, 1, &canCallMethod_return }; +inline constexpr reg::Arg canAccessConstant_args[] = { reg::typed("constantReflection", 0, "PHPStan\\Reflection\\ClassConstantReflection") }; +inline constexpr reg::Arg canAccessConstant_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canAccessConstant = { "canAccessConstant", ZEND_ACC_PUBLIC, 1, canAccessConstant_args, 1, &canAccessConstant_return }; +inline constexpr reg::Arg canAccessClassMember_args[] = { reg::typed("classMemberReflection", 0, "PHPStan\\Reflection\\ClassMemberReflection") }; +inline constexpr reg::Arg canAccessClassMember_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig canAccessClassMember = { "canAccessClassMember", ZEND_ACC_PRIVATE, 1, canAccessClassMember_args, 1, &canAccessClassMember_return }; +inline constexpr reg::Arg debug_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig debug = { "debug", ZEND_ACC_PUBLIC, 0, nullptr, 0, &debug_return }; +inline constexpr reg::Arg filterTypeWithMethod_args[] = { reg::typed("typeWithMethod", 0, "PHPStan\\Type\\Type"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg filterTypeWithMethod_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig filterTypeWithMethod = { "filterTypeWithMethod", ZEND_ACC_PUBLIC, 2, filterTypeWithMethod_args, 2, &filterTypeWithMethod_return }; +inline constexpr reg::Arg getMethodReflection_args[] = { reg::typed("typeWithMethod", 0, "PHPStan\\Type\\Type"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg getMethodReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getMethodReflection = { "getMethodReflection", ZEND_ACC_PUBLIC, 2, getMethodReflection_args, 2, &getMethodReflection_return }; +inline constexpr reg::Arg getNakedMethod_args[] = { reg::typed("typeWithMethod", 0, "PHPStan\\Type\\Type"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg getNakedMethod_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getNakedMethod = { "getNakedMethod", ZEND_ACC_PUBLIC, 2, getNakedMethod_args, 2, &getNakedMethod_return }; +inline constexpr reg::Arg getPropertyReflection_args[] = { reg::typed("typeWithProperty", 0, "PHPStan\\Type\\Type"), reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg getPropertyReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig getPropertyReflection = { "getPropertyReflection", ZEND_ACC_PUBLIC, 2, getPropertyReflection_args, 2, &getPropertyReflection_return }; +inline constexpr reg::Arg getInstancePropertyReflection_args[] = { reg::typed("typeWithProperty", 0, "PHPStan\\Type\\Type"), reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg getInstancePropertyReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig getInstancePropertyReflection = { "getInstancePropertyReflection", ZEND_ACC_PUBLIC, 2, getInstancePropertyReflection_args, 2, &getInstancePropertyReflection_return }; +inline constexpr reg::Arg getStaticPropertyReflection_args[] = { reg::typed("typeWithProperty", 0, "PHPStan\\Type\\Type"), reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg getStaticPropertyReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ExtendedPropertyReflection"); +inline constexpr reg::Sig getStaticPropertyReflection = { "getStaticPropertyReflection", ZEND_ACC_PUBLIC, 2, getStaticPropertyReflection_args, 2, &getStaticPropertyReflection_return }; +inline constexpr reg::Arg getConstantReflection_args[] = { reg::typed("typeWithConstant", 0, "PHPStan\\Type\\Type"), reg::typed("constantName", MAY_BE_STRING) }; +inline constexpr reg::Arg getConstantReflection_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Reflection\\ClassConstantReflection"); +inline constexpr reg::Sig getConstantReflection = { "getConstantReflection", ZEND_ACC_PUBLIC, 2, getConstantReflection_args, 2, &getConstantReflection_return }; +inline constexpr reg::Arg getConstantExplicitTypeFromConfig_args[] = { reg::typed("constantName", MAY_BE_STRING), reg::typed("constantType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg getConstantExplicitTypeFromConfig_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getConstantExplicitTypeFromConfig = { "getConstantExplicitTypeFromConfig", ZEND_ACC_PUBLIC, 2, getConstantExplicitTypeFromConfig_args, 2, &getConstantExplicitTypeFromConfig_return }; +inline constexpr reg::Arg getConstantTypes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getConstantTypes = { "getConstantTypes", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getConstantTypes_return }; +inline constexpr reg::Arg getGlobalConstantType_args[] = { reg::typed("name", 0, "PhpParser\\Node\\Name") }; +inline constexpr reg::Arg getGlobalConstantType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getGlobalConstantType = { "getGlobalConstantType", ZEND_ACC_PRIVATE, 1, getGlobalConstantType_args, 1, &getGlobalConstantType_return }; +inline constexpr reg::Arg createGlobalConstantFetches_args[] = { reg::typed("name", 0, "PhpParser\\Node\\Name"), reg::typed("nameString", MAY_BE_STRING), reg::typed("namespace", MAY_BE_NULL | MAY_BE_STRING) }; +inline constexpr reg::Arg createGlobalConstantFetches_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig createGlobalConstantFetches = { "createGlobalConstantFetches", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 3, createGlobalConstantFetches_args, 3, &createGlobalConstantFetches_return }; +inline constexpr reg::Arg getNativeConstantTypes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig getNativeConstantTypes = { "getNativeConstantTypes", ZEND_ACC_PRIVATE, 0, nullptr, 0, &getNativeConstantTypes_return }; +inline constexpr reg::Arg getIterableKeyType_args[] = { reg::typed("iteratee", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg getIterableKeyType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getIterableKeyType = { "getIterableKeyType", ZEND_ACC_PUBLIC, 1, getIterableKeyType_args, 1, &getIterableKeyType_return }; +inline constexpr reg::Arg getIterableValueType_args[] = { reg::typed("iteratee", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg getIterableValueType_return = reg::typed("", 0, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getIterableValueType = { "getIterableValueType", ZEND_ACC_PUBLIC, 1, getIterableValueType_args, 1, &getIterableValueType_return }; +inline constexpr reg::Arg getPhpVersion_return = reg::typed("", 0, "PHPStan\\Php\\PhpVersions"); +inline constexpr reg::Sig getPhpVersion = { "getPhpVersion", ZEND_ACC_PUBLIC, 0, nullptr, 0, &getPhpVersion_return }; +inline constexpr reg::Arg isOverallPhpVersionRange_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg isOverallPhpVersionRange_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig isOverallPhpVersionRange = { "isOverallPhpVersionRange", ZEND_ACC_PRIVATE, 1, isOverallPhpVersionRange_args, 1, &isOverallPhpVersionRange_return }; +inline constexpr reg::Arg invokeNodeCallback_args[] = { reg::typed("node", 0, "PhpParser\\Node") }; +inline constexpr reg::Arg invokeNodeCallback_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig invokeNodeCallback = { "invokeNodeCallback", ZEND_ACC_PUBLIC, 1, invokeNodeCallback_args, 1, &invokeNodeCallback_return }; +inline constexpr reg::Arg emitCollectedData_args[] = { reg::typed("collectorType", MAY_BE_STRING), reg::typed("data", MAY_BE_ANY) }; +inline constexpr reg::Arg emitCollectedData_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig emitCollectedData = { "emitCollectedData", ZEND_ACC_PUBLIC, 2, emitCollectedData_args, 2, &emitCollectedData_return }; +} // namespace sig + +} // namespace ptdecl::MutatingScope + +#endif diff --git a/turbo-ext/src/generated/PhpClassReflectionExtension.h b/turbo-ext/src/generated/PhpClassReflectionExtension.h new file mode 100644 index 00000000000..debdd292afc --- /dev/null +++ b/turbo-ext/src/generated/PhpClassReflectionExtension.h @@ -0,0 +1,141 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Reflection/Php/PhpClassReflectionExtension.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_PHP_CLASS_REFLECTION_EXTENSION_H +#define PHPSTANTURBO_GENERATED_PHP_CLASS_REFLECTION_EXTENSION_H + +#include "../reg.h" + +namespace ptdecl::PhpClassReflectionExtension { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t memberCacheOrder = 0; +inline constexpr uint32_t propertiesIncludingAnnotations = 1; +inline constexpr uint32_t nativeProperties = 2; +inline constexpr uint32_t methodsIncludingAnnotations = 3; +inline constexpr uint32_t nativeMethods = 4; +inline constexpr uint32_t propertyTypesCache = 5; +inline constexpr uint32_t inferClassConstructorPropertyTypesInProcess = 6; +inline constexpr uint32_t scopeFactory = 7; +inline constexpr uint32_t phpDocsResolver = 8; +inline constexpr uint32_t nodeScopeResolver = 9; +inline constexpr uint32_t methodReflectionFactory = 10; +inline constexpr uint32_t phpDocInheritanceResolver = 11; +inline constexpr uint32_t deprecationProvider = 12; +inline constexpr uint32_t annotationsMethodsClassReflectionExtension = 13; +inline constexpr uint32_t annotationsPropertiesClassReflectionExtension = 14; +inline constexpr uint32_t signatureMapProvider = 15; +inline constexpr uint32_t parser = 16; +inline constexpr uint32_t stubPhpDocProvider = 17; +inline constexpr uint32_t reflectionProviderProvider = 18; +inline constexpr uint32_t fileTypeMapper = 19; +inline constexpr uint32_t attributeReflectionFactory = 20; +inline constexpr uint32_t allowedConstantsMapProvider = 21; +inline constexpr uint32_t inferPrivatePropertyTypeFromConstructor = 22; +inline constexpr uint32_t phpVersion = 23; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("memberCacheOrder", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Internal\\LruCache"); + cls.property("propertiesIncludingAnnotations", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("nativeProperties", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("methodsIncludingAnnotations", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("nativeMethods", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("propertyTypesCache", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("inferClassConstructorPropertyTypesInProcess", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("scopeFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ScopeFactory"); + cls.property("phpDocsResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\PhpDocsResolver"); + cls.property("nodeScopeResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\NodeScopeResolver"); + cls.property("methodReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\Php\\PhpMethodReflectionFactory"); + cls.property("phpDocInheritanceResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\PhpDoc\\PhpDocInheritanceResolver"); + cls.property("deprecationProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\Deprecation\\DeprecationProvider"); + cls.property("annotationsMethodsClassReflectionExtension", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\Annotations\\AnnotationsMethodsClassReflectionExtension"); + cls.property("annotationsPropertiesClassReflectionExtension", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\Annotations\\AnnotationsPropertiesClassReflectionExtension"); + cls.property("signatureMapProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\SignatureMap\\SignatureMapProvider"); + cls.property("parser", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Parser\\Parser"); + cls.property("stubPhpDocProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\PhpDoc\\StubPhpDocProvider"); + cls.property("reflectionProviderProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\ReflectionProvider\\ReflectionProviderProvider"); + cls.property("fileTypeMapper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Type\\FileTypeMapper"); + cls.property("attributeReflectionFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\AttributeReflectionFactory"); + cls.property("allowedConstantsMapProvider", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\ParameterAllowedConstantsMapProvider"); + cls.property("inferPrivatePropertyTypeFromConstructor", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Php\\PhpVersion"); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg __construct_args[] = { reg::typed("scopeFactory", 0, "PHPStan\\Analyser\\ScopeFactory"), reg::typed("phpDocsResolver", 0, "PHPStan\\Analyser\\PhpDocsResolver"), reg::typed("nodeScopeResolver", 0, "PHPStan\\Analyser\\NodeScopeResolver"), reg::typed("methodReflectionFactory", 0, "PHPStan\\Reflection\\Php\\PhpMethodReflectionFactory"), reg::typed("phpDocInheritanceResolver", 0, "PHPStan\\PhpDoc\\PhpDocInheritanceResolver"), reg::typed("deprecationProvider", 0, "PHPStan\\Reflection\\Deprecation\\DeprecationProvider"), reg::typed("annotationsMethodsClassReflectionExtension", 0, "PHPStan\\Reflection\\Annotations\\AnnotationsMethodsClassReflectionExtension"), reg::typed("annotationsPropertiesClassReflectionExtension", 0, "PHPStan\\Reflection\\Annotations\\AnnotationsPropertiesClassReflectionExtension"), reg::typed("signatureMapProvider", 0, "PHPStan\\Reflection\\SignatureMap\\SignatureMapProvider"), reg::typed("parser", 0, "PHPStan\\Parser\\Parser"), reg::typed("stubPhpDocProvider", 0, "PHPStan\\PhpDoc\\StubPhpDocProvider"), reg::typed("reflectionProviderProvider", 0, "PHPStan\\Reflection\\ReflectionProvider\\ReflectionProviderProvider"), reg::typed("fileTypeMapper", 0, "PHPStan\\Type\\FileTypeMapper"), reg::typed("attributeReflectionFactory", 0, "PHPStan\\Reflection\\AttributeReflectionFactory"), reg::typed("allowedConstantsMapProvider", 0, "PHPStan\\Reflection\\ParameterAllowedConstantsMapProvider"), reg::typed("inferPrivatePropertyTypeFromConstructor", MAY_BE_BOOL), reg::typed("phpVersion", 0, "PHPStan\\Php\\PhpVersion"), reg::typed("memberCacheKeysMax", MAY_BE_LONG) }; +inline constexpr reg::Sig __construct = { "__construct", ZEND_ACC_PUBLIC, 18, __construct_args, 18, nullptr }; +inline constexpr reg::Arg touchMemberCacheKey_args[] = { reg::typed("cacheKey", MAY_BE_STRING) }; +inline constexpr reg::Arg touchMemberCacheKey_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig touchMemberCacheKey = { "touchMemberCacheKey", ZEND_ACC_PRIVATE, 1, touchMemberCacheKey_args, 1, &touchMemberCacheKey_return }; +inline constexpr reg::Arg hasProperty_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasProperty_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasProperty = { "hasProperty", ZEND_ACC_PUBLIC, 2, hasProperty_args, 2, &hasProperty_return }; +inline constexpr reg::Arg getProperty_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("propertyName", MAY_BE_STRING), reg::typed("scope", 0, "PHPStan\\Reflection\\ClassMemberAccessAnswerer") }; +inline constexpr reg::Arg getProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\Php\\PhpPropertyReflection"); +inline constexpr reg::Sig getProperty = { "getProperty", ZEND_ACC_PUBLIC, 3, getProperty_args, 3, &getProperty_return }; +inline constexpr reg::Arg getNativeProperty_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("propertyName", MAY_BE_STRING) }; +inline constexpr reg::Arg getNativeProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\Php\\PhpPropertyReflection"); +inline constexpr reg::Sig getNativeProperty = { "getNativeProperty", ZEND_ACC_PUBLIC, 2, getNativeProperty_args, 2, &getNativeProperty_return }; +inline constexpr reg::Arg createProperty_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("propertyName", MAY_BE_STRING), reg::typed("scope", 0, "PHPStan\\Reflection\\ClassMemberAccessAnswerer"), reg::typed("includingAnnotations", MAY_BE_BOOL) }; +inline constexpr reg::Arg createProperty_return = reg::typed("", 0, "PHPStan\\Reflection\\Php\\PhpPropertyReflection"); +inline constexpr reg::Sig createProperty = { "createProperty", ZEND_ACC_PRIVATE, 4, createProperty_args, 4, &createProperty_return }; +inline constexpr reg::Arg hasMethod_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasMethod_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasMethod = { "hasMethod", ZEND_ACC_PUBLIC, 2, hasMethod_args, 2, &hasMethod_return }; +inline constexpr reg::Arg getMethod_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg getMethod_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getMethod = { "getMethod", ZEND_ACC_PUBLIC, 2, getMethod_args, 2, &getMethod_return }; +inline constexpr reg::Arg hasNativeMethod_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg hasNativeMethod_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig hasNativeMethod = { "hasNativeMethod", ZEND_ACC_PUBLIC, 2, hasNativeMethod_args, 2, &hasNativeMethod_return }; +inline constexpr reg::Arg getNativeMethod_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodName", MAY_BE_STRING) }; +inline constexpr reg::Arg getNativeMethod_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig getNativeMethod = { "getNativeMethod", ZEND_ACC_PUBLIC, 2, getNativeMethod_args, 2, &getNativeMethod_return }; +inline constexpr reg::Arg createMethod_args[] = { reg::typed("classReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodName", MAY_BE_STRING), reg::typed("methodReflection", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod"), reg::typed("includingAnnotations", MAY_BE_BOOL) }; +inline constexpr reg::Arg createMethod_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedMethodReflection"); +inline constexpr reg::Sig createMethod = { "createMethod", ZEND_ACC_PRIVATE, 4, createMethod_args, 4, &createMethod_return }; +inline constexpr reg::Arg createUserlandMethodReflection_args[] = { reg::typed("fileDeclaringClass", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("actualDeclaringClass", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodReflection", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod"), reg::typed("declaringTraitName", MAY_BE_NULL | MAY_BE_STRING) }; +inline constexpr reg::Arg createUserlandMethodReflection_return = reg::typed("", 0, "PHPStan\\Reflection\\Php\\PhpMethodReflection"); +inline constexpr reg::Sig createUserlandMethodReflection = { "createUserlandMethodReflection", ZEND_ACC_PUBLIC, 4, createUserlandMethodReflection_args, 4, &createUserlandMethodReflection_return }; +inline constexpr reg::Arg createNativeMethodVariant_args[] = { reg::typed("declaringClassName", MAY_BE_STRING), reg::typed("methodName", MAY_BE_STRING), reg::typed("methodSignature", 0, "PHPStan\\Reflection\\SignatureMap\\FunctionSignature"), reg::typed("phpDocParameterTypes", MAY_BE_ARRAY), reg::typed("phpDocReturnType", MAY_BE_NULL, "PHPStan\\Type\\Type"), reg::typed("phpDocParameterNameMapping", MAY_BE_ARRAY), reg::typed("phpDocParameterOutTypes", MAY_BE_ARRAY), reg::typed("immediatelyInvokedCallableParameters", MAY_BE_ARRAY), reg::typed("closureThisParameters", MAY_BE_ARRAY), reg::typed("phpDocFromStubs", MAY_BE_BOOL), reg::typed("usePhpDocParameterNames", MAY_BE_BOOL) }; +inline constexpr reg::Arg createNativeMethodVariant_return = reg::typed("", 0, "PHPStan\\Reflection\\ExtendedFunctionVariant"); +inline constexpr reg::Sig createNativeMethodVariant = { "createNativeMethodVariant", ZEND_ACC_PRIVATE, 11, createNativeMethodVariant_args, 11, &createNativeMethodVariant_return }; +inline constexpr reg::Arg findPropertyTrait_args[] = { reg::typed("propertyReflection", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionProperty") }; +inline constexpr reg::Arg findPropertyTrait_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig findPropertyTrait = { "findPropertyTrait", ZEND_ACC_PRIVATE, 1, findPropertyTrait_args, 1, &findPropertyTrait_return }; +inline constexpr reg::Arg findMethodTrait_args[] = { reg::typed("methodReflection", 0, "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod") }; +inline constexpr reg::Arg findMethodTrait_return = reg::typed("", MAY_BE_NULL | MAY_BE_STRING); +inline constexpr reg::Sig findMethodTrait = { "findMethodTrait", ZEND_ACC_PRIVATE, 1, findMethodTrait_args, 1, &findMethodTrait_return }; +inline constexpr reg::Arg inferPrivatePropertyType_args[] = { reg::typed("propertyName", MAY_BE_STRING), reg::typed("constructor", 0, "PHPStan\\Reflection\\MethodReflection") }; +inline constexpr reg::Arg inferPrivatePropertyType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig inferPrivatePropertyType = { "inferPrivatePropertyType", ZEND_ACC_PRIVATE, 2, inferPrivatePropertyType_args, 2, &inferPrivatePropertyType_return }; +inline constexpr reg::Arg inferAndCachePropertyTypes_args[] = { reg::typed("constructor", 0, "PHPStan\\Reflection\\MethodReflection") }; +inline constexpr reg::Arg inferAndCachePropertyTypes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig inferAndCachePropertyTypes = { "inferAndCachePropertyTypes", ZEND_ACC_PRIVATE, 1, inferAndCachePropertyTypes_args, 1, &inferAndCachePropertyTypes_return }; +inline constexpr reg::Arg findClassNode_args[] = { reg::typed("className", MAY_BE_STRING), reg::typed("nodes", MAY_BE_ARRAY) }; +inline constexpr reg::Arg findClassNode_return = reg::typed("", MAY_BE_NULL, "PhpParser\\Node\\Stmt\\Class_"); +inline constexpr reg::Sig findClassNode = { "findClassNode", ZEND_ACC_PRIVATE, 2, findClassNode_args, 2, &findClassNode_return }; +inline constexpr reg::Arg findConstructorNode_args[] = { reg::typed("methodName", MAY_BE_STRING), reg::typed("classStatements", MAY_BE_ARRAY) }; +inline constexpr reg::Arg findConstructorNode_return = reg::typed("", MAY_BE_NULL, "PhpParser\\Node\\Stmt\\ClassMethod"); +inline constexpr reg::Sig findConstructorNode = { "findConstructorNode", ZEND_ACC_PRIVATE, 2, findConstructorNode_args, 2, &findConstructorNode_return }; +inline constexpr reg::Arg getPhpDocReturnType_args[] = { reg::typed("phpDocBlockClassReflection", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("resolvedPhpDoc", 0, "PHPStan\\PhpDoc\\ResolvedPhpDocBlock"), reg::typed("nativeReturnType", 0, "PHPStan\\Type\\Type") }; +inline constexpr reg::Arg getPhpDocReturnType_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Type\\Type"); +inline constexpr reg::Sig getPhpDocReturnType = { "getPhpDocReturnType", ZEND_ACC_PRIVATE, 3, getPhpDocReturnType_args, 3, &getPhpDocReturnType_return }; +inline constexpr reg::Arg findMethodPhpDocIncludingAncestors_args[] = { reg::typed("declaringClass", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("implementingClass", 0, "PHPStan\\Reflection\\ClassReflection"), reg::typed("methodName", MAY_BE_STRING), reg::typed("positionalParameterNames", MAY_BE_ARRAY) }; +inline constexpr reg::Arg findMethodPhpDocIncludingAncestors_return = reg::typed("", MAY_BE_NULL | MAY_BE_ARRAY); +inline constexpr reg::Sig findMethodPhpDocIncludingAncestors = { "findMethodPhpDocIncludingAncestors", ZEND_ACC_PRIVATE, 4, findMethodPhpDocIncludingAncestors_args, 4, &findMethodPhpDocIncludingAncestors_return }; +} // namespace sig + +} // namespace ptdecl::PhpClassReflectionExtension + +#endif diff --git a/turbo-ext/src/generated/VariableFlow.h b/turbo-ext/src/generated/VariableFlow.h new file mode 100644 index 00000000000..3338ae384e7 --- /dev/null +++ b/turbo-ext/src/generated/VariableFlow.h @@ -0,0 +1,89 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/VariableFlow.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_VARIABLE_FLOW_H +#define PHPSTANTURBO_GENERATED_VARIABLE_FLOW_H + +#include "../reg.h" + +namespace ptdecl::VariableFlow { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t kind = 0; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.abstract_(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("kind", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY, reg::PropertyKind::Typed, MAY_BE_STRING); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg __construct_args[] = { reg::typed("kind", MAY_BE_STRING) }; +inline constexpr reg::Sig __construct = { "__construct", ZEND_ACC_PROTECTED, 1, __construct_args, 1, nullptr }; +inline constexpr reg::Arg sequence_args[] = { reg::typed("flows", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow", false, true) }; +inline constexpr reg::Arg sequence_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig sequence = { "sequence", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 0, sequence_args, 1, &sequence_return }; +inline constexpr reg::Arg choice_args[] = { reg::typed("branches", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow", false, true) }; +inline constexpr reg::Arg choice_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig choice = { "choice", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 0, choice_args, 1, &choice_return }; +inline constexpr reg::Arg arrow_args[] = { reg::typed("arrow", 0, "PhpParser\\Node\\Expr\\ArrowFunction"), reg::typed("body", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("outputs", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow") }; +inline constexpr reg::Arg arrow_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig arrow = { "arrow", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 3, arrow_args, 3, &arrow_return }; +inline constexpr reg::Arg read_args[] = { reg::typed("name", MAY_BE_STRING), reg::typed("targetId", MAY_BE_NULL | MAY_BE_LONG, nullptr, false, false, "null"), reg::typed("container", MAY_BE_BOOL, nullptr, false, false, "false"), reg::typed("offset", 0, nullptr, false, false, "null") }; +inline constexpr reg::Arg read_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig read = { "read", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, read_args, 4, &read_return }; +inline constexpr reg::Arg conditional_args[] = { reg::typed("condition", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("if", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("else", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("truthy", MAY_BE_NULL | MAY_BE_BOOL) }; +inline constexpr reg::Arg conditional_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig conditional = { "conditional", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 4, conditional_args, 4, &conditional_return }; +inline constexpr reg::Arg switch__args[] = { reg::typed("condition", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("cases", MAY_BE_ARRAY), reg::typed("exhaustive", MAY_BE_BOOL) }; +inline constexpr reg::Arg switch__return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig switch_ = { "switch", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 3, switch__args, 3, &switch__return }; +inline constexpr reg::Arg write_args[] = { reg::typed("write", 0, "PHPStan\\Node\\Variable\\VariableWrite"), reg::typed("redundantType", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null") }; +inline constexpr reg::Arg write_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig write = { "write", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, write_args, 2, &write_return }; +inline constexpr reg::Arg discard_args[] = { reg::typed("write", 0, "PHPStan\\Node\\Variable\\VariableWrite") }; +inline constexpr reg::Arg discard_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig discard = { "discard", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, discard_args, 1, &discard_return }; +inline constexpr reg::Arg inputs_args[] = { reg::typed("writeId", MAY_BE_LONG), reg::typed("targetId", MAY_BE_NULL | MAY_BE_LONG) }; +inline constexpr reg::Arg inputs_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig inputs = { "inputs", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, inputs_args, 2, &inputs_return }; +inline constexpr reg::Arg escape_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg escape_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig escape = { "escape", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, escape_args, 1, &escape_return }; +inline constexpr reg::Arg mention_args[] = { reg::typed("name", MAY_BE_STRING) }; +inline constexpr reg::Arg mention_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig mention = { "mention", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, mention_args, 1, &mention_return }; +inline constexpr reg::Arg all_args[] = { reg::typed("kind", MAY_BE_STRING) }; +inline constexpr reg::Arg all_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig all = { "all", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, all_args, 1, &all_return }; +inline constexpr reg::Arg exit_args[] = { reg::typed("kind", MAY_BE_STRING), reg::typed("level", MAY_BE_LONG, nullptr, false, false, "1"), reg::typed("name", MAY_BE_NULL | MAY_BE_STRING, nullptr, false, false, "null") }; +inline constexpr reg::Arg exit_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig exit = { "exit", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, exit_args, 3, &exit_return }; +inline constexpr reg::Arg throwing_args[] = { reg::typed("type", 0, "PHPStan\\Type\\Type"), reg::typed("canContinue", MAY_BE_BOOL), reg::typed("canContainAnyThrowable", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg throwing_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig throwing = { "throwing", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, throwing_args, 3, &throwing_return }; +inline constexpr reg::Arg dead_args[] = { reg::typed("flow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow") }; +inline constexpr reg::Arg dead_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig dead = { "dead", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, dead_args, 1, &dead_return }; +inline constexpr reg::Arg loop_args[] = { reg::typed("condition", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("body", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("update", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("atLeastOnce", MAY_BE_BOOL), reg::typed("canExit", MAY_BE_BOOL), reg::typed("canRepeat", MAY_BE_BOOL, nullptr, false, false, "true") }; +inline constexpr reg::Arg loop_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig loop = { "loop", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 5, loop_args, 6, &loop_return }; +inline constexpr reg::Arg loopStatement_args[] = { reg::typed("stmt", 0, "PhpParser\\Node\\Stmt\\Foreach_|PhpParser\\Node\\Stmt\\For_"), reg::typed("flow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("bindings", MAY_BE_ARRAY), reg::typed("ownWrites", MAY_BE_ARRAY) }; +inline constexpr reg::Arg loopStatement_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig loopStatement = { "loopStatement", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 4, loopStatement_args, 4, &loopStatement_return }; +inline constexpr reg::Arg tryCatch_args[] = { reg::typed("body", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("catches", MAY_BE_ARRAY), reg::typed("finally", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow") }; +inline constexpr reg::Arg tryCatch_return = reg::typed("", 0, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig tryCatch = { "tryCatch", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 3, tryCatch_args, 3, &tryCatch_return }; +} // namespace sig + +} // namespace ptdecl::VariableFlow + +#endif diff --git a/turbo-ext/src/generated/VariableFlowBuilder.h b/turbo-ext/src/generated/VariableFlowBuilder.h new file mode 100644 index 00000000000..cb42bfd5f9b --- /dev/null +++ b/turbo-ext/src/generated/VariableFlowBuilder.h @@ -0,0 +1,52 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/VariableFlowBuilder.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_VARIABLE_FLOW_BUILDER_H +#define PHPSTANTURBO_GENERATED_VARIABLE_FLOW_BUILDER_H + +#include "../reg.h" + +namespace ptdecl::VariableFlowBuilder { + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + (void) cls; +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg throws_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr"), reg::typed("throwPoints", MAY_BE_ARRAY) }; +inline constexpr reg::Arg throws_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig throws = { "throws", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, throws_args, 2, &throws_return }; +inline constexpr reg::Arg arguments_args[] = { reg::typed("call", 0, "PhpParser\\Node\\Expr\\CallLike"), reg::typed("argsResult", 0, "PHPStan\\Analyser\\ArgsResult"), reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage") }; +inline constexpr reg::Arg arguments_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig arguments = { "arguments", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 3, arguments_args, 3, &arguments_return }; +inline constexpr reg::Arg child_args[] = { reg::typed("node", MAY_BE_NULL, "PhpParser\\Node"), reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage") }; +inline constexpr reg::Arg child_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig child = { "child", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, child_args, 2, &child_return }; +inline constexpr reg::Arg targetRead_args[] = { reg::typed("target", 0, "PhpParser\\Node\\Expr"), reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage"), reg::typed("read", MAY_BE_BOOL), reg::typed("targetId", MAY_BE_NULL | MAY_BE_LONG, nullptr, false, false, "null") }; +inline constexpr reg::Arg targetRead_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig targetRead = { "targetRead", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 3, targetRead_args, 4, &targetRead_return }; +inline constexpr reg::Arg writes_args[] = { reg::typed("flow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow") }; +inline constexpr reg::Arg writes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig writes = { "writes", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, writes_args, 1, &writes_return }; +inline constexpr reg::Arg targetWrite_args[] = { reg::typed("target", 0, "PhpParser\\Node\\Expr"), reg::typed("kind", MAY_BE_LONG), reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage"), reg::typed("redundant", MAY_BE_NULL, "PHPStan\\Type\\Type", false, false, "null") }; +inline constexpr reg::Arg targetWrite_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig targetWrite = { "targetWrite", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 4, targetWrite_args, 5, &targetWrite_return }; +inline constexpr reg::Arg writeSite_args[] = { reg::typed("target", 0, "PhpParser\\Node\\Expr"), reg::typed("kind", MAY_BE_LONG), reg::typed("scope", 0, "PHPStan\\Analyser\\MutatingScope"), reg::typed("storage", 0, "PHPStan\\Analyser\\ExpressionResultStorage") }; +inline constexpr reg::Arg writeSite_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Node\\Variable\\VariableWrite"); +inline constexpr reg::Sig writeSite = { "writeSite", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 4, writeSite_args, 4, &writeSite_return }; +inline constexpr reg::Arg escapeRoot_args[] = { reg::typed("expr", 0, "PhpParser\\Node\\Expr") }; +inline constexpr reg::Arg escapeRoot_return = reg::typed("", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"); +inline constexpr reg::Sig escapeRoot = { "escapeRoot", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 1, escapeRoot_args, 1, &escapeRoot_return }; +} // namespace sig + +} // namespace ptdecl::VariableFlowBuilder + +#endif diff --git a/turbo-ext/src/generated/VariableLivenessResolver.h b/turbo-ext/src/generated/VariableLivenessResolver.h new file mode 100644 index 00000000000..65c8c8aa493 --- /dev/null +++ b/turbo-ext/src/generated/VariableLivenessResolver.h @@ -0,0 +1,109 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/VariableLivenessResolver.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_VARIABLE_LIVENESS_RESOLVER_H +#define PHPSTANTURBO_GENERATED_VARIABLE_LIVENESS_RESOLVER_H + +#include "../reg.h" + +namespace ptdecl::VariableLivenessResolver { + +/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ +namespace slot { +inline constexpr uint32_t writes = 0; +inline constexpr uint32_t readIds = 1; +inline constexpr uint32_t observedIds = 2; +inline constexpr uint32_t readNames = 3; +inline constexpr uint32_t mentionedNames = 4; +inline constexpr uint32_t escapedNames = 5; +inline constexpr uint32_t redundantTypes = 6; +inline constexpr uint32_t accesses = 7; +inline constexpr uint32_t readKeys = 8; +inline constexpr uint32_t nameKeys = 9; +inline constexpr uint32_t observedKeys = 10; +inline constexpr uint32_t killedKeys = 11; +inline constexpr uint32_t dependencies = 12; +inline constexpr uint32_t inputCopies = 13; +inline constexpr uint32_t inputSinks = 14; +inline constexpr uint32_t literalItems = 15; +inline constexpr uint32_t coveredIds = 16; +inline constexpr uint32_t allReadKeys = 17; +inline constexpr uint32_t loopStatements = 18; +inline constexpr uint32_t ownWriteIds = 19; +inline constexpr uint32_t variableOverwritingLoops = 20; +inline constexpr uint32_t opaque = 21; +inline constexpr uint32_t readsAllVariables = 22; +inline constexpr uint32_t allNamesMentioned = 23; +inline constexpr uint32_t returnsByReference = 24; +} // namespace slot + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + cls.property("writes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("readIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("observedIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("readNames", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("mentionedNames", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("escapedNames", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("redundantTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("accesses", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("readKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("nameKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("observedKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("killedKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("dependencies", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("inputCopies", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("inputSinks", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("literalItems", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("coveredIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("allReadKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("loopStatements", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("ownWriteIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("variableOverwritingLoops", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("opaque", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); + cls.property("readsAllVariables", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); + cls.property("allNamesMentioned", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); + cls.property("returnsByReference", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedBool, 0); +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Sig __construct = { "__construct", ZEND_ACC_PRIVATE, 0, nullptr, 0, nullptr }; +inline constexpr reg::Arg resolve_args[] = { reg::typed("function", 0, "PhpParser\\Node\\FunctionLike"), reg::typed("flow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow") }; +inline constexpr reg::Arg resolve_return = reg::typed("", 0, "PHPStan\\Node\\VariableWritesNode"); +inline constexpr reg::Sig resolve = { "resolve", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, resolve_args, 2, &resolve_return }; +inline constexpr reg::Arg collect_args[] = { reg::typed("flow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("dead", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg collect_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig collect = { "collect", ZEND_ACC_PRIVATE, 1, collect_args, 2, &collect_return }; +inline constexpr reg::Arg liveBefore_args[] = { reg::typed("flow", MAY_BE_NULL, "PHPStan\\Analyser\\VariableFlow"), reg::typed("next", MAY_BE_ARRAY), reg::typed("context", 0, "PHPStan\\Analyser\\VariableFlowContext") }; +inline constexpr reg::Arg liveBefore_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig liveBefore = { "liveBefore", ZEND_ACC_PRIVATE, 3, liveBefore_args, 3, &liveBefore_return }; +inline constexpr reg::Arg bindingProbe_args[] = { reg::typed("binding", 0, "PHPStan\\Node\\Variable\\VariableWrite"), reg::typed("armed", MAY_BE_BOOL) }; +inline constexpr reg::Arg bindingProbe_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig bindingProbe = { "bindingProbe", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 2, bindingProbe_args, 2, &bindingProbe_return }; +inline constexpr reg::Arg passBindingProbes_args[] = { reg::typed("next", MAY_BE_ARRAY), reg::typed("name", MAY_BE_STRING), reg::typed("write", MAY_BE_NULL, "PHPStan\\Node\\Variable\\VariableWrite"), reg::typed("discard", MAY_BE_BOOL, nullptr, false, false, "false") }; +inline constexpr reg::Arg passBindingProbes_return = reg::typed("", MAY_BE_ARRAY); +inline constexpr reg::Sig passBindingProbes = { "passBindingProbes", ZEND_ACC_PRIVATE, 3, passBindingProbes_args, 4, &passBindingProbes_return }; +inline constexpr reg::Arg offsetKey_args[] = { reg::typed("offset", 0) }; +inline constexpr reg::Arg offsetKey_return = reg::typed("", MAY_BE_STRING); +inline constexpr reg::Sig offsetKey = { "offsetKey", ZEND_ACC_PRIVATE | ZEND_ACC_STATIC, 1, offsetKey_args, 1, &offsetKey_return }; +inline constexpr reg::Arg compileAccesses_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig compileAccesses = { "compileAccesses", ZEND_ACC_PRIVATE, 0, nullptr, 0, &compileAccesses_return }; +inline constexpr reg::Arg observeWrite_args[] = { reg::typed("id", MAY_BE_LONG), reg::typed("next", MAY_BE_ARRAY) }; +inline constexpr reg::Arg observeWrite_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig observeWrite = { "observeWrite", ZEND_ACC_PRIVATE, 2, observeWrite_args, 2, &observeWrite_return }; +inline constexpr reg::Arg resolveDependencies_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig resolveDependencies = { "resolveDependencies", ZEND_ACC_PRIVATE, 0, nullptr, 0, &resolveDependencies_return }; +inline constexpr reg::Arg resolveCoverage_return = reg::typed("", MAY_BE_VOID); +inline constexpr reg::Sig resolveCoverage = { "resolveCoverage", ZEND_ACC_PRIVATE, 0, nullptr, 0, &resolveCoverage_return }; +} // namespace sig + +} // namespace ptdecl::VariableLivenessResolver + +#endif diff --git a/turbo-ext/src/generated/VolatileExpressionHelper.h b/turbo-ext/src/generated/VolatileExpressionHelper.h new file mode 100644 index 00000000000..0118cc5a116 --- /dev/null +++ b/turbo-ext/src/generated/VolatileExpressionHelper.h @@ -0,0 +1,37 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Analyser/VolatileExpressionHelper.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_VOLATILE_EXPRESSION_HELPER_H +#define PHPSTANTURBO_GENERATED_VOLATILE_EXPRESSION_HELPER_H + +#include "../reg.h" + +namespace ptdecl::VolatileExpressionHelper { + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + (void) cls; +} + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr reg::Arg invalidateVolatileFunctionCalls_args[] = { reg::typed("expressionTypes", MAY_BE_ARRAY, nullptr, true, false), reg::typed("nativeExpressionTypes", MAY_BE_ARRAY, nullptr, true, false) }; +inline constexpr reg::Arg invalidateVolatileFunctionCalls_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig invalidateVolatileFunctionCalls = { "invalidateVolatileFunctionCalls", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, invalidateVolatileFunctionCalls_args, 2, &invalidateVolatileFunctionCalls_return }; +inline constexpr reg::Arg invalidateSuperglobals_args[] = { reg::typed("expressionTypes", MAY_BE_ARRAY, nullptr, true, false), reg::typed("nativeExpressionTypes", MAY_BE_ARRAY, nullptr, true, false) }; +inline constexpr reg::Arg invalidateSuperglobals_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig invalidateSuperglobals = { "invalidateSuperglobals", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 2, invalidateSuperglobals_args, 2, &invalidateSuperglobals_return }; +inline constexpr reg::Arg invalidateNegativeExistenceChecks_args[] = { reg::typed("scope", 0, "PHPStan\\Analyser\\Scope"), reg::typed("expressionTypes", MAY_BE_ARRAY, nullptr, true, false), reg::typed("nativeExpressionTypes", MAY_BE_ARRAY, nullptr, true, false), reg::typed("functionNames", MAY_BE_ARRAY, nullptr, false, false, "self::EXISTENCE_CHECK_FUNCTION_NAMES"), reg::typed("declaredSymbolName", MAY_BE_NULL | MAY_BE_STRING, nullptr, false, false, "null") }; +inline constexpr reg::Arg invalidateNegativeExistenceChecks_return = reg::typed("", MAY_BE_BOOL); +inline constexpr reg::Sig invalidateNegativeExistenceChecks = { "invalidateNegativeExistenceChecks", ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 3, invalidateNegativeExistenceChecks_args, 5, &invalidateNegativeExistenceChecks_return }; +} // namespace sig + +} // namespace ptdecl::VolatileExpressionHelper + +#endif diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index a46e418ba4b..e32394b6aa1 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -165,6 +165,7 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) pt_register_type_combinator_cache(); pt_register_arena_cache(); pt_register_expression_result_storage(); + pt_register_expression_result_storage_stack(); pt_register_php_file_cleaner(); pt_register_symbol_finder_in_files(); pt_register_scope_context(); @@ -288,6 +289,16 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) pt_register_called_on_type_unresolved_property_prototype_reflection(); pt_register_callback_unresolved_method_prototype_reflection(); pt_register_callback_unresolved_property_prototype_reflection(); + pt_register_mutating_scope(); + pt_register_class_reflection(); + pt_register_volatile_expression_helper(); + pt_register_variable_flow(); + pt_register_variable_flow_builder(); + pt_register_variable_liveness_resolver(); + pt_register_expression_result(); + /* the DI service behind ClassReflection's member lookups — after the + * Type family and LruCache, whose classes its signatures name */ + pt_register_php_class_reflection_extension(); return SUCCESS; } @@ -314,8 +325,11 @@ static PHP_RINIT_FUNCTION(phpstan_turbo) pt_integer_range_type_rinit(); pt_object_type_rinit(); pt_static_type_factory_rinit(); - pt_class_reflection_access_rinit(); + pt_scope_access_rinit(); pt_reflection_access_rinit(); + pt_mutating_scope_rinit(); + pt_variable_flow_rinit(); + pt_php_class_reflection_extension_rinit(); return SUCCESS; } diff --git a/turbo-ext/src/reg.h b/turbo-ext/src/reg.h index 2bf8198ca33..370f230281d 100644 --- a/turbo-ext/src/reg.h +++ b/turbo-ext/src/reg.h @@ -295,9 +295,9 @@ constexpr Arg doubleArg(const char *name) return { name, detail::codeMask(IS_DOUBLE, false) | detail::flagBits(false, false), nullptr }; } -constexpr Arg boolArg(const char *name) +constexpr Arg boolArg(const char *name, bool nullable = false) { - return { name, detail::codeMask(_IS_BOOL, false) | detail::flagBits(false, false), nullptr }; + return { name, detail::codeMask(_IS_BOOL, nullable) | detail::flagBits(false, false), nullptr }; } constexpr Arg stringArg(const char *name, bool nullable = false) @@ -310,9 +310,9 @@ constexpr Arg arrayArg(const char *name, bool nullable = false) return { name, detail::codeMask(IS_ARRAY, nullable) | detail::flagBits(false, false), nullptr }; } -constexpr Arg callableArg(const char *name) +constexpr Arg callableArg(const char *name, bool nullable = false) { - return { name, MAY_BE_CALLABLE | detail::flagBits(false, false), nullptr }; + return { name, MAY_BE_CALLABLE | (nullable ? MAY_BE_NULL : 0) | detail::flagBits(false, false), nullptr }; } constexpr Arg objectArg(const char *name, bool nullable = false) @@ -337,6 +337,19 @@ constexpr Arg mixedArg(const char *name) return { name, MAY_BE_ANY | detail::flagBits(false, false), nullptr }; } +/* an `array &$x` parameter (ZEND_ARG_TYPE_INFO with IS_ARRAY, by reference) */ +constexpr Arg arrayRefArg(const char *name) +{ + return { name, detail::codeMask(IS_ARRAY, false) | detail::flagBits(true, false), nullptr }; +} + +/* a `?Foo ...$x` nullable variadic of a specific class; className must be a + * persistent literal */ +constexpr Arg nullableVariadicObj(const char *name, const char *className) +{ + return { name, _ZEND_TYPE_LITERAL_NAME_BIT | MAY_BE_NULL | detail::flagBits(false, true), className }; +} + /* a parameter or return type the way a generated signature spells it * (turbo-ext/src/generated): the MAY_BE_* mask, a persistent literal class * name ("Foo", "Foo|Bar", "self") or nullptr, by reference / variadic, the @@ -416,6 +429,10 @@ struct ShadowPlan * at activation (Shadow.cpp) and kept here for the children */ pt_type_op_fn opFns[PT_OP_COUNT]; const pt_type_ops *ops; + /* declared only by a prefixed activation (the differential tests): a + * class whose port is still incomplete — never under the twin's real + * name (reg::Class::shadowDifferentialOnly()) */ + bool differentialOnly; }; /* declares the builder's properties and constants on a registered or @@ -763,6 +780,13 @@ class Class return *this; } + /* the PHP twin is abstract — the declared class is too */ + Class &abstract_() + { + flags |= ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; + return *this; + } + /* the PHP twin's parent class (real name; a shadowed parent resolves to * its native class, anything else autoloads at activation) */ Class &parent(const char *parentName) @@ -1118,6 +1142,14 @@ class Class return *this; } + /* a `public const X = ''` class constant; value is a persistent + * literal */ + Class &publicClassConstantString(const char *constantName, const char *value) + { + constants.push_back({ constantName, 0, ZEND_ACC_PUBLIC, value }); + return *this; + } + /* a `public const X = [...]` class constant whose value the builder * fills in at declaration (a persistent, immutable value — the engine * references it for the process lifetime) */ @@ -1134,6 +1166,14 @@ class Class return *this; } + /* a `private const X = [...]` class constant, filled in like + * classConstantValue() */ + Class &privateClassConstantValue(const char *constantName, void (*buildValue)(zval *out)) + { + constants.push_back({ constantName, 0, ZEND_ACC_PRIVATE, nullptr, buildValue }); + return *this; + } + /* an internal class registered at module startup (extension-only * classes with no PHP twin) */ zend_class_entry *register_() @@ -1172,6 +1212,7 @@ class Class plan.ce = NULL; memcpy(plan.opFns, opFns, sizeof(opFns)); plan.ops = NULL; + plan.differentialOnly = differentialOnly; pt_shadow_plan_add(std::move(plan)); } @@ -1190,6 +1231,17 @@ class Class return *this; } + /* a plan for a class whose port is incomplete: declared next to the + * twin by the prefixed activation of the differential tests only, so + * the partial native class can be compared method by method, and never + * under the real name (the twin keeps running everywhere else). The + * finished port replaces this call by shadow(). */ + void shadowDifferentialOnly(zend_class_entry **out) + { + differentialOnly = true; + shadow(out); + } + private: const char *name; uint32_t flags = 0; @@ -1200,6 +1252,7 @@ class Class std::vector constants; pt_type_op_fn opFns[PT_OP_COUNT] = {}; bool lastTraitMethodAdded = false; + bool differentialOnly = false; }; } // namespace reg diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 5d59883d245..8e395593df4 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -129,8 +129,6 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_OFFSET_ACCESS_TYPE_NODE */ {"offsetAccessTypeNode", "PHPStan\\PhpDocParser\\Ast\\Type\\OffsetAccessTypeNode"}, /* PT_CLASS_CONDITIONAL_TYPE_NODE */ {"conditionalTypeNode", "PHPStan\\PhpDocParser\\Ast\\Type\\ConditionalTypeNode"}, /* PT_CLASS_CONDITIONAL_TYPE_FOR_PARAMETER_NODE */ {"conditionalTypeForParameterNode", "PHPStan\\PhpDocParser\\Ast\\Type\\ConditionalTypeForParameterNode"}, - /* PT_CLASS_CLASS_REFLECTION */ {"classReflection", "PHPStan\\Reflection\\ClassReflection"}, - /* PT_CLASS_MUTATING_SCOPE */ {"mutatingScope", "PHPStan\\Analyser\\MutatingScope"}, /* PT_CLASS_REFLECTION_ENUM */ {"reflectionEnum", "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionEnum"}, /* PT_CLASS_MEMOIZING_REFLECTION_PROVIDER */ {"memoizingReflectionProvider", "PHPStan\\Reflection\\ReflectionProvider\\MemoizingReflectionProvider"}, /* PT_CLASS_UNRESOLVABLE_TYPE_RESULT */ {"unresolvableTypeResult", "PHPStan\\Rules\\PhpDoc\\UnresolvableTypeResult"}, @@ -140,6 +138,85 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_RESOLVED_PROPERTY_REFLECTION */ {"resolvedPropertyReflection", "PHPStan\\Reflection\\ResolvedPropertyReflection"}, /* PT_CLASS_CHANGED_TYPE_METHOD_REFLECTION */ {"changedTypeMethodReflection", "PHPStan\\Reflection\\Dummy\\ChangedTypeMethodReflection"}, /* PT_CLASS_CHANGED_TYPE_PROPERTY_REFLECTION */ {"changedTypePropertyReflection", "PHPStan\\Reflection\\Dummy\\ChangedTypePropertyReflection"}, + /* PT_CLASS_UNDEFINED_VARIABLE_EXCEPTION */ {"undefinedVariableException", "PHPStan\\Analyser\\UndefinedVariableException"}, + /* PT_CLASS_NODE_CALLBACK_SCOPE */ {"nodeCallbackScope", "PHPStan\\Analyser\\NodeCallbackScope"}, + /* PT_CLASS_PROPERTY_INITIALIZATION_EXPR */ {"propertyInitializationExpr", "PHPStan\\Node\\Expr\\PropertyInitializationExpr"}, + /* PT_CLASS_POSSIBLY_IMPURE_CALL_EXPR */ {"possiblyImpureCallExpr", "PHPStan\\Node\\Expr\\PossiblyImpureCallExpr"}, + /* PT_CLASS_CONST_FETCH */ {"constFetch", "PhpParser\\Node\\Expr\\ConstFetch"}, + /* PT_CLASS_HALT_COMPILER */ {"haltCompiler", "PhpParser\\Node\\Stmt\\HaltCompiler"}, + /* PT_CLASS_NODE_SCOPE_RESOLVER */ {"nodeScopeResolver", "PHPStan\\Analyser\\NodeScopeResolver"}, + /* PT_CLASS_EXPR_HANDLER_REGISTRY */ {"exprHandlerRegistry", "PHPStan\\Analyser\\ExprHandlerRegistry"}, + /* PT_CLASS_TEMPLATE_ARGUMENT_FRAME */ {"templateArgumentFrame", "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame"}, + /* PT_CLASS_INITIALIZER_EXPR_CONTEXT */ {"initializerExprContext", "PHPStan\\Reflection\\InitializerExprContext"}, + /* PT_CLASS_EXTENDED_PARAMETERS_ACCEPTOR */ {"extendedParametersAcceptor", "PHPStan\\Reflection\\ExtendedParametersAcceptor"}, + /* PT_CLASS_MATCH */ {"match", "PhpParser\\Node\\Expr\\Match_"}, + /* PT_CLASS_NULLSAFE_METHOD_CALL */ {"nullsafeMethodCall", "PhpParser\\Node\\Expr\\NullsafeMethodCall"}, + /* PT_CLASS_STATIC_PROPERTY_FETCH */ {"staticPropertyFetch", "PhpParser\\Node\\Expr\\StaticPropertyFetch"}, + /* PT_CLASS_CLASS_CONST_FETCH */ {"classConstFetch", "PhpParser\\Node\\Expr\\ClassConstFetch"}, + /* PT_CLASS_SCALAR_STRING */ {"scalarString", "PhpParser\\Node\\Scalar\\String_"}, + /* PT_CLASS_SCALAR_INT */ {"scalarInt", "PhpParser\\Node\\Scalar\\Int_"}, + /* PT_CLASS_SCALAR_FLOAT */ {"scalarFloat", "PhpParser\\Node\\Scalar\\Float_"}, + /* PT_CLASS_VAR_LIKE_IDENTIFIER */ {"varLikeIdentifier", "PhpParser\\Node\\VarLikeIdentifier"}, + /* PT_CLASS_EXTENDED_METHOD_REFLECTION */ {"extendedMethodReflection", "PHPStan\\Reflection\\ExtendedMethodReflection"}, + /* PT_CLASS_ARG */ {"arg", "PhpParser\\Node\\Arg"}, + /* PT_CLASS_FUNCTION_REFLECTION */ {"functionReflection", "PHPStan\\Reflection\\FunctionReflection"}, + /* PT_CLASS_PHP_VERSIONS */ {"phpVersions", "PHPStan\\Php\\PhpVersions"}, + /* PT_CLASS_PARAM */ {"param", "PhpParser\\Node\\Param"}, + /* PT_CLASS_TRANSFORM_STATIC_TYPE_TRAVERSER */ {"transformStaticTypeTraverser", "PHPStan\\Analyser\\Traverser\\TransformStaticTypeTraverser"}, + /* PT_CLASS_PHP_METHOD_FROM_PARSER_NODE_REFLECTION */ {"phpMethodFromParserNodeReflection", "PHPStan\\Reflection\\Php\\PhpMethodFromParserNodeReflection"}, + /* PT_CLASS_PHP_FUNCTION_FROM_PARSER_NODE_REFLECTION */ {"phpFunctionFromParserNodeReflection", "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection"}, + /* PT_CLASS_PARAMETER_VARIABLE_ORIGINAL_VALUE_EXPR */ {"parameterVariableOriginalValueExpr", "PHPStan\\Node\\Expr\\ParameterVariableOriginalValueExpr"}, + /* PT_CLASS_WRAPPED_EXTENDED_METHOD_REFLECTION */ {"wrappedExtendedMethodReflection", "PHPStan\\Reflection\\WrappedExtendedMethodReflection"}, + /* PT_CLASS_EXTENDED_PROPERTY_REFLECTION */ {"extendedPropertyReflection", "PHPStan\\Reflection\\ExtendedPropertyReflection"}, + /* PT_CLASS_WRAPPED_EXTENDED_PROPERTY_REFLECTION */ {"wrappedExtendedPropertyReflection", "PHPStan\\Reflection\\WrappedExtendedPropertyReflection"}, + /* PT_CLASS_VARIABLE_ACCESS_FLOW */ {"variableAccessFlow", "PHPStan\\Analyser\\VariableAccessFlow"}, + /* PT_CLASS_ENUM_CASE_REFLECTION */ {"enumCaseReflection", "PHPStan\\Reflection\\EnumCaseReflection"}, + /* PT_CLASS_REFLECTION_ENUM_BACKED_CASE */ {"reflectionEnumBackedCase", "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionEnumBackedCase"}, + /* PT_CLASS_REAL_CLASS_CLASS_CONSTANT_REFLECTION */ {"realClassClassConstantReflection", "PHPStan\\Reflection\\RealClassClassConstantReflection"}, + /* PT_CLASS_TYPE_ALIAS */ {"typeAlias", "PHPStan\\Type\\TypeAlias"}, + /* PT_CLASS_CIRCULAR_TYPE_ALIAS_DEFINITION_EXCEPTION */ {"circularTypeAliasDefinitionException", "PHPStan\\Type\\CircularTypeAliasDefinitionException"}, + /* PT_CLASS_ARGUMENTS_NORMALIZER */ {"argumentsNormalizer", "PHPStan\\Analyser\\ArgumentsNormalizer"}, + /* PT_CLASS_VARIABLE_SEQUENCE_FLOW */ {"variableSequenceFlow", "PHPStan\\Analyser\\VariableSequenceFlow"}, + /* PT_CLASS_VARIABLE_CONTROL_FLOW */ {"variableControlFlow", "PHPStan\\Analyser\\VariableControlFlow"}, + /* PT_CLASS_VARIABLE_INPUT_FLOW */ {"variableInputFlow", "PHPStan\\Analyser\\VariableInputFlow"}, + /* PT_CLASS_VARIABLE_WRITE */ {"variableWrite", "PHPStan\\Node\\Variable\\VariableWrite"}, + /* PT_CLASS_VARIABLE_WRITE_OFFSET */ {"variableWriteOffset", "PHPStan\\Analyser\\VariableWriteOffset"}, + /* PT_CLASS_LIST_EXPR */ {"listExpr", "PhpParser\\Node\\Expr\\List_"}, + /* PT_CLASS_VARIABLE_WRITES_NODE */ {"variableWritesNode", "PHPStan\\Node\\VariableWritesNode"}, + /* PT_CLASS_TYPE_SPECIFIER_CONTEXT */ {"typeSpecifierContext", "PHPStan\\Analyser\\TypeSpecifierContext"}, + /* PT_CLASS_VOID_TO_NULL_TRAVERSER */ {"voidToNullTraverser", "PHPStan\\Analyser\\Traverser\\VoidToNullTraverser"}, + /* PT_CLASS_ISSETABILITY_RESOLUTION */ {"issetabilityResolution", "PHPStan\\Analyser\\IssetabilityResolution"}, + /* PT_CLASS_ISSETABILITY_LINK_INFO */ {"issetabilityLinkInfo", "PHPStan\\Analyser\\IssetabilityLinkInfo"}, + /* PT_CLASS_ALWAYS_REMEMBERED_EXPR */ {"alwaysRememberedExpr", "PHPStan\\Node\\Expr\\AlwaysRememberedExpr"}, + /* PT_CLASS_PHP_PROPERTY_REFLECTION */ {"phpPropertyReflection", "PHPStan\\Reflection\\Php\\PhpPropertyReflection"}, + /* PT_CLASS_NATIVE_METHOD_REFLECTION */ {"nativeMethodReflection", "PHPStan\\Reflection\\Native\\NativeMethodReflection"}, + /* PT_CLASS_EXTENDED_NATIVE_PARAMETER_REFLECTION */ {"extendedNativeParameterReflection", "PHPStan\\Reflection\\Native\\ExtendedNativeParameterReflection"}, + /* PT_CLASS_ENUM_CASES_METHOD_REFLECTION */ {"enumCasesMethodReflection", "PHPStan\\Reflection\\Php\\EnumCasesMethodReflection"}, + /* PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE */ {"privatePropertyAttribute", "PHPStan\\Reflection\\Attribute\\PrivateProperty"}, + /* PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE */ {"protectedPropertyAttribute", "PHPStan\\Reflection\\Attribute\\ProtectedProperty"}, + /* PT_CLASS_ADAPTER_REFLECTION_METHOD */ {"adapterReflectionMethod", "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod"}, + /* PT_CLASS_EXPRESSION_STMT */ {"expressionStmt", "PhpParser\\Node\\Stmt\\Expression"}, + /* PT_CLASS_ASSIGN_EXPR */ {"assignExpr", "PhpParser\\Node\\Expr\\Assign"}, + /* PT_CLASS_NAMESPACE_STMT */ {"namespaceStmt", "PhpParser\\Node\\Stmt\\Namespace_"}, + /* PT_CLASS_DECLARE_STMT */ {"declareStmt", "PhpParser\\Node\\Stmt\\Declare_"}, + /* PT_CLASS_CLASS_METHOD_STMT */ {"classMethodStmt", "PhpParser\\Node\\Stmt\\ClassMethod"}, + /* PT_CLASS_ADAPTER_REFLECTION_CLASS */ {"adapterReflectionClass", "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClass"}, + /* PT_CLASS_BETTER_REFLECTION_CLASS */ {"betterReflectionClass", "PHPStan\\BetterReflection\\Reflection\\ReflectionClass"}, + /* PT_CLASS_ORIGINAL_FOREACH_VALUE_EXPR */ {"originalForeachValueExpr", "PHPStan\\Node\\Expr\\OriginalForeachValueExpr"}, + /* PT_CLASS_ORIGINAL_FOREACH_KEY_EXPR */ {"originalForeachKeyExpr", "PHPStan\\Node\\Expr\\OriginalForeachKeyExpr"}, + /* PT_CLASS_SET_EXISTING_OFFSET_VALUE_TYPE_EXPR */ {"setExistingOffsetValueTypeExpr", "PHPStan\\Node\\Expr\\SetExistingOffsetValueTypeExpr"}, + /* PT_CLASS_NATIVE_TYPE_EXPR */ {"nativeTypeExpr", "PHPStan\\Node\\Expr\\NativeTypeExpr"}, + /* PT_CLASS_CLONE_REINITIALIZATION_EXPR */ {"cloneReinitializationExpr", "PHPStan\\Node\\Expr\\CloneReinitializationExpr"}, + /* PT_CLASS_METHOD_REFLECTION */ {"methodReflection", "PHPStan\\Reflection\\MethodReflection"}, + /* PT_CLASS_PRE_INC */ {"preInc", "PhpParser\\Node\\Expr\\PreInc"}, + /* PT_CLASS_PRE_DEC */ {"preDec", "PhpParser\\Node\\Expr\\PreDec"}, + /* PT_CLASS_POST_INC */ {"postInc", "PhpParser\\Node\\Expr\\PostInc"}, + /* PT_CLASS_POST_DEC */ {"postDec", "PhpParser\\Node\\Expr\\PostDec"}, + /* PT_CLASS_ISSET_EXPR */ {"issetExpr", "PHPStan\\Node\\IssetExpr"}, + /* PT_CLASS_EMIT_COLLECTED_DATA_NODE */ {"emitCollectedDataNode", "PHPStan\\Node\\EmitCollectedDataNode"}, + /* PT_CLASS_LAZY_CLASS_REFLECTION_EXTENSION_REGISTRY_PROVIDER */ {"lazyClassReflectionExtensionRegistryProvider", "PHPStan\\DependencyInjection\\Reflection\\LazyClassReflectionExtensionRegistryProvider"}, + /* PT_CLASS_CLASS_REFLECTION_EXTENSION_REGISTRY */ {"classReflectionExtensionRegistry", "PHPStan\\Reflection\\ClassReflectionExtensionRegistry"}, + /* PT_CLASS_LAZY_INTERNAL_SCOPE_FACTORY */ {"lazyInternalScopeFactory", "PHPStan\\Analyser\\LazyInternalScopeFactory"}, }; zend_class_entry *pt_class(int idx) @@ -230,6 +307,7 @@ zend_string *pt_str_cache_printer = nullptr; zend_string *pt_str_contains_super_global = nullptr; zend_string *pt_str_array_map_args = nullptr; zend_string *pt_str_start_file_pos = nullptr; +zend_string *pt_str_end_file_pos = nullptr; static bool pt_strs_inited = false; static HashTable pt_node_class_cache; @@ -242,6 +320,7 @@ void pt_init_strs() pt_str_contains_super_global = zend_string_init("containsSuperGlobal", sizeof("containsSuperGlobal") - 1, 0); pt_str_array_map_args = zend_string_init("arrayMapArgs", sizeof("arrayMapArgs") - 1, 0); pt_str_start_file_pos = zend_string_init("startFilePos", sizeof("startFilePos") - 1, 0); + pt_str_end_file_pos = zend_string_init("endFilePos", sizeof("endFilePos") - 1, 0); pt_strs_inited = true; } @@ -283,6 +362,7 @@ void pt_support_rshutdown() zend_string_release(pt_str_contains_super_global); zend_string_release(pt_str_array_map_args); zend_string_release(pt_str_start_file_pos); + zend_string_release(pt_str_end_file_pos); pt_strs_inited = false; } if (pt_node_class_cache_inited) { @@ -694,7 +774,7 @@ zend_object *pt_find_first_recursive(zend_object *node, pt_node_matcher matcher, return NULL; } -static const struct { const char *name; size_t len; } pt_superglobals[] = { +static const pt_superglobal_name pt_superglobals[] = { {"GLOBALS", 7}, {"_SERVER", 7}, {"_GET", 4}, @@ -706,17 +786,44 @@ static const struct { const char *name; size_t len; } pt_superglobals[] = { {"_ENV", 4}, }; -bool pt_is_superglobal_name(zend_string *name) +bool pt_is_superglobal_cstr(const char *name, size_t len) { for (size_t i = 0; i < sizeof(pt_superglobals) / sizeof(pt_superglobals[0]); i++) { - if (ZSTR_LEN(name) == pt_superglobals[i].len - && memcmp(ZSTR_VAL(name), pt_superglobals[i].name, pt_superglobals[i].len) == 0) { - return true; - } + if (len == pt_superglobals[i].len && memcmp(name, pt_superglobals[i].name, len) == 0) return true; } return false; } +bool pt_is_superglobal_name(zend_string *name) +{ + return pt_is_superglobal_cstr(ZSTR_VAL(name), ZSTR_LEN(name)); +} + +const pt_superglobal_name *pt_superglobal_names(size_t *count) +{ + *count = sizeof(pt_superglobals) / sizeof(pt_superglobals[0]); + return pt_superglobals; +} + +bool pt_call_like_is_first_class_callable(zend_object *call, bool &out) +{ + zend_class_entry *variadicPlaceholderCe = pt_class(PT_CLASS_VARIADIC_PLACEHOLDER); + if (UNEXPECTED(variadicPlaceholderCe == NULL)) return false; + out = false; + int32_t argsOffset = pt_instance_prop_offset(call->ce, "args", sizeof("args") - 1); + if (argsOffset < 0) return true; + zval *args = OBJ_PROP(call, (uint32_t) argsOffset); + ZVAL_DEINDIRECT(args); + ZVAL_DEREF(args); + if (Z_TYPE_P(args) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(args)) != 1) return true; + /* current($rawArgs): the array's internal pointer, like the twin */ + zval *first = zend_hash_get_current_data(Z_ARRVAL_P(args)); + if (first == NULL) return true; + ZVAL_DEREF(first); + out = Z_TYPE_P(first) == IS_OBJECT && instanceof_function(Z_OBJCE_P(first), variadicPlaceholderCe); + return true; +} + static bool pt_superglobal_matcher(zend_object *node, void *ctx) { pt_node_class_info *info = pt_get_node_class_info(node->ce); diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index e1ed9a97afb..b02978acea9 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -166,8 +166,6 @@ enum { PT_CLASS_OFFSET_ACCESS_TYPE_NODE, PT_CLASS_CONDITIONAL_TYPE_NODE, PT_CLASS_CONDITIONAL_TYPE_FOR_PARAMETER_NODE, - PT_CLASS_CLASS_REFLECTION, - PT_CLASS_MUTATING_SCOPE, PT_CLASS_REFLECTION_ENUM, PT_CLASS_MEMOIZING_REFLECTION_PROVIDER, PT_CLASS_UNRESOLVABLE_TYPE_RESULT, @@ -177,6 +175,85 @@ enum { PT_CLASS_RESOLVED_PROPERTY_REFLECTION, PT_CLASS_CHANGED_TYPE_METHOD_REFLECTION, PT_CLASS_CHANGED_TYPE_PROPERTY_REFLECTION, + PT_CLASS_UNDEFINED_VARIABLE_EXCEPTION, + PT_CLASS_NODE_CALLBACK_SCOPE, + PT_CLASS_PROPERTY_INITIALIZATION_EXPR, + PT_CLASS_POSSIBLY_IMPURE_CALL_EXPR, + PT_CLASS_CONST_FETCH, + PT_CLASS_HALT_COMPILER, + PT_CLASS_NODE_SCOPE_RESOLVER, + PT_CLASS_EXPR_HANDLER_REGISTRY, + PT_CLASS_TEMPLATE_ARGUMENT_FRAME, + PT_CLASS_INITIALIZER_EXPR_CONTEXT, + PT_CLASS_EXTENDED_PARAMETERS_ACCEPTOR, + PT_CLASS_MATCH, + PT_CLASS_NULLSAFE_METHOD_CALL, + PT_CLASS_STATIC_PROPERTY_FETCH, + PT_CLASS_CLASS_CONST_FETCH, + PT_CLASS_SCALAR_STRING, + PT_CLASS_SCALAR_INT, + PT_CLASS_SCALAR_FLOAT, + PT_CLASS_VAR_LIKE_IDENTIFIER, + PT_CLASS_EXTENDED_METHOD_REFLECTION, + PT_CLASS_ARG, + PT_CLASS_FUNCTION_REFLECTION, + PT_CLASS_PHP_VERSIONS, + PT_CLASS_PARAM, + PT_CLASS_TRANSFORM_STATIC_TYPE_TRAVERSER, + PT_CLASS_PHP_METHOD_FROM_PARSER_NODE_REFLECTION, + PT_CLASS_PHP_FUNCTION_FROM_PARSER_NODE_REFLECTION, + PT_CLASS_PARAMETER_VARIABLE_ORIGINAL_VALUE_EXPR, + PT_CLASS_WRAPPED_EXTENDED_METHOD_REFLECTION, + PT_CLASS_EXTENDED_PROPERTY_REFLECTION, + PT_CLASS_WRAPPED_EXTENDED_PROPERTY_REFLECTION, + PT_CLASS_VARIABLE_ACCESS_FLOW, + PT_CLASS_ENUM_CASE_REFLECTION, + PT_CLASS_REFLECTION_ENUM_BACKED_CASE, + PT_CLASS_REAL_CLASS_CLASS_CONSTANT_REFLECTION, + PT_CLASS_TYPE_ALIAS, + PT_CLASS_CIRCULAR_TYPE_ALIAS_DEFINITION_EXCEPTION, + PT_CLASS_ARGUMENTS_NORMALIZER, + PT_CLASS_VARIABLE_SEQUENCE_FLOW, + PT_CLASS_VARIABLE_CONTROL_FLOW, + PT_CLASS_VARIABLE_INPUT_FLOW, + PT_CLASS_VARIABLE_WRITE, + PT_CLASS_VARIABLE_WRITE_OFFSET, + PT_CLASS_LIST_EXPR, + PT_CLASS_VARIABLE_WRITES_NODE, + PT_CLASS_TYPE_SPECIFIER_CONTEXT, + PT_CLASS_VOID_TO_NULL_TRAVERSER, + PT_CLASS_ISSETABILITY_RESOLUTION, + PT_CLASS_ISSETABILITY_LINK_INFO, + PT_CLASS_ALWAYS_REMEMBERED_EXPR, + PT_CLASS_PHP_PROPERTY_REFLECTION, + PT_CLASS_NATIVE_METHOD_REFLECTION, + PT_CLASS_EXTENDED_NATIVE_PARAMETER_REFLECTION, + PT_CLASS_ENUM_CASES_METHOD_REFLECTION, + PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE, + PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE, + PT_CLASS_ADAPTER_REFLECTION_METHOD, + PT_CLASS_EXPRESSION_STMT, + PT_CLASS_ASSIGN_EXPR, + PT_CLASS_NAMESPACE_STMT, + PT_CLASS_DECLARE_STMT, + PT_CLASS_CLASS_METHOD_STMT, + PT_CLASS_ADAPTER_REFLECTION_CLASS, + PT_CLASS_BETTER_REFLECTION_CLASS, + PT_CLASS_ORIGINAL_FOREACH_VALUE_EXPR, + PT_CLASS_ORIGINAL_FOREACH_KEY_EXPR, + PT_CLASS_SET_EXISTING_OFFSET_VALUE_TYPE_EXPR, + PT_CLASS_NATIVE_TYPE_EXPR, + PT_CLASS_CLONE_REINITIALIZATION_EXPR, + PT_CLASS_METHOD_REFLECTION, + PT_CLASS_PRE_INC, + PT_CLASS_PRE_DEC, + PT_CLASS_POST_INC, + PT_CLASS_POST_DEC, + PT_CLASS_ISSET_EXPR, + PT_CLASS_EMIT_COLLECTED_DATA_NODE, + PT_CLASS_LAZY_CLASS_REFLECTION_EXTENSION_REGISTRY_PROVIDER, + PT_CLASS_CLASS_REFLECTION_EXTENSION_REGISTRY, + PT_CLASS_LAZY_INTERNAL_SCOPE_FACTORY, PT_CLASS_COUNT }; @@ -282,6 +359,7 @@ void pt_register_parser_runner(); void pt_register_type_combinator_cache(); void pt_register_arena_cache(); void pt_register_expression_result_storage(); +void pt_register_expression_result_storage_stack(); void pt_register_php_file_cleaner(); void pt_register_symbol_finder_in_files(); void pt_register_scope_context(); @@ -1185,27 +1263,12 @@ void pt_register_template_key_of_type(); * against KeyOfType); false = pending exception */ [[nodiscard]] bool pt_template_key_of_type_new(zval *out, zval *scope, zval *strategy, zval *variance, zend_string *name, zval *bound, zval *defaultType); -/* ClassReflectionAccess.cpp — native readers of the memo slots of - * PHPStan\Reflection\ClassReflection (a userland final class) and of a - * MutatingScope's ScopeContext: the Type kernel's hottest native->PHP calls - * (getName()/isGeneric()/hasMethod()/getCacheKey() on class reflections, - * isInClass()/getClassReflection() on scopes), answered from the twin's own - * property slot when it holds the memoized answer and through the PHP - * method otherwise, so the observable behaviour (lazy computation, the - * Error on an uninitialized slot, a subclass's override) stays the twin's */ -void pt_class_reflection_access_rinit(); -/* $classReflection->getName() / ->getCacheKey() / ->getNativeReflection(); - * UNDEF = pending exception */ -zv::Val pt_class_reflection_get_name(zend_object *classReflection); -zv::Val pt_class_reflection_get_cache_key(zend_object *classReflection); -zv::Val pt_class_reflection_get_native_reflection(zend_object *classReflection); -/* $classReflection->isGeneric() / ->hasMethod($methodName) / - * ->hasFinalByKeywordOverride() / ->isEnum(), coerced to bool as the call - * sites always did; false = pending exception */ -[[nodiscard]] bool pt_class_reflection_is_generic(zend_object *classReflection, bool &out); -bool pt_class_reflection_has_method(zend_object *classReflection, zval *methodName, bool &out); -bool pt_class_reflection_has_final_by_keyword_override(zend_object *classReflection, bool &out); -bool pt_class_reflection_is_enum(zend_object *classReflection, bool &out); +/* ScopeContext.cpp — $scope->isInClass() / ->getClassReflection() for + * native callers: when the scope is exactly a MutatingScope (or a subclass + * inheriting both bodies) holding a native ScopeContext, the answer comes + * out of the context's $classReflection slot, otherwise the PHP method + * decides; the per-request slot cache is reset by the rinit */ +void pt_scope_access_rinit(); /* $scope->isInClass() (coerced to bool) / ->getClassReflection(); false / * UNDEF = pending exception */ [[nodiscard]] bool pt_scope_is_in_class(zend_object *scope, bool &out); @@ -1247,6 +1310,23 @@ zv::Val pt_reflection_provider_instance(); [[nodiscard]] bool pt_reflection_provider_has_class(zend_object *provider, zval *className, bool &out); zv::Val pt_reflection_provider_has_class_zv(zend_object *provider, zval *className); zv::Val pt_reflection_provider_get_class(zend_object *provider, zval *className); +/* the members of the ClassReflectionExtensionRegistry the native + * ClassReflection asks for, each naming the registry property holding it and + * the twin's getter */ +enum pt_registry_member +{ + PT_REGISTRY_PHP_CLASS_REFLECTION_EXTENSION = 0, + PT_REGISTRY_METHODS_EXTENSIONS, + PT_REGISTRY_PROPERTIES_EXTENSIONS, + PT_REGISTRY_REQUIRE_EXTENDS_METHODS_EXTENSION, + PT_REGISTRY_REQUIRE_EXTENDS_PROPERTIES_EXTENSION, + PT_REGISTRY_ALLOWED_SUB_TYPES_EXTENSIONS, + PT_REGISTRY_MEMBER_COUNT +}; +/* $provider->getRegistry()->() — both hops out of property slots for + * the classes the twins declare, the methods for anything else; UNDEF = + * pending exception */ +zv::Val pt_class_reflection_extension_registry_member(zend_object *provider, pt_registry_member member); /* }}} */ @@ -1282,4 +1362,174 @@ zv::Val pt_callback_unresolved_property_prototype_reflection_new(uint32_t argc, /* }}} */ +/* the native MutatingScope (MutatingScope.cpp): PHPStan\Analyser\MutatingScope + * itself once activateShadowing() ran, NULL before that */ +extern zend_class_entry *pt_ce_mutating_scope; +void pt_register_mutating_scope(); +/* forgets the internal scope factory's slot offsets */ +void pt_mutating_scope_rinit(); +/* the file / traitReflection slots of a native ScopeContext, next to + * pt_scope_context_class_reflection() (ScopeContext.cpp) */ +zval *pt_scope_context_file(zend_object *context); +zval *pt_scope_context_trait_reflection(zend_object *context); +/* ScopeOps::hasVariableType($scope, $variableName) / + * ScopeOps::hasExpressionType($scope, $node, $exprPrinter) natively + * (ScopeOps.cpp); the TrinaryLogic singleton, UNDEF = pending exception */ +zv::Val pt_scope_ops_has_variable_type(zval *scope, zend_string *variableName); +zv::Val pt_scope_ops_has_expression_type(zval *scope, zend_object *node, zval *exprPrinter); +/* StaticTypeFactory::argc() / argv() — copies of the memoized types; + * UNDEF = pending exception */ +zv::Val pt_static_type_factory_argc(); +zv::Val pt_static_type_factory_argv(); +/* StaticTypeFactory::generalOffsetAccessibleType() / + * intOffsetAccessibleType() — copies of the memoized types; UNDEF = + * pending exception */ +zv::Val pt_static_type_factory_general_offset_accessible(); +zv::Val pt_static_type_factory_int_offset_accessible(); +/* ScopeOps::getTypeFromCache($scope, $node, $key) / + * ScopeOps::expressionTypeByKey($scope, $node, $exprString) natively + * (ScopeOps.cpp), for the native MutatingScope's getType() / resolveType(): + * the memoized type (null on a miss, *keyOut the owned node key either + * way, NULL only with an exception pending) / the tracked type of a + * certainty-yes holder (null otherwise); UNDEF = pending exception */ +zv::Val pt_scope_ops_get_type_from_cache(zval *scope, zend_object *node, zend_string **keyOut); +zv::Val pt_scope_ops_expression_type_by_key(zval *scope, zend_object *node, zend_string *exprString); +/* ScopeOps::scopeWith() / ::invalidateExpressionEntries() / + * ::invalidateMethodsOnExpression() / ::getIntertwinedRefRootVariableName() + * natively (ScopeOps.cpp), for the native MutatingScope's assignment and + * invalidation family; UNDEF = pending exception */ +zv::Val pt_scope_ops_scope_with(zval *scope, HashTable *expressionTypes, HashTable *nativeExpressionTypes, HashTable *conditionalExpressions, HashTable *currentlyAssignedExpressions, HashTable *currentlyAllowedUndefinedExpressions, HashTable *inFunctionCallsStack, bool inFirstLevelStatement, bool afterExtractCall); +zv::Val pt_scope_ops_invalidate_expression_entries(zval *scope, zval *exprPrinter, zend_string *exprStringToInvalidate, zval *expressionToInvalidate, bool requireMoreCharacters, zval *invalidatingClass, HashTable *expressionTypes, HashTable *nativeExpressionTypes, HashTable *conditionalExpressions, bool keepPropertyFetches); +zv::Val pt_scope_ops_invalidate_methods_on_expression(zval *exprPrinter, zend_string *exprStringToInvalidate, HashTable *expressionTypes, HashTable *nativeExpressionTypes); +zv::Val pt_scope_ops_intertwined_ref_root_variable_name(zend_object *expr); +zv::Val pt_scope_ops_match_conditional_expressions(HashTable *conditionalExpressions, HashTable *specifiedExpressions); +zv::Val pt_scope_ops_merge_variable_holders(HashTable *ourVariableTypeHolders, HashTable *theirVariableTypeHolders, HashTable *differingKeys); +zv::Val pt_scope_ops_finish_merge(HashTable *mergedExpressionTypes, HashTable *ourExpressionTypes, HashTable *theirExpressionTypes, HashTable *ourNativeExpressionTypes, HashTable *theirNativeExpressionTypes); +zv::Val pt_scope_ops_intersect_conditional_expressions(HashTable *ourConditionalExpressions, HashTable *theirConditionalExpressions); +zv::Val pt_scope_ops_create_conditional_expressions(HashTable *conditionalExpressions, HashTable *ourExpressionTypes, HashTable *theirExpressionTypes, HashTable *mergedExpressionTypes, HashTable *differingKeys); +bool pt_scope_ops_should_invalidate_expression(zval *scope, zval *exprPrinter, zend_string *exprStringToInvalidate, zval *exprToInvalidate, zend_object *expr, zend_string *exprString, bool requireMoreCharacters, zval *invalidatingClass, bool keepPropertyFetches, bool *failed); +/* the shadowing ExpressionResultStorage (ExpressionResultStorage.cpp) — new + * ExpressionResultStorage(), $storage->findExpressionResult($expr) and + * $storage->duplicate(): the native bodies for a native storage, the + * methods of anything else (the PHP twin under the prefixed differential + * activation); UNDEF = pending exception */ +extern zend_class_entry *pt_ce_expression_result_storage; +/* VolatileExpressionHelper.cpp — the shadowing class entry (MutatingScope + * calls its statics directly) */ +extern zend_class_entry *pt_ce_volatile_expression_helper; +zv::Val pt_expression_result_storage_new(); +zv::Val pt_expression_result_storage_find(zval *storage, zval *expr); +zv::Val pt_expression_result_storage_duplicate(zval *storage); +/* the shadowing ExpressionResultStorageStack (ExpressionResultStorageStack.cpp) + * — $stack->getCurrent(): the native body for a native stack, the method for + * anything else; UNDEF = pending exception */ +extern zend_class_entry *pt_ce_expression_result_storage_stack; +zv::Val pt_expression_result_storage_stack_current(zval *stack); + +/* merged from the parallel port branch */ +/* the native ClassReflection (ClassReflection.cpp), shadowing + * PHPStan\Reflection\ClassReflection */ +extern zend_class_entry *pt_ce_class_reflection; +void pt_register_class_reflection(); +/* the getters the Type kernel calls millions of times per run: a direct + * C++ call into the native body when the object is the shadowing class + * (the common case), the PHP method when it is a foreign object. + * $classReflection->getName() / ->getCacheKey() / ->getNativeReflection(); + * UNDEF = pending exception */ +zv::Val pt_class_reflection_get_name(zend_object *classReflection); +zv::Val pt_class_reflection_get_cache_key(zend_object *classReflection); +zv::Val pt_class_reflection_get_native_reflection(zend_object *classReflection); +/* $classReflection->isGeneric() / ->hasMethod($methodName) / + * ->hasFinalByKeywordOverride() / ->isEnum(), coerced to bool as the call + * sites always did; false = pending exception */ +[[nodiscard]] bool pt_class_reflection_is_generic(zend_object *classReflection, bool &out); +bool pt_class_reflection_has_method(zend_object *classReflection, zval *methodName, bool &out); +bool pt_class_reflection_has_final_by_keyword_override(zend_object *classReflection, bool &out); +bool pt_class_reflection_is_enum(zend_object *classReflection, bool &out); +/* TypehintHelper::decideTypeFromReflection() for native callers (every + * argument borrowed, NULL for a null / the default); UNDEF = pending + * exception */ +zv::Val pt_typehint_helper_decide_type_from_reflection(zval *reflectionType, zval *phpDocType = NULL, zval *selfClass = NULL, bool isVariadic = false); + +/* merged from the parallel port branch */ +extern zend_string *pt_str_end_file_pos; + +/* the same for a bare byte range */ +bool pt_is_superglobal_cstr(const char *name, size_t len); +/* the superglobal names (Scope::SUPERGLOBAL_VARIABLES), in the twin's order */ +typedef struct _pt_superglobal_name { const char *name; size_t len; } pt_superglobal_name; +const pt_superglobal_name *pt_superglobal_names(size_t *count); + +/* $call->isFirstClassCallable() of a PhpParser CallLike node, read from its + * args: a single VariadicPlaceholder argument; false = pending exception */ +[[nodiscard]] bool pt_call_like_is_first_class_callable(zend_object *call, bool &out); + +/* {{{ the NodeScopeResolver-adjacent helper services (VolatileExpressionHelper.cpp, + * VariableFlow.cpp, VariableFlowBuilder.cpp) — registered at the END of the + * sequence (their signatures name the Type interface and their own classes; + * VariableFlow before VariableFlowBuilder, whose return types name it) */ + +extern zend_class_entry *pt_ce_variable_flow; +void pt_register_volatile_expression_helper(); +void pt_register_variable_flow(); +void pt_register_variable_flow_builder(); +void pt_register_variable_liveness_resolver(); +/* ExpressionResult.cpp — registered after VariableFlow (its signatures name + * it) */ +extern zend_class_entry *pt_ce_expression_result; +void pt_register_expression_result(); +/* $result->getVariableFlow() — the slot of a native result, the method + * otherwise; UNDEF = pending exception */ +zv::Val pt_expression_result_variable_flow(zval *result); +/* the per-request slot cache of VariableFlow.cpp (the class entry of the + * PHP VariableWrite class) */ +void pt_variable_flow_rinit(); +/* the property slots (OBJ_PROP byte offsets) of PHPStan\Node\Variable\VariableWrite, + * a final PHP class whose getters return its promoted properties: for an + * object that is exactly that class with every slot initialized the slots + * answer the getters; NULL otherwise (the caller calls the getters, which + * answer — or throw — the way the twin's calls did), with `error` set when + * the class map cannot resolve the class at all (exception pending) */ +typedef struct _pt_variable_write_slots { + zend_class_entry *ce; + uint32_t variableName; + uint32_t node; + uint32_t id; + uint32_t kind; + uint32_t offsetWrite; + uint32_t offset; + uint32_t parentId; + uint32_t replacesOffset; +} pt_variable_write_slots; +const pt_variable_write_slots *pt_variable_write_slots_of(zend_object *write, bool &error); +/* $storage->findExpressionResult($expr) — natively for a native storage, + * through the method otherwise ($expr borrowed); the result or null, UNDEF + * = pending exception */ +/* VariableFlow::sequence(...$flows) / read($name, $targetId, $container, + * $offset) / write($write, $redundantType) / escape($name) / dead($flow) / + * throwing($type, $canContinue, $canContainAnyThrowable) — the twin's + * factories (every argument borrowed, NULL for a null); a flow, PHP null + * where the twin returns null, UNDEF = pending exception */ +zv::Val pt_variable_flow_sequence(uint32_t argc, zval *argv); +/* VariableFlow::sequence(...$flows) spread from a PHP list */ +zv::Val pt_variable_flow_sequence_list(HashTable *flows); +zv::Val pt_variable_flow_read(zend_string *name, zval *targetId, bool container, zval *offset); +zv::Val pt_variable_flow_write(zval *write, zval *redundantType); +zv::Val pt_variable_flow_escape(zend_string *name); +zv::Val pt_variable_flow_dead(zval *flow); +zv::Val pt_variable_flow_throwing(zval *type, bool canContinue, bool canContainAnyThrowable); + +/* }}} */ + + +/* PhpClassReflectionExtension.cpp — the shadowing member factory behind + * ClassReflection's has*()/get*() methods; registered at the END of the + * sequence (its signatures name ClassReflection and the Type family, and + * its constructor instantiates the native LruCache) */ +extern zend_class_entry *pt_ce_php_class_reflection_extension; +void pt_register_php_class_reflection_extension(); +/* forgets the per-request class-entry/slot cache of the BetterReflection + * adapter memo readers */ +void pt_php_class_reflection_extension_rinit(); + #endif /* PHPSTANTURBO_SUPPORT_H */ diff --git a/turbo-ext/tests/php-class-reflection-family-fixture.php b/turbo-ext/tests/php-class-reflection-family-fixture.php new file mode 100644 index 00000000000..14da17e0b55 --- /dev/null +++ b/turbo-ext/tests/php-class-reflection-family-fixture.php @@ -0,0 +1,223 @@ +fromTrait, $times); + } + + public function aliasedMethod(): int + { + return 1; + } + +} + +interface FixtureInterface +{ + + /** @return non-empty-string */ + public function fromInterface(): string; + +} + +/** + * @property-read string $magicRead + * @property int $magicWrite + * @method string magicMethod(int $a) + * @template T of object + */ +abstract class FixtureBase implements FixtureInterface +{ + + use FixtureTrait { aliasedMethod as protected renamedMethod; } + + /** + * @var array + * @deprecated use something else + */ + public array $deprecatedProperty = []; + + /** @internal */ + protected int $internalProperty = 0; + + #[FixtureAttribute(label: 'attributed', weight: 3)] + public ?string $attributed = null; + + /** the type of this one is inferred from the constructor body */ + private $inferredFromConstructor; + + #[PrivateProperty] + public int $publicButPrivate = 1; + + #[ProtectedProperty] + public int $publicButProtected = 2; + + public function __construct( + /** @var non-empty-string */ + public readonly string $promoted = 'p', + protected int $promotedProtected = 7, + ) + { + $this->inferredFromConstructor = ['a' => 1, 'b' => 2]; + } + + public function __get(string $name): mixed + { + return null; + } + + public function __call(string $name, array $arguments): mixed + { + return null; + } + + /** @return non-empty-string */ + public function fromInterface(): string + { + return 'i'; + } + + /** + * @param callable(int): string $callback + * @param-immediately-invoked-callable $callback + * @param-out int $counter + * @throws \RuntimeException + * @phpstan-assert-if-true non-empty-string $input + * @deprecated do not + */ + abstract public function rich(callable $callback, int &$counter, mixed $input): bool; + + /** @return static */ + public function fluent(): static + { + return $this; + } + +} + +final class FixtureChild extends FixtureBase +{ + + public string $hooked = 'h' { + get => $this->hooked . '!'; + set (string $value) { + $this->hooked = $value; + } + } + + public function rich(callable $callback, int &$counter, mixed $input): bool + { + return true; + } + + /** @param-closure-this self $closure */ + public function withClosureThis(\Closure $closure): void + { + } + +} + +enum FixturePureEnum +{ + + case Alpha; + case Beta; + + public function label(): string + { + return $this->name; + } + +} + +enum FixtureBackedEnum: string implements FixtureInterface +{ + + case One = 'one'; + case Two = 'two'; + + /** @return non-empty-string */ + public function fromInterface(): string + { + return $this->value; + } + +} + +/** @immutable */ +final class FixtureImmutable +{ + + public function __construct(public int $value = 0) + { + } + +} + +/** + * The annotation properties here sit closer in the hierarchy than the real + * ones they shadow, so createProperty() takes its annotation branch. + * + * @property int $deprecatedProperty + * @property-read string $publicButProtected + * @method int traitMethod(int $times) + */ +#[\AllowDynamicProperties] +class FixtureAnnotated extends FixtureBase +{ + + public function rich(callable $callback, int &$counter, mixed $input): bool + { + return true; + } + +} + +/** + * The same annotations without #[AllowDynamicProperties]: the branch is + * then gated on the scope being able to read the native property. + * + * @property int $deprecatedProperty + * @property-read string $internalProperty + */ +class FixtureAnnotatedStrict extends FixtureBase +{ + + public function rich(callable $callback, int &$counter, mixed $input): bool + { + return true; + } + +} diff --git a/turbo-ext/tests/php-class-reflection-family.php b/turbo-ext/tests/php-class-reflection-family.php new file mode 100644 index 00000000000..91945cfff8d --- /dev/null +++ b/turbo-ext/tests/php-class-reflection-family.php @@ -0,0 +1,523 @@ += $this->maxDepth) { + return 'array(' . count($value) . ')'; + } + $out = []; + foreach ($value as $key => $item) { + $out[$key] = $this->describe($item, $depth + 1); + } + return $out; + } + if ($value instanceof \PHPStan\Type\Type) { + return 'type:' . $value->describe(VerbosityLevel::precise()); + } + if ($value instanceof \PHPStan\TrinaryLogic) { + return 'tri:' . $value->describe(); + } + if ($value instanceof ClassReflection) { + return 'class:' . $value->getName(); + } + if ($value instanceof \PHPStan\Type\Generic\TemplateTypeMap) { + return 'map:' . implode(',', array_map( + static fn (string $name, \PHPStan\Type\Type $type): string => $name . '=' . $type->describe(VerbosityLevel::precise()), + array_keys($value->getTypes()), + array_values($value->getTypes()), + )); + } + if ($value instanceof \PHPStan\Reflection\PassedByReference) { + return 'byRef:' . ($value->no() ? 'no' : ($value->createsNewVariable() ? 'new' : 'readsWrites')); + } + if ($value instanceof \PHPStan\Reflection\Assertions) { + return [ + 'asserts' => count($value->getAsserts()), + 'ifTrue' => count($value->getAssertsIfTrue()), + 'ifFalse' => count($value->getAssertsIfFalse()), + ]; + } + if ($value instanceof \PHPStan\PhpDoc\ResolvedPhpDocBlock) { + return 'phpdoc:' . ($value->hasPhpDocString() ? $value->getPhpDocString() : '(none)'); + } + if ($value instanceof \PHPStan\Reflection\AttributeReflection) { + return 'attr:' . $value->getName() . '(' . implode(',', array_map( + static fn (string $name, \PHPStan\Type\Type $type): string => $name . '=' . $type->describe(VerbosityLevel::precise()), + array_keys($value->getArgumentTypes()), + array_values($value->getArgumentTypes()), + )) . ')'; + } + if ($value instanceof \PHPStan\Reflection\ParameterAllowedConstants) { + return 'allowedConstants'; + } + if ($value instanceof \PHPStan\Reflection\ExtendedParametersAcceptor) { + return $this->describeVariant($value, $depth); + } + if ($value instanceof \PHPStan\Reflection\ExtendedParameterReflection) { + return $this->describeParameter($value, $depth); + } + if ($value instanceof \PHPStan\Reflection\ExtendedMethodReflection) { + return $this->describeMethod($value, $depth); + } + if ($value instanceof \PHPStan\Reflection\ExtendedPropertyReflection) { + return $this->describeProperty($value, $depth); + } + if (is_object($value)) { + return 'object:' . $this->normalizeClass(get_class($value)); + } + + return get_debug_type($value); + } + + /** @return array */ + public function describeProperty(\PHPStan\Reflection\ExtendedPropertyReflection $property, int $depth = 0): array + { + if ($depth >= $this->maxDepth) { + return ['property' => $property->getName()]; + } + + return [ + 'class' => $this->normalizeClass(get_class($property)), + 'name' => $property->getName(), + 'declaringClass' => $property->getDeclaringClass()->getName(), + 'docComment' => $property->getDocComment(), + 'static' => $property->isStatic(), + 'private' => $property->isPrivate(), + 'public' => $property->isPublic(), + 'readableType' => $property->getReadableType()->describe(VerbosityLevel::precise()), + 'writableType' => $property->getWritableType()->describe(VerbosityLevel::precise()), + 'hasPhpDocType' => $property->hasPhpDocType(), + 'phpDocType' => $property->getPhpDocType()->describe(VerbosityLevel::precise()), + 'hasNativeType' => $property->hasNativeType(), + 'nativeType' => $property->getNativeType()->describe(VerbosityLevel::precise()), + 'readable' => $property->isReadable(), + 'writable' => $property->isWritable(), + 'canChangeTypeAfterAssignment' => $property->canChangeTypeAfterAssignment(), + 'deprecated' => $property->isDeprecated()->describe(), + 'deprecatedDescription' => $property->getDeprecatedDescription(), + 'internal' => $property->isInternal()->describe(), + 'abstract' => $property->isAbstract()->describe(), + 'finalByKeyword' => $property->isFinalByKeyword()->describe(), + 'final' => $property->isFinal()->describe(), + 'virtual' => $property->isVirtual()->describe(), + 'hasGetHook' => $property->hasHook('get'), + 'hasSetHook' => $property->hasHook('set'), + 'getHook' => $property->hasHook('get') ? $this->describeMethod($property->getHook('get'), $depth + 1) : null, + 'setHook' => $property->hasHook('set') ? $this->describeMethod($property->getHook('set'), $depth + 1) : null, + 'protectedSet' => $property->isProtectedSet(), + 'privateSet' => $property->isPrivateSet(), + 'attributes' => $this->describe($property->getAttributes(), $depth + 1), + 'dummy' => $property->isDummy()->describe(), + ]; + } + + /** @return array */ + public function describeMethod(\PHPStan\Reflection\ExtendedMethodReflection $method, int $depth = 0): array + { + if ($depth >= $this->maxDepth) { + return ['method' => $method->getName()]; + } + + $namedVariants = $method->getNamedArgumentsVariants(); + + return [ + 'class' => $this->normalizeClass(get_class($method)), + 'name' => $method->getName(), + 'declaringClass' => $method->getDeclaringClass()->getName(), + 'docComment' => $method->getDocComment(), + 'static' => $method->isStatic(), + 'private' => $method->isPrivate(), + 'public' => $method->isPublic(), + 'deprecated' => $method->isDeprecated()->describe(), + 'deprecatedDescription' => $method->getDeprecatedDescription(), + 'final' => $method->isFinal()->describe(), + 'finalByKeyword' => $method->isFinalByKeyword()->describe(), + 'internal' => $method->isInternal()->describe(), + 'throwType' => $method->getThrowType() === null ? null : $method->getThrowType()->describe(VerbosityLevel::precise()), + 'hasSideEffects' => $method->hasSideEffects()->describe(), + 'pure' => $method->isPure()->describe(), + 'acceptsNamedArguments' => $method->acceptsNamedArguments()->describe(), + 'returnsByReference' => $method->returnsByReference()->describe(), + 'abstract' => $method->isAbstract() instanceof \PHPStan\TrinaryLogic ? $method->isAbstract()->describe() : $method->isAbstract(), + 'builtin' => $method->isBuiltin() instanceof \PHPStan\TrinaryLogic ? $method->isBuiltin()->describe() : $method->isBuiltin(), + 'selfOutType' => $method->getSelfOutType() === null ? null : $method->getSelfOutType()->describe(VerbosityLevel::precise()), + 'asserts' => $this->describe($method->getAsserts(), $depth + 1), + 'attributes' => $this->describe($method->getAttributes(), $depth + 1), + 'pureUnlessCallableIsImpureParameters' => $method->getPureUnlessCallableIsImpureParameters(), + 'mustUseReturnValue' => $method->mustUseReturnValue()->describe(), + 'resolvedPhpDoc' => $this->describe($method->getResolvedPhpDoc(), $depth + 1), + 'variants' => array_map(fn ($variant) => $this->describeVariant($variant, $depth + 1), $method->getVariants()), + 'namedVariants' => $namedVariants === null ? null : array_map(fn ($variant) => $this->describeVariant($variant, $depth + 1), $namedVariants), + ]; + } + + /** @return array */ + public function describeVariant(\PHPStan\Reflection\ExtendedParametersAcceptor $variant, int $depth = 0): array + { + return [ + 'templateTypeMap' => $this->describe($variant->getTemplateTypeMap(), $depth + 1), + 'resolvedTemplateTypeMap' => $this->describe($variant->getResolvedTemplateTypeMap(), $depth + 1), + 'variadic' => $variant->isVariadic(), + 'returnType' => $variant->getReturnType()->describe(VerbosityLevel::precise()), + 'phpDocReturnType' => $variant->getPhpDocReturnType()->describe(VerbosityLevel::precise()), + 'nativeReturnType' => $variant->getNativeReturnType()->describe(VerbosityLevel::precise()), + 'parameters' => array_map(fn ($parameter) => $this->describeParameter($parameter, $depth + 1), $variant->getParameters()), + ]; + } + + /** @return array */ + public function describeParameter(\PHPStan\Reflection\ExtendedParameterReflection $parameter, int $depth = 0): array + { + return [ + 'class' => $this->normalizeClass(get_class($parameter)), + 'name' => $parameter->getName(), + 'optional' => $parameter->isOptional(), + 'type' => $parameter->getType()->describe(VerbosityLevel::precise()), + 'phpDocType' => $parameter->getPhpDocType()->describe(VerbosityLevel::precise()), + 'hasNativeType' => $parameter->hasNativeType(), + 'nativeType' => $parameter->getNativeType()->describe(VerbosityLevel::precise()), + 'byRef' => $this->describe($parameter->passedByReference(), $depth + 1), + 'variadic' => $parameter->isVariadic(), + 'defaultValue' => $parameter->getDefaultValue() === null ? null : $parameter->getDefaultValue()->describe(VerbosityLevel::precise()), + 'outType' => $parameter->getOutType() === null ? null : $parameter->getOutType()->describe(VerbosityLevel::precise()), + 'immediatelyInvokedCallable' => $parameter->isImmediatelyInvokedCallable()->describe(), + 'closureThisType' => $parameter->getClosureThisType() === null ? null : $parameter->getClosureThisType()->describe(VerbosityLevel::precise()), + 'attributes' => $this->describe($parameter->getAttributes(), $depth + 1), + 'allowedConstants' => $parameter->getAllowedConstants() === null ? null : 'allowedConstants', + 'pureUnlessCallableIsImpure' => $parameter->isPureUnlessCallableIsImpureParameter()->describe(), + ]; + } + + public function normalizeClass(string $class): string + { + return str_starts_with($class, 'PHPStanTurbo\\') ? substr($class, strlen('PHPStanTurbo\\')) : $class; + } + +} + +} + +namespace { + +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; + +$pcreRoot = dirname(__DIR__, 2); +$pcreFixture = __DIR__ . '/php-class-reflection-family-fixture.php'; +require_once $pcreFixture; + +$pcreContainerFactory = new \PHPStan\DependencyInjection\ContainerFactory($pcreRoot); +$pcreContainer = $pcreContainerFactory->create( + sys_get_temp_dir() . '/phpstan-turbo-smoke-php-class-reflection', + [$pcreContainerFactory->getConfigDirectory() . '/config.level8.neon'], + [$pcreFixture], +); +$pcreReflectionProvider = $pcreContainer->getByType(\PHPStan\Reflection\ReflectionProvider::class); +$pcreReal = $pcreContainer->getByType(PhpClassReflectionExtension::class); + +// the collaborators of the container's own instance, by the constructor's +// parameter names: both sides are built from exactly the same services +$pcreCollaborators = []; +$pcreRealReflection = new ReflectionObject($pcreReal); +foreach ([ + 'scopeFactory', 'phpDocsResolver', 'nodeScopeResolver', 'methodReflectionFactory', + 'phpDocInheritanceResolver', 'deprecationProvider', 'annotationsMethodsClassReflectionExtension', + 'annotationsPropertiesClassReflectionExtension', 'signatureMapProvider', 'parser', + 'stubPhpDocProvider', 'reflectionProviderProvider', 'fileTypeMapper', + 'attributeReflectionFactory', 'allowedConstantsMapProvider', 'phpVersion', +] as $pcreName) { + $pcreProperty = $pcreRealReflection->getProperty($pcreName); + $pcreCollaborators[$pcreName] = $pcreProperty->getValue($pcreReal); +} + +/** @return array{PhpClassReflectionExtension, \PHPStanTurbo\PhpClassReflectionExtension} */ +$pcreBuildSides = static function (bool $infer, int $memberCacheKeysMax) use ($pcreCollaborators): array { + $make = static fn (string $class) => new $class( + scopeFactory: $pcreCollaborators['scopeFactory'], + phpDocsResolver: $pcreCollaborators['phpDocsResolver'], + nodeScopeResolver: $pcreCollaborators['nodeScopeResolver'], + methodReflectionFactory: $pcreCollaborators['methodReflectionFactory'], + phpDocInheritanceResolver: $pcreCollaborators['phpDocInheritanceResolver'], + deprecationProvider: $pcreCollaborators['deprecationProvider'], + annotationsMethodsClassReflectionExtension: $pcreCollaborators['annotationsMethodsClassReflectionExtension'], + annotationsPropertiesClassReflectionExtension: $pcreCollaborators['annotationsPropertiesClassReflectionExtension'], + signatureMapProvider: $pcreCollaborators['signatureMapProvider'], + parser: $pcreCollaborators['parser'], + stubPhpDocProvider: $pcreCollaborators['stubPhpDocProvider'], + reflectionProviderProvider: $pcreCollaborators['reflectionProviderProvider'], + fileTypeMapper: $pcreCollaborators['fileTypeMapper'], + attributeReflectionFactory: $pcreCollaborators['attributeReflectionFactory'], + allowedConstantsMapProvider: $pcreCollaborators['allowedConstantsMapProvider'], + inferPrivatePropertyTypeFromConstructor: $infer, + phpVersion: $pcreCollaborators['phpVersion'], + memberCacheKeysMax: $memberCacheKeysMax, + ); + + return [$make(PhpClassReflectionExtension::class), $make(\PHPStanTurbo\PhpClassReflectionExtension::class)]; +}; + +$pcreDescriber = new \PhpClassReflectionFamily\Describer(); +// an in-class scope makes getProperty() key its cache by both classes and +// gives createProperty() a scope whose canReadProperty()/getClassReflection() +// steer the annotation branch +$pcreScopeFactory = $pcreCollaborators['scopeFactory']; +$pcreBaseScope = $pcreScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($pcreFixture)); +$pcreScopes = [ + 'outOfClass' => new OutOfClassScope(), + 'inChild' => $pcreBaseScope->enterClass($pcreReflectionProvider->getClass('PhpClassReflectionFamilyFixture\\FixtureChild')), + 'inBase' => $pcreBaseScope->enterClass($pcreReflectionProvider->getClass('PhpClassReflectionFamilyFixture\\FixtureAnnotated')), +]; + +$pcreClasses = [ + 'PhpClassReflectionFamilyFixture\FixtureChild', + 'PhpClassReflectionFamilyFixture\FixtureBase', + 'PhpClassReflectionFamilyFixture\FixturePureEnum', + 'PhpClassReflectionFamilyFixture\FixtureBackedEnum', + 'PhpClassReflectionFamilyFixture\FixtureInterface', + 'PhpClassReflectionFamilyFixture\FixtureImmutable', + 'PhpClassReflectionFamilyFixture\FixtureAnnotated', + 'PhpClassReflectionFamilyFixture\FixtureAnnotatedStrict', + 'PhpClassReflectionFamilyFixture\FixtureTrait', + 'Exception', + 'ArrayObject', + 'DateTimeImmutable', + 'SplObjectStorage', + 'UnitEnum', + 'BackedEnum', + 'Closure', +]; +$pcreMembers = [ + 'fromTrait', 'traitMethod', 'aliasedMethod', 'renamedMethod', 'deprecatedProperty', + 'internalProperty', 'attributed', 'inferredFromConstructor', 'publicButPrivate', + 'publicButProtected', 'promoted', 'promotedProtected', 'hooked', 'rich', 'fluent', + 'fromInterface', 'magicRead', 'magicWrite', 'magicMethod', 'withClosureThis', + '__construct', '__get', '__call', 'name', 'value', 'cases', 'from', 'tryFrom', + 'label', 'getMessage', 'getCode', 'getPrevious', 'count', 'offsetGet', 'offsetSet', + 'format', 'modify', 'attach', 'fromCallable', 'bindTo', 'nonExistentMember', + // the adapter's `$name === ''` early return, and a spelling the + // lowercased-name memo has to normalize + '', 'GETMESSAGE', 'TraitMethod', +]; + +// {{{ every method's answer on every (class, member) pair +$pcreObservations = []; +foreach (['php', 'native'] as $pcreSide) { + [$pcrePhpSide, $pcreNativeSide] = $pcreBuildSides(true, 4096); + $pcreExtension = $pcreSide === 'php' ? $pcrePhpSide : $pcreNativeSide; + $pcreRows = []; + foreach ($pcreClasses as $pcreClassName) { + if (!$pcreReflectionProvider->hasClass($pcreClassName)) { + $pcreRows[$pcreClassName] = 'unknown class'; + continue; + } + $pcreClassReflection = $pcreReflectionProvider->getClass($pcreClassName); + foreach ($pcreMembers as $pcreMember) { + $pcreRow = []; + foreach ([ + 'hasProperty' => static fn () => $pcreExtension->hasProperty($pcreClassReflection, $pcreMember), + 'hasMethod' => static fn () => $pcreExtension->hasMethod($pcreClassReflection, $pcreMember), + 'hasNativeMethod' => static fn () => $pcreExtension->hasNativeMethod($pcreClassReflection, $pcreMember), + ] as $pcreLabel => $pcreCall) { + try { + $pcreRow[$pcreLabel] = $pcreCall(); + } catch (\Throwable $e) { + $pcreRow[$pcreLabel] = 'throws ' . $pcreDescriber->normalizeClass(get_class($e)) . ': ' . $e->getMessage(); + } + } + foreach ([ + 'getNativeProperty' => static fn () => $pcreDescriber->describeProperty($pcreExtension->getNativeProperty($pcreClassReflection, $pcreMember)), + 'getProperty' => static fn () => $pcreDescriber->describeProperty($pcreExtension->getProperty($pcreClassReflection, $pcreMember, $pcreScopes['outOfClass'])), + 'getProperty inChild' => static fn () => $pcreDescriber->describeProperty($pcreExtension->getProperty($pcreClassReflection, $pcreMember, $pcreScopes['inChild'])), + 'getProperty inBase' => static fn () => $pcreDescriber->describeProperty($pcreExtension->getProperty($pcreClassReflection, $pcreMember, $pcreScopes['inBase'])), + 'getMethod' => static fn () => $pcreDescriber->describeMethod($pcreExtension->getMethod($pcreClassReflection, $pcreMember)), + 'getNativeMethod' => static fn () => $pcreDescriber->describeMethod($pcreExtension->getNativeMethod($pcreClassReflection, $pcreMember)), + ] as $pcreLabel => $pcreCall) { + try { + $pcreRow[$pcreLabel] = $pcreCall(); + } catch (\Throwable $e) { + $pcreRow[$pcreLabel] = 'throws ' . $pcreDescriber->normalizeClass(get_class($e)) . ': ' . $e->getMessage(); + } + } + $pcreRows[$pcreClassName . '::' . $pcreMember] = $pcreRow; + } + + // createUserlandMethodReflection on the class's own native methods + foreach (['traitMethod', 'rich', 'fromInterface', '__construct'] as $pcreMethodName) { + try { + $pcreNative = $pcreClassReflection->getNativeReflection(); + if (!$pcreNative->hasMethod($pcreMethodName)) { + continue; + } + $pcreRows[$pcreClassName . '#userland#' . $pcreMethodName] = $pcreDescriber->describeMethod( + $pcreExtension->createUserlandMethodReflection( + $pcreClassReflection, + $pcreClassReflection, + $pcreNative->getMethod($pcreMethodName), + null, + ), + ); + } catch (\Throwable $e) { + $pcreRows[$pcreClassName . '#userland#' . $pcreMethodName] = 'throws ' . $pcreDescriber->normalizeClass(get_class($e)) . ': ' . $e->getMessage(); + } + } + } + + // memoization: repeated calls hand back the identical object + $pcreMemoClass = $pcreReflectionProvider->getClass('PhpClassReflectionFamilyFixture\FixtureChild'); + $pcreRows['#memo#nativeProperty'] = $pcreExtension->getNativeProperty($pcreMemoClass, 'attributed') === $pcreExtension->getNativeProperty($pcreMemoClass, 'attributed'); + $pcreRows['#memo#property'] = $pcreExtension->getProperty($pcreMemoClass, 'attributed', $pcreScopes['outOfClass']) === $pcreExtension->getProperty($pcreMemoClass, 'attributed', $pcreScopes['outOfClass']); + $pcreRows['#memo#method'] = $pcreExtension->getMethod($pcreMemoClass, 'rich') === $pcreExtension->getMethod($pcreMemoClass, 'rich'); + $pcreRows['#memo#nativeMethod'] = $pcreExtension->getNativeMethod($pcreMemoClass, 'rich') === $pcreExtension->getNativeMethod($pcreMemoClass, 'rich'); + // the case-insensitive alias the twin also stores under the requested spelling + $pcreRows['#memo#methodCase'] = $pcreExtension->getMethod($pcreMemoClass, 'RICH') === $pcreExtension->getMethod($pcreMemoClass, 'rich'); + + $pcreObservations[$pcreSide] = $pcreRows; +} + +/** The first differing path inside two descriptions, or null. */ +$pcreFirstDifference = static function ($expected, $actual, string $path = '') use (&$pcreFirstDifference): ?string { + if (is_array($expected) && is_array($actual)) { + foreach ($expected as $key => $value) { + if (!array_key_exists($key, $actual)) { + return $path . '/' . $key . ': missing natively'; + } + $deeper = $pcreFirstDifference($value, $actual[$key], $path . '/' . $key); + if ($deeper !== null) { + return $deeper; + } + } + foreach ($actual as $key => $value) { + if (!array_key_exists($key, $expected)) { + return $path . '/' . $key . ': only natively'; + } + } + + return null; + } + if ($expected === $actual) { + return null; + } + + return sprintf('%s: PHP %s, native %s', $path, json_encode($expected), json_encode($actual)); +}; + +foreach ($pcreObservations['php'] as $pcreKey => $pcreExpected) { + $pcreActual = $pcreObservations['native'][$pcreKey] ?? '(missing)'; + $pcreDifference = $pcreFirstDifference($pcreExpected, $pcreActual); + check($pcreDifference === null, 'PhpClassReflectionExtension ' . $pcreKey . ' ' . ($pcreDifference ?? '')); +} +check(count($pcreObservations['php']) === count($pcreObservations['native']), 'PhpClassReflectionExtension: both sides observed the same number of rows'); +// }}} + +// {{{ the shared member-cache LRU: the same keys survive, the same are evicted +$pcreEvictionObservations = []; +foreach (['php', 'native'] as $pcreSide) { + [$pcrePhpSide, $pcreNativeSide] = $pcreBuildSides(false, 2); + $pcreExtension = $pcreSide === 'php' ? $pcrePhpSide : $pcreNativeSide; + $pcreEvictionClasses = []; + foreach (['PhpClassReflectionFamilyFixture\FixtureChild', 'Exception', 'ArrayObject', 'DateTimeImmutable'] as $pcreClassName) { + $pcreEvictionClasses[$pcreClassName] = $pcreReflectionProvider->getClass($pcreClassName); + } + $pcreFirst = []; + foreach ($pcreEvictionClasses as $pcreClassName => $pcreClassReflection) { + $pcreMethodName = $pcreClassName === 'PhpClassReflectionFamilyFixture\FixtureChild' ? 'rich' : 'getIterator'; + if (!$pcreClassReflection->getNativeReflection()->hasMethod($pcreMethodName)) { + $pcreMethodName = $pcreClassReflection->getNativeReflection()->getMethods()[0]->getName(); + } + $pcreFirst[$pcreClassName] = [$pcreMethodName, $pcreExtension->getNativeMethod($pcreClassReflection, $pcreMethodName)]; + } + $pcreState = []; + foreach ($pcreFirst as $pcreClassName => [$pcreMethodName, $pcreFirstResult]) { + // with a limit of 2 the two oldest keys were evicted: those classes + // recompute (a different object), the two newest are memo hits + $pcreState[$pcreClassName] = $pcreExtension->getNativeMethod($pcreEvictionClasses[$pcreClassName], $pcreMethodName) === $pcreFirstResult; + } + $pcreCacheReflection = new ReflectionObject($pcreExtension); + foreach (['nativeMethods', 'methodsIncludingAnnotations', 'nativeProperties', 'propertiesIncludingAnnotations'] as $pcreCacheName) { + $pcreCacheProperty = $pcreCacheReflection->getProperty($pcreCacheName); + $pcreState['#keys#' . $pcreCacheName] = array_keys($pcreCacheProperty->getValue($pcreExtension)); + } + $pcreOrderProperty = $pcreCacheReflection->getProperty('memberCacheOrder'); + $pcreOrder = $pcreOrderProperty->getValue($pcreExtension); + $pcreState['#lru#count'] = $pcreOrder->count(); + $pcreState['#lru#keys'] = array_keys($pcreOrder->all()); + $pcreEvictionObservations[$pcreSide] = $pcreState; +} +check( + $pcreEvictionObservations['php'] === $pcreEvictionObservations['native'], + sprintf( + 'PhpClassReflectionExtension member-cache eviction: PHP %s, native %s', + json_encode($pcreEvictionObservations['php']), + json_encode($pcreEvictionObservations['native']), + ), +); +// }}} + +if (isset($pcreStandalone)) { + echo $failures === 0 ? "ALL OK\n" : "$failures failure(s)\n"; + exit($failures === 0 ? 0 : 1); +} + +} diff --git a/turbo-ext/tests/php84-syntax.neon b/turbo-ext/tests/php84-syntax.neon new file mode 100644 index 00000000000..42596f3bdbc --- /dev/null +++ b/turbo-ext/tests/php84-syntax.neon @@ -0,0 +1,5 @@ +# The fixtures use PHP 8.4 syntax (property hooks, asymmetric visibility). On +# an older PHP only the emulating lexer reads it, and PHPStan picks that lexer +# when phpVersion differs from the running PHP. +parameters: + phpVersion: 80400 diff --git a/turbo-ext/tests/reflection-family-fixture.php b/turbo-ext/tests/reflection-family-fixture.php new file mode 100644 index 00000000000..ec81d185077 --- /dev/null +++ b/turbo-ext/tests/reflection-family-fixture.php @@ -0,0 +1,447 @@ +secret(); + } + + private function secret(): int + { + return 1; + } + + public static function make(): static + { + return new static(); + } + +} + +trait Nested +{ + + use Greets; + + protected int $fromTrait = 0; + +} + +abstract class Base implements Shape +{ + + public int $pub = 0; + + protected static int $count = 0; + + private string $priv = ''; + + public function __construct(public int $x = 0) + { + } + + abstract public function area(): float; + + public function base(): int + { + return $this->x; + } + + private function hidden(): void + { + } + +} + +final class Circle extends Base implements Labeled, Countable +{ + + use Nested; + + public function area(): float + { + return 3.14; + } + + public function name(): string + { + return 'circle'; + } + + public function count(): int + { + return 1; + } + +} + +class Plain +{ + +} + +class Legacy +{ + + public function Legacy(): void + { + } + +} + +#[\AllowDynamicProperties] +class Dynamic +{ + +} + +class DynamicChild extends Dynamic +{ + +} + +class Magic +{ + + public function __get(string $name): mixed + { + return null; + } + +} + +readonly class Frozen +{ + + public function __construct(public int $v) + { + } + +} + +enum Suit: string +{ + + case Hearts = 'H'; + case Spades = 'S'; + + public const DEFAULT = self::Hearts; + + public function label(): string + { + return $this->value; + } + +} + +enum Pure +{ + + case A; + case B; + +} + +/** + * @template T of object + * @template U + * @implements IteratorAggregate + */ +class Box implements IteratorAggregate +{ + + /** @param T $value */ + public function __construct(public object $value) + { + } + + /** @return U|null */ + public function extra(): mixed + { + return null; + } + + public function getIterator(): Traversable + { + yield $this->value; + } + +} + +/** + * @extends Box + */ +class CircleBox extends Box +{ + +} + +/** + * @template T + * @extends Box + */ +class BoxOf extends Box +{ + +} + +/** + * @implements Repo + */ +class CircleRepo implements Repo +{ + + public function find(int $id): mixed + { + return null; + } + +} + +/** + * @template T of Shape + */ +abstract class ShapeRepo implements Repo +{ + +} + +class WithProps implements RequiresBase +{ + + public int $a = 0; + + public static int $s = 0; + + private string $p = ''; + + protected ?Circle $c = null; + +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class Marker +{ + + public function __construct(public string $v = '') + { + } + +} + +/** + * @final + */ +class DocFinal +{ + +} + +/** + * @deprecated Use Plain instead. + */ +class Old +{ + +} + +/** + * @phpstan-type Id int + * @phpstan-type Pair array{Id, string} + */ +class Aliases +{ + + public const LIMIT = 10; + + /** @var non-empty-string */ + public const NAME = 'alias'; + +} + +/** + * @phpstan-import-type Id from Aliases + * @phpstan-import-type Pair from Aliases as Duo + * @phpstan-import-type Missing from Aliases + * @phpstan-import-type Whatever from \Nope\Missing + * @phpstan-type Local float + */ +class ImportsAliases extends Aliases +{ + +} + +interface HasConstants +{ + + public const INHERITED = 'i'; + + /** @var array */ + public const DOCUMENTED = ['a']; + +} + +/** + * @template T of Shape + */ +class ConstantsHolder implements HasConstants +{ + + /** @var T|null */ + public const TEMPLATED = null; + + /** + * @deprecated no longer used + * @internal + * @final + */ + public const OLD = 1; + + public const int TYPED = 3; + +} + +/** + * @mixin Circle + * @property int $magicProp + * @property-read string $magicReadOnly + * @method string magicMethod(int $a) + * @method static int magicStatic() + * @phpstan-require-implements HasName + */ +interface Mixed_ +{ + +} + +/** + * @template T + * @mixin Box + */ +class MixinHolder +{ + +} + +/** + * @phpstan-sealed Circle|Plain + * @immutable + * @internal + * @phpstan-consistent-constructor + * @no-named-arguments + */ +abstract class SealedBase +{ + +} + +class ImmutableChild extends SealedBase +{ + +} + +#[Attribute] +final class DefaultFlags +{ + + public function __construct(public int $flags = Attribute::TARGET_ALL) + { + } + +} + +#[Attribute(flags: Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +final class NamedFlags +{ + +} + +#[Marker('x')] +#[\AllowDynamicProperties] +class Decorated +{ + +} + +enum Cards: int +{ + + #[Marker('h')] + case Hearts = 1; + + /** @deprecated use Hearts */ + case Spades = 2; + + public const FIRST = self::Hearts; + +} + +/** + * A documented trait, for the trait-context PHPDoc. + * + * @template T + * @property int $traitProp + */ +trait Documented +{ + + public function documented(): int + { + return 1; + } + +} + +class UsesDocumented +{ + + use Documented; + +} diff --git a/turbo-ext/tests/reflection-family.php b/turbo-ext/tests/reflection-family.php new file mode 100644 index 00000000000..e263d0ef13f --- /dev/null +++ b/turbo-ext/tests/reflection-family.php @@ -0,0 +1,823 @@ + */ + private static array $byNativeId = []; + + public static function register(object $native, ClassReflection $delegate): void + { + self::$byNativeId[spl_object_id($native)] = $delegate; + } + + public static function of(mixed $classReflection): mixed + { + if ($classReflection instanceof \PHPStanTurbo\ClassReflection) { + return self::$byNativeId[spl_object_id($classReflection)] ?? $classReflection; + } + + return $classReflection; + } + +} + +/** + * A class reflection extension of any kind, called on the delegate twin + * (the methods are declared: the native side resolves them through the + * class's function table, never through __call()). + */ +final class DuckExtension +{ + + public function __construct(private object $real) + { + } + + public function hasMethod(mixed $classReflection, string $methodName): bool + { + return $this->real->hasMethod(Twins::of($classReflection), $methodName); + } + + public function getMethod(mixed $classReflection, string $methodName): mixed + { + return $this->real->getMethod(Twins::of($classReflection), $methodName); + } + + public function hasNativeMethod(mixed $classReflection, string $methodName): bool + { + return $this->real->hasNativeMethod(Twins::of($classReflection), $methodName); + } + + public function getNativeMethod(mixed $classReflection, string $methodName): mixed + { + return $this->real->getNativeMethod(Twins::of($classReflection), $methodName); + } + + public function hasProperty(mixed $classReflection, string $propertyName): bool + { + return $this->real->hasProperty(Twins::of($classReflection), $propertyName); + } + + public function getProperty(mixed $classReflection, string $propertyName, mixed ...$rest): mixed + { + return $this->real->getProperty(Twins::of($classReflection), $propertyName, ...$rest); + } + + public function getNativeProperty(mixed $classReflection, string $propertyName): mixed + { + return $this->real->getNativeProperty(Twins::of($classReflection), $propertyName); + } + + public function hasInstanceProperty(mixed $classReflection, string $propertyName): bool + { + return $this->real->hasInstanceProperty(Twins::of($classReflection), $propertyName); + } + + public function getInstanceProperty(mixed $classReflection, string $propertyName): mixed + { + return $this->real->getInstanceProperty(Twins::of($classReflection), $propertyName); + } + + public function hasStaticProperty(mixed $classReflection, string $propertyName): bool + { + return $this->real->hasStaticProperty(Twins::of($classReflection), $propertyName); + } + + public function getStaticProperty(mixed $classReflection, string $propertyName): mixed + { + return $this->real->getStaticProperty(Twins::of($classReflection), $propertyName); + } + + public function supports(mixed $classReflection): bool + { + return $this->real->supports(Twins::of($classReflection)); + } + + /** @return array<\PHPStan\Type\Type> */ + public function getAllowedSubTypes(mixed $classReflection): array + { + return $this->real->getAllowedSubTypes(Twins::of($classReflection)); + } + +} + +final class DuckRegistry +{ + + public function __construct(private \PHPStan\Reflection\ClassReflectionExtensionRegistry $real) + { + } + + public function getPhpClassReflectionExtension(): DuckExtension + { + return new DuckExtension($this->real->getPhpClassReflectionExtension()); + } + + /** @return list */ + public function getPropertiesClassReflectionExtensions(): array + { + return array_map(static fn (object $e): DuckExtension => new DuckExtension($e), $this->real->getPropertiesClassReflectionExtensions()); + } + + /** @return list */ + public function getMethodsClassReflectionExtensions(): array + { + return array_map(static fn (object $e): DuckExtension => new DuckExtension($e), $this->real->getMethodsClassReflectionExtensions()); + } + + public function getRequireExtendsPropertyClassReflectionExtension(): DuckExtension + { + return new DuckExtension($this->real->getRequireExtendsPropertyClassReflectionExtension()); + } + + public function getRequireExtendsMethodsClassReflectionExtension(): DuckExtension + { + return new DuckExtension($this->real->getRequireExtendsMethodsClassReflectionExtension()); + } + + /** @return list */ + public function getAllowedSubTypesClassReflectionExtensions(): array + { + return array_map(static fn (object $e): DuckExtension => new DuckExtension($e), $this->real->getAllowedSubTypesClassReflectionExtensions()); + } + +} + +/** The native side's classReflectionExtensionRegistryProvider. */ +final class DuckRegistryProvider +{ + + public function __construct(private \PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider $real) + { + } + + public function getRegistry(): DuckRegistry + { + return new DuckRegistry($this->real->getRegistry()); + } + +} + +/** The class-map stand-in for the universal object crates check. */ +final class DuckUniversalObjectCrates +{ + + public static function isUniversalObjectCrate(ReflectionProvider $reflectionProvider, mixed $classReflection): bool + { + return UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate($reflectionProvider, Twins::of($classReflection)); + } + +} + +/** + * The class-map stand-in for InitializerExprContext: its + * fromClassReflection() receives the native $this. + */ +final class DuckInitializerExprContext +{ + + public static function fromClassReflection(mixed $classReflection): \PHPStan\Reflection\InitializerExprContext + { + return \PHPStan\Reflection\InitializerExprContext::fromClassReflection(Twins::of($classReflection)); + } + + public static function fromClass(string $className, ?string $fileName): \PHPStan\Reflection\InitializerExprContext + { + return \PHPStan\Reflection\InitializerExprContext::fromClass($className, $fileName); + } + + public static function fromScope(mixed ...$args): \PHPStan\Reflection\InitializerExprContext + { + return \PHPStan\Reflection\InitializerExprContext::fromScope(...$args); + } + +} + +/** + * The class-map stand-in for ParserNodeTypeToPHPStanType: the native + * TypehintHelper hands its resolve() the selfClass it was called with, + * which is the native $this for a class constant's native type. + */ +final class DuckParserNodeTypeToPHPStanType +{ + + public static function resolve($type, mixed $classReflection): \PHPStan\Type\Type + { + return \PHPStan\Type\ParserNodeTypeToPHPStanType::resolve($type, Twins::of($classReflection)); + } + +} + +/** + * The native side's phpDocInheritanceResolver: resolvePhpDocForConstant() + * takes the declaring class, which is the native $this for a constant the + * class itself declares. + */ +final class DuckPhpDocInheritanceResolver +{ + + public function __construct(private \PHPStan\PhpDoc\PhpDocInheritanceResolver $real) + { + } + + public function resolvePhpDocForConstant(mixed $declaringClass, string $constantName, ?\PHPStan\PhpDoc\ResolvedPhpDocBlock $currentResolvedPhpDoc): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock + { + return $this->real->resolvePhpDocForConstant(Twins::of($declaringClass), $constantName, $currentResolvedPhpDoc); + } + +} + +/** + * The native side's classReflectionFactory: withTypes()/withVariances() + * hand it the maps their native bodies build, which are the native + * TemplateTypeMap / TemplateTypeVarianceMap under the prefix (the twin's + * are the PHP ones its create() types). Rebuilding from getTypes() / + * getVariances() is exact for these two call sites — the maps the twin + * passes never carry lower-bound types. + */ +final class DuckClassReflectionFactory +{ + + public function __construct(private \PHPStan\Reflection\ClassReflectionFactory $real) + { + } + + public function create( + string $displayName, + \ReflectionClass $reflection, + ?string $anonymousFilename, + mixed $resolvedTemplateTypeMap, + ?\Closure $stubPhpDocBlockCallback, + ?string $extraCacheKey = null, + mixed $resolvedCallSiteVarianceMap = null, + ?bool $finalByKeywordOverride = null, + ): ClassReflection + { + if ($resolvedTemplateTypeMap instanceof \PHPStanTurbo\TemplateTypeMap) { + $resolvedTemplateTypeMap = new TemplateTypeMap($resolvedTemplateTypeMap->getTypes()); + } + if ($resolvedCallSiteVarianceMap instanceof \PHPStanTurbo\TemplateTypeVarianceMap) { + $resolvedCallSiteVarianceMap = new TemplateTypeVarianceMap($resolvedCallSiteVarianceMap->getVariances()); + } + + return $this->real->create($displayName, $reflection, $anonymousFilename, $resolvedTemplateTypeMap, $stubPhpDocBlockCallback, $extraCacheKey, $resolvedCallSiteVarianceMap, $finalByKeywordOverride); + } + +} + +/** A reflected type rendered for an eval()'d declaration: class names fully qualified, self/static the declaring class. */ +function qualifyType(string $type, string $selfClass = ClassReflection::class): string +{ + $builtin = ['string', 'int', 'bool', 'array', 'void', 'null', 'mixed', 'float', 'callable', 'iterable', 'object', 'never', 'false', 'true']; + return preg_replace_callback('~[A-Za-z_][A-Za-z0-9_\\\\]*~', static function (array $m) use ($builtin, $selfClass): string { + if (in_array(strtolower($m[0]), $builtin, true)) { + return $m[0]; + } + if (in_array(strtolower($m[0]), ['self', 'static'], true)) { + return '\\' . $selfClass; + } + return '\\' . $m[0]; + }, $type); +} + +/** + * Declares a stand-in for a class-map class the native bodies instantiate + * with the native $this: it builds the real object with the class + * reflections of $swapIndexes swapped for their twins and delegates every + * public method to it, implementing the real class's interfaces. The + * harness normalizes its name back to the real class, so the observations + * stay side-independent. + * + * @param list $swapIndexes constructor positions holding a class reflection + */ +function declareStandIn(string $realClass, string $standInName, array $swapIndexes): void +{ + $real = new \ReflectionClass($realClass); + $params = []; + $args = []; + foreach ($real->getMethod('__construct')->getParameters() as $i => $parameter) { + $swap = in_array($i, $swapIndexes, true); + $param = $swap ? 'mixed ' : ($parameter->hasType() ? qualifyType((string) $parameter->getType(), $realClass) . ' ' : ''); + $param .= '$' . $parameter->getName(); + if ($parameter->isDefaultValueAvailable()) { + $param .= ' = ' . var_export($parameter->getDefaultValue(), true); + } + $params[] = $param; + $args[] = $swap ? sprintf('\ReflectionFamily\Twins::of($%s)', $parameter->getName()) : '$' . $parameter->getName(); + } + + $methods = ''; + foreach ($real->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->isStatic() || $method->isConstructor()) { + continue; + } + $methodParams = []; + $methodArgs = []; + foreach ($method->getParameters() as $parameter) { + $param = $parameter->hasType() ? qualifyType((string) $parameter->getType(), $realClass) . ' ' : ''; + $param .= $parameter->isVariadic() ? '...' : ''; + $param .= '$' . $parameter->getName(); + if ($parameter->isDefaultValueAvailable()) { + $param .= ' = ' . var_export($parameter->getDefaultValue(), true); + } + $methodParams[] = $param; + $methodArgs[] = ($parameter->isVariadic() ? '...' : '') . '$' . $parameter->getName(); + } + $returnType = $method->hasReturnType() ? (string) $method->getReturnType() : ''; + $methods .= sprintf( + "\tpublic function %s(%s)%s { %s\$this->real->%s(%s); }\n", + $method->getName(), + implode(', ', $methodParams), + $returnType === '' ? '' : ': ' . qualifyType($returnType, $realClass), + $returnType === 'void' ? '' : 'return ', + $method->getName(), + implode(', ', $methodArgs), + ); + } + + $interfaces = $real->getInterfaceNames(); + eval(sprintf( + "namespace ReflectionFamily; final class %s%s {\n\tprivate \\%s \$real;\n\tpublic function __construct(%s) { \$this->real = new \\%s(%s); }\n%s}", + $standInName, + $interfaces === [] ? '' : ' implements \\' . implode(', \\', $interfaces), + $realClass, + implode(', ', $params), + $realClass, + implode(', ', $args), + $methods, + )); +} + +declareStandIn(\PHPStan\Reflection\EnumCaseReflection::class, 'DuckEnumCaseReflection', [0]); +declareStandIn(\PHPStan\Reflection\RealClassClassConstantReflection::class, 'DuckRealClassClassConstantReflection', [1]); + +final class Harness +{ + + /** @var array */ + private array $classNorm; + + /** @param array $manifest */ + public function __construct(array $manifest, private \PHPStan\PhpDoc\TypeNodeResolver $typeNodeResolver) + { + $this->classNorm = []; + foreach ($manifest as $shadowedClass => $entry) { + $this->classNorm[$entry['turboClass']] = $shadowedClass; + } + $this->classNorm[\PHPStanTurbo\ClassReflection::class] = ClassReflection::class; + $this->classNorm[DuckEnumCaseReflection::class] = \PHPStan\Reflection\EnumCaseReflection::class; + $this->classNorm[DuckRealClassClassConstantReflection::class] = \PHPStan\Reflection\RealClassClassConstantReflection::class; + } + + + public function className(object $object): string + { + return strtr(get_class($object), $this->classNorm); + } + + /** The 19 constructor arguments of a class reflection, by parameter name. */ + public function constructorArgs(ClassReflection $classReflection): array + { + $args = []; + $reflection = new \ReflectionClass(ClassReflection::class); + foreach ($reflection->getMethod('__construct')->getParameters() as $parameter) { + $args[$parameter->getName()] = $reflection->getProperty($parameter->getName())->getValue($classReflection); + } + + return $args; + } + + /** A comparable, side-independent rendering of any value. */ + public function norm(mixed $value): mixed + { + if (is_array($value)) { + $out = []; + foreach ($value as $k => $v) { + $out[$k] = $this->norm($v); + } + return $out; + } + if (!is_object($value)) { + return $value; + } + if ($value instanceof ClassReflection || $value instanceof \PHPStanTurbo\ClassReflection) { + return ['R', $this->className($value), $value->getDisplayName()]; + } + if ($value instanceof \ReflectionClass || $value instanceof \ReflectionMethod || $value instanceof \ReflectionProperty || $value instanceof \ReflectionClassConstant) { + return ['B', get_class($value), $value->getName()]; + } + if ($value instanceof MethodReflection) { + return ['M', $this->className($value), $value->getName(), $value->getDeclaringClass()->getName()]; + } + if ($value instanceof PropertyReflection) { + return ['P', $this->className($value), $value->getDeclaringClass()->getName(), $value->isStatic(), $value->isPrivate()]; + } + if ($value instanceof \PHPStan\Reflection\ClassConstantReflection) { + return ['K', $this->className($value), $value->getName(), $value->getDeclaringClass()->getName()]; + } + if ($value instanceof Type) { + return ['Y', $this->className($value), $value->describe(VerbosityLevel::precise())]; + } + if ($value instanceof TemplateTypeMap || $value instanceof \PHPStanTurbo\TemplateTypeMap) { + return ['TM', $this->norm($value->getTypes())]; + } + if ($value instanceof TemplateTypeVarianceMap || $value instanceof \PHPStanTurbo\TemplateTypeVarianceMap) { + return ['VM', array_map(static fn (TemplateTypeVariance $v): string => $v->describe(), $value->getVariances())]; + } + if ($value instanceof \PHPStan\PhpDoc\ResolvedPhpDocBlock) { + return ['D', $this->className($value)]; + } + if ($this->className($value) === \PHPStan\Reflection\EnumCaseReflection::class) { + return ['EC', $value->getName(), $value->getDeclaringEnum()->getName(), $this->norm($value->getBackingValueType()), $value->isDeprecated()->describe(), $value->getDeprecatedDescription(), $this->norm($value->getAttributes())]; + } + if ($value instanceof \PHPStan\Reflection\AttributeReflection) { + return ['A', $value->getName(), $this->norm($value->getArgumentTypes())]; + } + if ($value instanceof \PHPStan\Type\TypeAlias) { + return ['TA', $this->norm($value->resolve($this->typeNodeResolver))]; + } + if ($value instanceof \PHPStan\PhpDoc\Tag\TemplateTag) { + return ['TT', $value->getName(), $this->norm($value->getBound()), $this->norm($value->getDefault()), $value->getVariance()->describe()]; + } + if ($value instanceof \PHPStan\PhpDoc\Tag\PropertyTag) { + return ['PT', $this->norm($value->getReadableType()), $this->norm($value->getWritableType()), $value->isReadable(), $value->isWritable()]; + } + if ($value instanceof \PHPStan\PhpDoc\Tag\MethodTag) { + return ['MT', $this->norm($value->getReturnType()), $value->isStatic(), $this->norm(array_keys($value->getParameters()))]; + } + if ($value instanceof \Closure) { + return ['F']; + } + if ($value instanceof \Throwable) { + return ['E', $this->className($value), strtr($value->getMessage(), $this->classNorm)]; + } + if (method_exists($value, 'getType')) { + return ['G', $this->className($value), $this->norm($value->getType())]; + } + + return ['O', $this->className($value)]; + } + +} + +} + +namespace { + +use ReflectionFamily\DuckRegistryProvider; +use ReflectionFamily\DuckUniversalObjectCrates; +use ReflectionFamily\Harness; +use ReflectionFamily\Twins; + +$rfManifest = json_decode(file_get_contents(dirname(__DIR__, 2) . '/vendor/turbo-shadowed-classes.json'), true, 8, JSON_THROW_ON_ERROR); +$rfClassMap = require dirname(__DIR__, 2) . '/vendor/turbo-class-map.php'; + +// a container of its own: the fixture must be an analysed path for the +// reflection provider to find its classes +$rfFile = __DIR__ . '/reflection-family-fixture.php'; +$rfContainerFactory = new \PHPStan\DependencyInjection\ContainerFactory(dirname(__DIR__, 2)); +$rfContainer = $rfContainerFactory->create(sys_get_temp_dir() . '/phpstan-turbo-smoke-reflection', [$rfContainerFactory->getConfigDirectory() . '/config.level8.neon'], [$rfFile]); +$rfHarness = new Harness($rfManifest, $rfContainer->getByType(\PHPStan\PhpDoc\TypeNodeResolver::class)); +$rfReflectionProvider = $rfContainer->getByType(\PHPStan\Reflection\ReflectionProvider::class); +$rfScopeFactory = $rfContainer->getByType(\PHPStan\Analyser\ScopeFactory::class); +$rfRegistryProvider = $rfContainer->getByType(\PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider::class); + +// the class-map classes the native bodies hand the native $this to: routed +// to the stand-ins for the duration of the test +\PHPStanTurbo\Runtime::configure([ + 'universalObjectCratesClassReflectionExtension' => DuckUniversalObjectCrates::class, + 'initializerExprContext' => \ReflectionFamily\DuckInitializerExprContext::class, + 'enumCaseReflection' => \ReflectionFamily\DuckEnumCaseReflection::class, + 'realClassClassConstantReflection' => \ReflectionFamily\DuckRealClassClassConstantReflection::class, + 'parserNodeTypeToPHPStanType' => \ReflectionFamily\DuckParserNodeTypeToPHPStanType::class, +]); + +// ---- the samples: the fixture's classes, built-ins with stubs, derived reflections ---- +/** @var array $rfSamples */ +$rfSamples = []; +foreach ([ + 'ReflectionFamilyFixture\Shape', 'ReflectionFamilyFixture\HasName', 'ReflectionFamilyFixture\Labeled', 'ReflectionFamilyFixture\Repo', 'ReflectionFamilyFixture\RequiresBase', + 'ReflectionFamilyFixture\Greets', 'ReflectionFamilyFixture\Nested', + 'ReflectionFamilyFixture\Base', 'ReflectionFamilyFixture\Circle', 'ReflectionFamilyFixture\Plain', 'ReflectionFamilyFixture\Legacy', + 'ReflectionFamilyFixture\Dynamic', 'ReflectionFamilyFixture\DynamicChild', 'ReflectionFamilyFixture\Magic', 'ReflectionFamilyFixture\Frozen', + 'ReflectionFamilyFixture\Suit', 'ReflectionFamilyFixture\Pure', + 'ReflectionFamilyFixture\Box', 'ReflectionFamilyFixture\CircleBox', 'ReflectionFamilyFixture\BoxOf', 'ReflectionFamilyFixture\CircleRepo', 'ReflectionFamilyFixture\ShapeRepo', + 'ReflectionFamilyFixture\WithProps', 'ReflectionFamilyFixture\Marker', 'ReflectionFamilyFixture\DocFinal', 'ReflectionFamilyFixture\Old', + 'ReflectionFamilyFixture\Aliases', 'ReflectionFamilyFixture\ImportsAliases', 'ReflectionFamilyFixture\HasConstants', 'ReflectionFamilyFixture\ConstantsHolder', + 'ReflectionFamilyFixture\Mixed_', 'ReflectionFamilyFixture\MixinHolder', 'ReflectionFamilyFixture\SealedBase', 'ReflectionFamilyFixture\ImmutableChild', + 'ReflectionFamilyFixture\DefaultFlags', 'ReflectionFamilyFixture\NamedFlags', 'ReflectionFamilyFixture\Decorated', 'ReflectionFamilyFixture\Cards', + 'ReflectionFamilyFixture\Documented', 'ReflectionFamilyFixture\UsesDocumented', + 'ArrayObject', 'ArrayAccess', 'Countable', 'Traversable', 'Iterator', 'IteratorAggregate', 'Attribute', 'stdClass', 'BackedEnum', 'UnitEnum', + 'Exception', 'Throwable', 'Closure', 'DateTimeImmutable', 'SplObjectStorage', 'WeakMap', 'ReflectionClass', +] as $rfName) { + check($rfReflectionProvider->hasClass($rfName), "reflection-family: $rfName is known to the reflection provider"); + if (!$rfReflectionProvider->hasClass($rfName)) { + continue; + } + $rfSamples[$rfName] = $rfReflectionProvider->getClass($rfName); +} +$rfCircle = new \PHPStan\Type\ObjectType('ReflectionFamilyFixture\Circle'); +$rfSamples['Box'] = $rfSamples['ReflectionFamilyFixture\Box']->withTypes([$rfCircle, new \PHPStan\Type\IntegerType()]); +$rfSamples['Box covariant'] = $rfSamples['Box']->withVariances([\PHPStan\Type\Generic\TemplateTypeVariance::createCovariant(), \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant()]); +$rfSamples['BoxOf'] = $rfSamples['ReflectionFamilyFixture\BoxOf']->withTypes([new \PHPStan\Type\StringType()]); +$rfSamples['Repo'] = $rfSamples['ReflectionFamilyFixture\Repo']->withTypes([$rfCircle]); +$rfSamples['Circle<>'] = $rfSamples['ReflectionFamilyFixture\Circle']->withTypes([]); +$rfSamples['Plain final'] = $rfSamples['ReflectionFamilyFixture\Plain']->asFinal(); +$rfSamples['Plain non-final'] = $rfSamples['ReflectionFamilyFixture\Plain']->removeFinalKeywordOverride(); +$rfSamples['ArrayObject'] = $rfSamples['ArrayObject']->withTypes([new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()]); +$rfSamples['ConstantsHolder'] = $rfSamples['ReflectionFamilyFixture\ConstantsHolder']->withTypes([$rfCircle]); +$rfSamples['MixinHolder'] = $rfSamples['ReflectionFamilyFixture\MixinHolder']->withTypes([new \PHPStan\Type\IntegerType()]); +$rfSamples['ImmutableChild final'] = $rfSamples['ReflectionFamilyFixture\ImmutableChild']->asFinal(); + +$rfProbeMethods = ['__construct', '__get', '__set', '__isset', 'area', 'greet', 'secret', 'make', 'base', 'hidden', 'label', 'getIterator', 'count', 'name', 'find', 'extra', 'cases', 'from', 'tryFrom', 'offsetGet', 'getMessage', 'nope', '123', 'Legacy']; +$rfProbeProperties = ['x', 'pub', 'priv', 'count', 'fromTrait', 'a', 's', 'p', 'c', 'v', 'value', 'name', 'nope', 'message', '123']; +$rfProbeTraits = ['ReflectionFamilyFixture\Greets', 'ReflectionFamilyFixture\Nested', 'ReflectionFamilyFixture\Documented', 'Nope\Missing']; +$rfProbeConstants = ['DEFAULT', 'LIMIT', 'NAME', 'INHERITED', 'DOCUMENTED', 'TEMPLATED', 'OLD', 'TYPED', 'FIRST', 'nope', 'TARGET_CLASS']; +$rfProbeEnumCases = ['Hearts', 'Spades', 'A', 'B', 'Nope']; +$rfProbeClasses = ['ReflectionFamilyFixture\Base', 'ReflectionFamilyFixture\Shape', 'ReflectionFamilyFixture\Circle', 'ReflectionFamilyFixture\Box', 'ReflectionFamilyFixture\HasName', 'ReflectionFamilyFixture\Dynamic', 'Countable', 'Traversable', 'IteratorAggregate', 'stdClass', 'Throwable', 'Nope\Missing']; +// the memo slots the ported methods own (a slot an unported method fills +// lands in the delegate twin on the native side) and the constructor's +// value slots; $subclasses is compared as a subset — the crate check's +// callback into the PHP twin's is() fills it on that side only +$rfMemoSlots = [ + 'methods', 'properties', 'instanceProperties', 'staticProperties', 'constants', 'enumCases', 'classHierarchyDistances', + 'deprecatedDescription', 'isDeprecated', 'isGeneric', 'isInternal', 'isFinal', 'isImmutable', 'hasConsistentConstructor', 'acceptsNamedArguments', + 'templateTypeMap', 'activeTemplateTypeMap', 'defaultCallSiteVarianceMap', 'callSiteVarianceMap', 'ancestors', 'cacheKey', 'filename', + 'reflectionDocComment', 'stubPhpDocBlock', 'resolvedPhpDocBlock', 'traitContextResolvedPhpDocBlock', + 'cachedInterfaces', 'cachedParentClass', 'typeAliases', 'hasMethodCache', 'hasPropertyCache', 'hasInstancePropertyCache', 'hasStaticPropertyCache', 'name', + 'displayName', 'reflection', 'anonymousFilename', 'resolvedTemplateTypeMap', 'stubPhpDocBlockCallback', 'extraCacheKey', 'resolvedCallSiteVarianceMap', 'finalByKeywordOverride', +]; + +// ---- settle the shared state first ---- +// The twin's hasInstanceProperty() writes its last two answers into +// $hasPropertyCache (not the instance cache): a require-extends probe +// reaching the provider's reflection of the required class through an +// ObjectType overwrites that class's hasProperty() memo mid-sequence, so +// the side probing first would see a different answer than the side +// probing second. Both sides are ported faithfully; the shared state is +// settled here so the comparison sees one answer. +foreach ($rfSamples as $rfOriginal) { + foreach ($rfProbeProperties as $property) { + $rfOriginal->hasProperty($property); + $rfOriginal->hasInstanceProperty($property); + $rfOriginal->hasStaticProperty($property); + } +} + +// ---- rebuild each sample on both sides and compare method by method ---- +$rfObservations = ['php' => [], 'native' => []]; +$rfSubclassesMemo = ['php' => [], 'native' => []]; +$rfKeepAlive = []; +$rfSampleCount = 0; +foreach ($rfSamples as $rfLabel => $rfOriginal) { + $rfArgs = $rfHarness->constructorArgs($rfOriginal); + $rfSampleCount++; + $rfOutOfClassScope = new \PHPStan\Analyser\OutOfClassScope(); + // a trait is entered through a class using it + $rfInClassContext = $rfOriginal->isTrait() + ? \PHPStan\Analyser\ScopeContext::create($rfFile)->enterClass($rfSamples['ReflectionFamilyFixture\Circle'])->enterTrait($rfOriginal) + : \PHPStan\Analyser\ScopeContext::create($rfFile)->enterClass($rfOriginal); + $rfInClassScope = $rfScopeFactory->create($rfInClassContext); + $rfOtherScope = $rfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($rfFile)->enterClass($rfSamples['ReflectionFamilyFixture\Plain'])); + foreach (['php', 'native'] as $side) { + $observe = static function (string $label, callable $fn) use (&$rfObservations, $side, $rfHarness, $rfLabel): void { + try { + $rfObservations[$side][$rfLabel][$label] = $rfHarness->norm($fn()); + } catch (\Throwable $e) { + $rfObservations[$side][$rfLabel][$label] = $rfHarness->norm($e); + } + }; + + $args = $rfArgs; + if ($side === 'native') { + $delegate = new \PHPStan\Reflection\ClassReflection(...array_values($args)); + $args['classReflectionExtensionRegistryProvider'] = new DuckRegistryProvider($rfRegistryProvider); + $args['phpDocInheritanceResolver'] = new \ReflectionFamily\DuckPhpDocInheritanceResolver($args['phpDocInheritanceResolver']); + $args['classReflectionFactory'] = new \ReflectionFamily\DuckClassReflectionFactory($args['classReflectionFactory']); + $classReflection = new \PHPStanTurbo\ClassReflection(...array_values($args)); + Twins::register($classReflection, $delegate); + $rfKeepAlive[] = $classReflection; + $rfKeepAlive[] = $delegate; + $nativeReflection = new \ReflectionClass(\PHPStanTurbo\ClassReflection::class); + } else { + $classReflection = new \PHPStan\Reflection\ClassReflection(...array_values($args)); + $nativeReflection = new \ReflectionClass(\PHPStan\Reflection\ClassReflection::class); + } + + // the plain getters, twice each: the memoized answer must equal the computed one + foreach ([ + 'getNativeReflection', 'getFileName', 'getName', 'getDisplayName', 'getCacheKey', 'isAbstract', 'isInterface', 'isTrait', 'isEnum', 'getClassTypeDescription', 'isReadOnly', 'isBackedEnum', 'isClass', 'isAnonymous', + 'hasFinalByKeywordOverride', 'isFinalByKeyword', 'isFinal', 'isGeneric', 'allowsDynamicProperties', 'hasConstructor', 'getConstructor', + 'getParentClass', 'getParents', 'getImmediateInterfaces', 'getInterfaces', 'getClassHierarchyDistances', + 'getBackedEnumType', 'getEnumCases', 'getParentClassesNames', 'getTypeAliases', 'getDeprecatedDescription', 'isDeprecated', + 'isBuiltin', 'isInternal', 'isImmutable', 'hasConsistentConstructor', 'acceptsNamedArguments', 'isAttributeClass', 'getAttributeClassFlags', + 'getAttributes', 'getObjectType', 'getTemplateTypeMap', 'getActiveTemplateTypeMap', 'getPossiblyIncompleteActiveTemplateTypeMap', + 'getCallSiteVarianceMap', 'getResolvedPhpDoc', 'getExtendsTags', 'getImplementsTags', 'getTemplateTags', 'getMixinTags', + 'getRequireExtendsTags', 'getRequireImplementsTags', 'getSealedTags', 'getPropertyTags', 'getMethodTags', + 'getAncestors', 'getResolvedMixinTypes', 'getAllowedSubTypes', + ] as $method) { + $observe($method, static fn () => $classReflection->$method()); + $observe("$method again", static fn () => $classReflection->$method()); + } + $observe('getDisplayName(false)', static fn () => $classReflection->getDisplayName(false)); + $observe('getDisplayName(true)', static fn () => $classReflection->getDisplayName(true)); + + // members + foreach ($rfProbeMethods as $method) { + $observe("hasMethod($method)", static fn () => $classReflection->hasMethod($method)); + $observe("hasNativeMethod($method)", static fn () => $classReflection->hasNativeMethod($method)); + $observe("getMethod($method, out of class)", static fn () => $classReflection->getMethod($method, $rfOutOfClassScope)); + $observe("getMethod($method, in class)", static fn () => $classReflection->getMethod($method, $rfInClassScope)); + $observe("getMethod($method, other class)", static fn () => $classReflection->getMethod($method, $rfOtherScope)); + $observe("getNativeMethod($method)", static fn () => $classReflection->getNativeMethod($method)); + $observe("hasMethod($method) again", static fn () => $classReflection->hasMethod($method)); + } + foreach ($rfProbeProperties as $property) { + $observe("hasProperty($property)", static fn () => $classReflection->hasProperty($property)); + $observe("hasInstanceProperty($property)", static fn () => $classReflection->hasInstanceProperty($property)); + $observe("hasStaticProperty($property)", static fn () => $classReflection->hasStaticProperty($property)); + $observe("hasNativeProperty($property)", static fn () => $classReflection->hasNativeProperty($property)); + $observe("getProperty($property, out of class)", static fn () => $classReflection->getProperty($property, $rfOutOfClassScope)); + $observe("getProperty($property, in class)", static fn () => $classReflection->getProperty($property, $rfInClassScope)); + $observe("getInstanceProperty($property, in class)", static fn () => $classReflection->getInstanceProperty($property, $rfInClassScope)); + $observe("getInstanceProperty($property, other class)", static fn () => $classReflection->getInstanceProperty($property, $rfOtherScope)); + $observe("getStaticProperty($property)", static fn () => $classReflection->getStaticProperty($property)); + $observe("getNativeProperty($property)", static fn () => $classReflection->getNativeProperty($property)); + $observe("hasInstanceProperty($property) again", static fn () => $classReflection->hasInstanceProperty($property)); + } + + // class relations + foreach ($rfProbeClasses as $className) { + $observe("is($className)", static fn () => $classReflection->is($className)); + $observe("isSubclassOf($className)", static fn () => $classReflection->isSubclassOf($className)); + $observe("implementsInterface($className)", static fn () => $classReflection->implementsInterface($className)); + if (isset($rfSamples[$className])) { + $observe("isSubclassOfClass($className)", static fn () => $classReflection->isSubclassOfClass($rfSamples[$className])); + $observe("isSubclassOfClass($className) again", static fn () => $classReflection->isSubclassOfClass($rfSamples[$className])); + } + } + $observe('isSubclassOfClass(Plain final)', static fn () => $classReflection->isSubclassOfClass($rfSamples['Plain final'])); + $observe('isSubclassOfClass(Box)', static fn () => $classReflection->isSubclassOfClass($rfSamples['Box'])); + + // traits, ancestors, constants, enum cases + $observe('getTraits(false)', static fn () => $classReflection->getTraits()); + $observe('getTraits(true)', static fn () => $classReflection->getTraits(true)); + $observe('getTraits(true) again', static fn () => $classReflection->getTraits(true)); + foreach ($rfProbeTraits as $traitName) { + $observe("hasTraitUse($traitName)", static fn () => $classReflection->hasTraitUse($traitName)); + } + foreach ($rfProbeClasses as $className) { + $observe("getAncestorWithClassName($className)", static fn () => $classReflection->getAncestorWithClassName($className)); + } + foreach ($rfProbeConstants as $constantName) { + $observe("hasConstant($constantName)", static fn () => $classReflection->hasConstant($constantName)); + $observe("getConstant($constantName)", static fn () => $classReflection->getConstant($constantName)); + $observe("getConstant($constantName) again", static fn () => $classReflection->getConstant($constantName)); + $observe("getConstantPhpDocType($constantName)", static fn () => $classReflection->getConstantPhpDocType($constantName)); + } + foreach ($rfProbeEnumCases as $caseName) { + $observe("hasEnumCase($caseName)", static fn () => $classReflection->hasEnumCase($caseName)); + $observe("getEnumCase($caseName)", static fn () => $classReflection->getEnumCase($caseName)); + } + + // the generics machinery + $observe('typeMapToList(getTemplateTypeMap())', static fn () => $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); + $observe('typeMapToList(empty)', static fn () => $classReflection->typeMapToList(TemplateTypeMap::createEmpty())); + $observe('varianceMapToList(getCallSiteVarianceMap())', static fn () => $classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap())); + $observe('varianceMapToList(empty)', static fn () => $classReflection->varianceMapToList(TemplateTypeVarianceMap::createEmpty())); + $observe('typeMapFromList([])', static fn () => $classReflection->typeMapFromList([])); + $observe('typeMapFromList([Circle])', static fn () => $classReflection->typeMapFromList([new \PHPStan\Type\ObjectType('ReflectionFamilyFixture\Circle')])); + $observe('typeMapFromList([Circle,int])', static fn () => $classReflection->typeMapFromList([new \PHPStan\Type\ObjectType('ReflectionFamilyFixture\Circle'), new \PHPStan\Type\IntegerType()])); + $observe('varianceMapFromList([])', static fn () => $classReflection->varianceMapFromList([])); + $observe('varianceMapFromList([covariant])', static fn () => $classReflection->varianceMapFromList([TemplateTypeVariance::createCovariant()])); + $observe('withTypes([])', static fn () => $classReflection->withTypes([])); + $observe('withTypes([Circle,int])', static fn () => $classReflection->withTypes([new \PHPStan\Type\ObjectType('ReflectionFamilyFixture\Circle'), new \PHPStan\Type\IntegerType()])); + $observe('withVariances([covariant])', static fn () => $classReflection->withVariances([TemplateTypeVariance::createCovariant()])); + $observe('asFinal', static fn () => $classReflection->asFinal()); + $observe('withoutFinalByKeywordOverride', static fn () => $classReflection->withoutFinalByKeywordOverride()); + $observe('removeFinalKeywordOverride', static fn () => $classReflection->removeFinalKeywordOverride()); + $observe('getTraitContextResolvedPhpDoc(Circle)', static fn () => $classReflection->getTraitContextResolvedPhpDoc($rfSamples['ReflectionFamilyFixture\Circle'])); + $observe('getTraitContextResolvedPhpDoc(Greets)', static fn () => $classReflection->getTraitContextResolvedPhpDoc($rfSamples['ReflectionFamilyFixture\Greets'])); + $observe('getTraitContextResolvedPhpDoc(UsesDocumented)', static fn () => $classReflection->getTraitContextResolvedPhpDoc($rfSamples['ReflectionFamilyFixture\UsesDocumented'])); + + // the memo state after all of the above, then after the eviction + $memo = static function () use ($classReflection, $nativeReflection, $rfMemoSlots, $rfHarness): array { + $state = []; + foreach ($rfMemoSlots as $slot) { + try { + $state[$slot] = $rfHarness->norm($nativeReflection->getProperty($slot)->getValue($classReflection)); + } catch (\Throwable $e) { + $state[$slot] = $rfHarness->norm($e); + } + } + return $state; + }; + $observe('memo', $memo); + $subclasses = static fn (): array => $rfHarness->norm($nativeReflection->getProperty('subclasses')->getValue($classReflection)); + $rfSubclassesMemo[$side][$rfLabel] = $subclasses(); + $observe('evictPrivateSymbols', static fn () => $classReflection->evictPrivateSymbols()); + $observe('memo after eviction', $memo); + } +} + +// the class map back to the generated one +\PHPStanTurbo\Runtime::configure([ + 'universalObjectCratesClassReflectionExtension' => $rfClassMap['universalObjectCratesClassReflectionExtension'], + 'initializerExprContext' => $rfClassMap['initializerExprContext'], + 'enumCaseReflection' => $rfClassMap['enumCaseReflection'], + 'realClassClassConstantReflection' => $rfClassMap['realClassClassConstantReflection'], + 'parserNodeTypeToPHPStanType' => $rfClassMap['parserNodeTypeToPHPStanType'], +]); + +foreach ($rfObservations['php'] as $rfLabel => $rfPhpObservations) { + $rfNativeObservations = $rfObservations['native'][$rfLabel] ?? []; + foreach ($rfPhpObservations as $label => $expected) { + $actual = array_key_exists($label, $rfNativeObservations) ? $rfNativeObservations[$label] : ''; + check($expected === $actual, sprintf('ClassReflection parity (%s) %s: %s vs %s', $rfLabel, $label, json_encode($expected), json_encode($actual))); + } + check(array_keys($rfPhpObservations) === array_keys($rfNativeObservations), "ClassReflection parity ($rfLabel): the same observations on both sides"); + foreach ($rfSubclassesMemo['native'][$rfLabel] ?? [] as $cacheKey => $isSubclass) { + check(($rfSubclassesMemo['php'][$rfLabel][$cacheKey] ?? '') === $isSubclass, sprintf('ClassReflection parity (%s) subclasses memo[%s]: %s vs %s', $rfLabel, $cacheKey, json_encode($rfSubclassesMemo['php'][$rfLabel][$cacheKey] ?? ''), json_encode($isSubclass))); + } +} +$rfObservationCount = array_sum(array_map('count', $rfObservations['php'])); +check($rfObservationCount > 5000, "reflection-family: enough observations ($rfObservationCount over $rfSampleCount samples)"); + +if (isset($reflectionFamilyStandalone)) { + echo $failures === 0 ? "ALL OK ($rfObservationCount observations over $rfSampleCount samples)\n" : "$failures FAILURES\n"; + exit($failures === 0 ? 0 : 1); +} + +} diff --git a/turbo-ext/tests/scope-family-fixture.php b/turbo-ext/tests/scope-family-fixture.php new file mode 100644 index 00000000000..dd1bf784f54 --- /dev/null +++ b/turbo-ext/tests/scope-family-fixture.php @@ -0,0 +1,175 @@ + 4) { + $maybe = 'yes'; + } + + return $doubled; + } + +} + +final class Holder +{ + + use HelperTrait; + + public int $counter = 0; + + public function __construct( + public readonly string $name, + private readonly ?Holder $inner = null, + ) + { + $this->counter = 1; + if (class_exists('Nope\\Missing') && function_exists('nope_missing')) { + $this->counter = 2; + } + if (defined('ScopeFamilyFixture\\ANSWER')) { + $this->counter = ANSWER; + } + } + + public function read(): string + { + if ($this->inner !== null && $this->inner->name !== '') { + return $this->inner->name . $this->name; + } + + return $this->name; + } + + public function stat(string $path): void + { + if (is_file($path) && \file_exists($path)) { + clearstatcache(); + } + $level = ob_get_level(); + $error = \openssl_error_string(); + if ($error !== false && $level > 0) { + echo $error; + } + if (isset($_GET['x']) && $_SERVER['REQUEST_METHOD'] === 'GET') { + echo 'get'; + } + } + +} + +final class Custom implements Serializable +{ + + public function __construct(public readonly int $value) + { + if ($this->value > 1) { + $big = true; + } + } + + public function serialize(): ?string + { + return null; + } + + public function unserialize(string $data): void + { + } + +} + +/** + * @param list $items + */ +function walk(array $items, ?string $prefix = null): string +{ + $out = ''; + foreach ($items as $i => $item) { + $out .= $prefix . $item; + } + + $mapped = array_map(static fn (int $item): int => $item + 1, $items); + $closure = function (int $y) use ($out): string { + $z = $y + 1; + return $out . $z; + }; + + if (rand() > 3) { + $sometimes = 'x'; + } + + extract(['dynamic' => 1]); + + return $out . $closure(count($mapped)) . ($sometimes ?? '') . PHP_EOL; +} + +$global = walk([1, 2]); +if (rand() > 5) { + $maybeGlobal = new Holder('a'); +} +echo $global; + +abstract class Base +{ + + public const PUBLIC_CONST = 1; + protected const PROTECTED_CONST = 2; + private const PRIVATE_CONST = 3; + + public private(set) string $tag = ''; + + protected int $protectedCounter = 0; + + private int $privateCounter = 0; + + protected function protectedMethod(): int + { + return $this->protectedCounter + self::PRIVATE_CONST; + } + + private function privateMethod(): int + { + return $this->privateCounter; + } + + public function publicMethod(): int + { + return $this->protectedMethod() + $this->privateMethod() + self::PUBLIC_CONST; + } + +} + +final class Child extends Base +{ + + protected const CHILD_PROTECTED_CONST = 5; + + protected int $childCounter = 0; + + protected function protectedMethod(): int + { + return parent::protectedMethod() + static::PROTECTED_CONST + $this->childCounter; + } + +} + +final class Sibling extends Base +{ + + public function siblingMethod(): int + { + return $this->protectedCounter + self::PUBLIC_CONST; + } + +} diff --git a/turbo-ext/tests/scope-family.php b/turbo-ext/tests/scope-family.php new file mode 100644 index 00000000000..0c084424ad6 --- /dev/null +++ b/turbo-ext/tests/scope-family.php @@ -0,0 +1,2219 @@ +scopeFactory->create(...) sites are compared by the exact + * argument list they produce rather than by the scope they get back. + * The native side's tables hold native ExpressionTypeHolders (the native + * bodies read them through their slots; the PHP twin's typed returns + * need PHP TrinaryLogic on its side), normalized before comparison. + * + * The three union-filtering member lookups a ported body dispatches + * through $this over the walk's own PHP types (getMethodReflection() and + * the two property lookups) are routed to the original walk scope by the + * NativeScope test subclass on both sides — see the barrier below; their + * native bodies are probed directly, with types of the side under test. + * + * The prefix is a type barrier: the engine collaborators the type + * resolution core hands the walk scope to (NodeScopeResolver::processExprOnDemand(), + * ExpressionResult::getTypeOnScope(), ClosureTypeResolver::getClosureType()) + * are typed with the real class name, which the prefixed native class is + * not. Both sides therefore override the (non-final, dispatched) + * toWalkScope() to answer the original walk scope — the native side must, + * and the PHP side does the same so the two walks stay symmetric (the + * PhpScope subclass); the native bodies are still observed + * through their own toWalkScope() dispatch. A body that passes $this + * itself (TemplateArgumentFrame::returnTypeOfCall() from + * resolveScopeStateType(), `new ExpressionResultStorage()` for the + * on-demand walk with no analysis in progress) cannot cross the barrier + * under the prefix, and neither can a PHP twin of a shadowed class flowing + * into a native body that requires the native class (ClassReflection's + * getObjectType() answering the native ObjectType's ancestor lookup with + * the PHP ObjectType): such an observation is recorded as a barrier hit + * (Harness::BARRIER) and skipped, counted in the summary. + * + * Included by smoke.php (uses its check()); runnable alone too. + */ + +namespace { + +if (!function_exists('check')) { + require __DIR__ . '/activate-prefixed.php'; + $failures = 0; + function check(bool $cond, string $msg): void + { + global $failures; + if (!$cond) { + $failures++; + echo "FAIL: $msg\n"; + } + } + $scopeFamilyStandalone = true; +} + +} + +namespace ScopeFamily { + +use PhpParser\Node; +use PHPStan\Analyser\ConditionalExpressionHolder; +use PHPStan\Analyser\ExpressionTypeHolder; +use PHPStan\Analyser\InternalScopeFactory; +use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\Scope; +use PHPStan\Analyser\ScopeContext; +use PHPStan\Type\Type; +use PHPStan\Type\VerbosityLevel; + +/** + * An InternalScopeFactory that records every create() argument list and + * hands back a fixed result; the parameter types are widened so a native + * ClosureType or a native scope passes where the twin's interface names + * the PHP classes. + */ +final class RecordingScopeFactory implements InternalScopeFactory +{ + + /** @var list> */ + public array $calls = []; + + public ?self $nodeCallbackScopeFactory = null; + + /** + * When set, create() builds a real scope of the side under test out of its + * arguments instead of answering with the canned $result: most bodies + * chain ($scope = $this->a()->b()), and a canned result cannot + * model a chain — nor the twin's ScopeOps::scopeWith(), which reaches the + * factory through duplicateWith() while the native one clones. + * + * @var (callable(array): MutatingScope)|null + */ + public $builder = null; + + public function __construct(public MutatingScope $result) + { + } + + public function create( + ScopeContext $context, + bool $declareStrictTypes = false, + $function = null, + ?string $namespace = null, + array $expressionTypes = [], + array $nativeExpressionTypes = [], + array $conditionalExpressions = [], + array $inClosureBindScopeClasses = [], + $anonymousFunctionReflection = null, + bool $inFirstLevelStatement = true, + array $currentlyAssignedExpressions = [], + array $currentlyAllowedUndefinedExpressions = [], + array $inFunctionCallsStack = [], + bool $afterExtractCall = false, + $parentScope = null, + bool $nativeTypesPromoted = false, + $templateArgumentFrame = null, + $templateArgumentConstraints = null, + ): MutatingScope + { + $this->calls[] = func_get_args(); + if ($this->builder === null) { + return $this->result; + } + + return ($this->builder)([ + $context, $declareStrictTypes, $function, $namespace, $expressionTypes, $nativeExpressionTypes, + $conditionalExpressions, $inClosureBindScopeClasses, $anonymousFunctionReflection, $inFirstLevelStatement, + $currentlyAssignedExpressions, $currentlyAllowedUndefinedExpressions, $inFunctionCallsStack, + $afterExtractCall, $parentScope, $nativeTypesPromoted, $templateArgumentFrame, $templateArgumentConstraints, + ]); + } + + public function toNodeCallbackScopeFactory(): InternalScopeFactory + { + return $this->nodeCallbackScopeFactory ??= new self($this->result); + } + + public function toWalkScopeFactory(): InternalScopeFactory + { + return $this; + } + +} + +/** + * The native class under test, subclassed for the harness: it answers + * toWalkScope() with the original walk scope, which the PHP collaborators + * typed with the twin's class name accept, and routes the three + * union-filtering member lookups there too (see the file comment). The + * class itself carries the twin's interfaces. + */ +function declareNativeScope(): void +{ + eval( + "namespace ScopeFamily; final class NativeScope extends \\PHPStanTurbo\\MutatingScope {\n" + . "\tpublic ?\\PHPStan\\Analyser\\MutatingScope \$twin = null;\n" + . "\t/** the walk scope the engine collaborators accept (see the file comment); the native method declares the twin's return type */\n" + . "\tpublic function toWalkScope(): \\PHPStan\\Analyser\\MutatingScope { return \$this->twin; }\n" + . "\tpublic function parentToWalkScope(): \\PHPStanTurbo\\MutatingScope { return parent::toWalkScope(); }\n" + // The three member lookups resolveScopeStateType() dispatches by name + // over the walk's own (PHP) types: their union filter tests + // `instanceof UnionType` against the native class, which a PHP + // UnionType is not under the prefix, so a `Holder|null` property read + // would answer with no reflection where the twin filters the null + // away. Both sides therefore keep taking these through the original + // walk scope; the native bodies are probed directly through the + // native() accessors below, with types of the side under test. + . "\tpublic function getMethodReflection(\\PHPStan\\Type\\Type \$typeWithMethod, string \$methodName): ?\\PHPStan\\Reflection\\ExtendedMethodReflection { return \$this->twin->getMethodReflection(\$typeWithMethod, \$methodName); }\n" + . "\tpublic function getInstancePropertyReflection(\\PHPStan\\Type\\Type \$typeWithProperty, string \$propertyName): ?\\PHPStan\\Reflection\\ExtendedPropertyReflection { return \$this->twin->getInstancePropertyReflection(\$typeWithProperty, \$propertyName); }\n" + . "\tpublic function getStaticPropertyReflection(\\PHPStan\\Type\\Type \$typeWithProperty, string \$propertyName): ?\\PHPStan\\Reflection\\ExtendedPropertyReflection { return \$this->twin->getStaticPropertyReflection(\$typeWithProperty, \$propertyName); }\n" + . "\tpublic function nativeGetMethodReflection(\\PHPStan\\Type\\Type \$typeWithMethod, string \$methodName): ?\\PHPStan\\Reflection\\ExtendedMethodReflection { return parent::getMethodReflection(\$typeWithMethod, \$methodName); }\n" + . "\tpublic function nativeGetInstancePropertyReflection(\\PHPStan\\Type\\Type \$typeWithProperty, string \$propertyName): ?\\PHPStan\\Reflection\\ExtendedPropertyReflection { return parent::getInstancePropertyReflection(\$typeWithProperty, \$propertyName); }\n" + . "\tpublic function nativeGetStaticPropertyReflection(\\PHPStan\\Type\\Type \$typeWithProperty, string \$propertyName): ?\\PHPStan\\Reflection\\ExtendedPropertyReflection { return parent::getStaticPropertyReflection(\$typeWithProperty, \$propertyName); }\n" + . "}" + ); +} + +declareNativeScope(); + +/** The PHP side's counterpart: the same walk-scope delegation over the twin. */ +final class PhpScope extends MutatingScope +{ + + public ?MutatingScope $inner = null; + + public function toWalkScope(): MutatingScope + { + return $this->inner ?? $this; + } + + public function parentToWalkScope(): MutatingScope + { + return parent::toWalkScope(); + } + + /** The PHP counterparts of NativeScope's native() accessors. */ + public function nativeGetMethodReflection(Type $typeWithMethod, string $methodName): ?\PHPStan\Reflection\ExtendedMethodReflection + { + return parent::getMethodReflection($typeWithMethod, $methodName); + } + + public function nativeGetInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?\PHPStan\Reflection\ExtendedPropertyReflection + { + return parent::getInstancePropertyReflection($typeWithProperty, $propertyName); + } + + public function nativeGetStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?\PHPStan\Reflection\ExtendedPropertyReflection + { + return parent::getStaticPropertyReflection($typeWithProperty, $propertyName); + } + + /** + * The one private method of the twin the prefixed native bodies reach by + * name (specifyExpressionType() opens its working copy through the + * factory, which cannot answer with the prefixed class): they hand over + * the native TrinaryLogic singleton, which the twin's typed parameter + * rejects. A method-table lookup finds this declaration over the + * inherited one; the twin's body runs through a closure bound to its own + * scope. Both sides go through it, so nothing is one-sided. + */ + private function specifyExpressionTypeInPlace(Node\Expr $expr, Type $type, Type $nativeType, object $certainty): void + { + if ($certainty instanceof \PHPStanTurbo\TrinaryLogic) { + $certainty = self::phpTrinary($certainty); + } + \Closure::bind(function () use ($expr, $type, $nativeType, $certainty): void { + $this->specifyExpressionTypeInPlace($expr, $type, $nativeType, $certainty); + }, $this, MutatingScope::class)(); + } + + /** + * The two public entries the prefixed native bodies reach with an argument + * of the prefixed class: the TrinaryLogic they build themselves and the + * scope they were handed. Parameter contravariance makes the widening + * legal; both sides go through these, so nothing is one-sided. + * + * @param list $intertwinedPropagatedFrom + */ + public function assignVariable(string $variableName, Type $type, Type $nativeType, object $certainty, array $intertwinedPropagatedFrom = []): MutatingScope + { + return parent::assignVariable($variableName, $type, $nativeType, $certainty instanceof \PHPStanTurbo\TrinaryLogic ? self::phpTrinary($certainty) : $certainty, $intertwinedPropagatedFrom); + } + + public function enterForeachKey(object $originalScope, Node\Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $keyName): MutatingScope + { + return parent::enterForeachKey($originalScope instanceof MutatingScope ? $originalScope : $this, $iteratee, $iterateeType, $nativeIterateeType, $keyName); + } + + private static function phpTrinary(\PHPStanTurbo\TrinaryLogic $certainty): \PHPStan\TrinaryLogic + { + return $certainty->yes() + ? \PHPStan\TrinaryLogic::createYes() + : ($certainty->maybe() ? \PHPStan\TrinaryLogic::createMaybe() : \PHPStan\TrinaryLogic::createNo()); + } + +} + +/** + * A deferred SpecifiedTypes augment for applySpecifiedTypes(): the interface + * types evaluate() with the twin's class, which the prefixed native scope is + * not — a widened parameter type (contravariance) lets both sides through. + */ +final class TestAugment implements \PHPStan\Analyser\DeferredSpecifiedTypesAugment +{ + + public function __construct(private ?\PHPStan\Analyser\SpecifiedTypes $result) + { + } + + public function evaluate(object $scope): ?\PHPStan\Analyser\SpecifiedTypes + { + return $this->result; + } + +} + +/** + * A conditional-expression holder recipe: applySpecifiedTypes() calls + * evaluate() by name, and the twin's ConditionalExpressionHolderRecipe is + * final with a MutatingScope-typed parameter the prefixed scope cannot cross. + */ +final class TestRecipe +{ + + /** @param array> $result */ + public function __construct(private array $result) + { + } + + /** @return array> */ + public function evaluate(object $scope): array + { + return $this->result; + } + +} + +final class Harness +{ + + /** an observation the prefix's type barrier cut short (see the file comment) */ + public const BARRIER = ['L', 'prefix type barrier']; + + /** @var array */ + private array $classNorm; + + /** + * The expressions both sides share (the walk's own nodes, held by the + * rebuilt tables): a holder over one of them is compared by its + * identity, a holder over an expression a body just built by that + * expression's class and printed key. + * + * @var array + */ + public array $sharedExprIds = []; + + /** The scope under test on this side: `$this` in a normalized value. */ + public ?object $currentScope = null; + + private \PHPStan\Node\Printer\ExprPrinter $exprPrinter; + + /** @param array $manifest */ + public function __construct(array $manifest, \PHPStan\DependencyInjection\Container $container) + { + $this->classNorm = []; + foreach ($manifest as $shadowedClass => $entry) { + $this->classNorm[$entry['turboClass']] = $shadowedClass; + } + $this->classNorm[\PHPStanTurbo\MutatingScope::class] = MutatingScope::class; + $this->classNorm[NativeScope::class] = MutatingScope::class; + $this->classNorm[PhpScope::class] = MutatingScope::class; + $this->exprPrinter = $container->getByType(\PHPStan\Node\Printer\ExprPrinter::class); + } + + public function className(object $object): string + { + return strtr(get_class($object), $this->classNorm); + } + + /** The 33 constructor arguments of a scope, by parameter name. */ + public function constructorArgs(MutatingScope $scope): array + { + $args = []; + $reflection = new \ReflectionClass(MutatingScope::class); + foreach ($reflection->getMethod('__construct')->getParameters() as $parameter) { + $property = $reflection->getProperty($parameter->getName()); + $args[$parameter->getName()] = $property->getValue($scope); + } + + return $args; + } + + /** @param array $table */ + public function nativeHolders(array $table): array + { + $result = []; + foreach ($table as $key => $holder) { + $certainty = $holder->getCertainty(); + $result[$key] = new \PHPStanTurbo\ExpressionTypeHolder( + $holder->getExpr(), + $holder->getType(), + $certainty->yes() ? \PHPStanTurbo\TrinaryLogic::createYes() : ($certainty->maybe() ? \PHPStanTurbo\TrinaryLogic::createMaybe() : \PHPStanTurbo\TrinaryLogic::createNo()), + ); + } + + return $result; + } + + /** + * The native ConditionalExpressionHolders of a conditionalExpressions + * table: ScopeOps' native bodies read them through their slots, as they do + * the ExpressionTypeHolders. + * + * @param array> $table + */ + public function nativeConditionalExpressions(array $table): array + { + $result = []; + foreach ($table as $exprString => $holders) { + $converted = []; + foreach ($holders as $key => $holder) { + $converted[$key] = new \PHPStanTurbo\ConditionalExpressionHolder( + $this->nativeHolders($holder->getConditionExpressionTypeHolders()), + $this->nativeHolders(['x' => $holder->getTypeHolder()])['x'], + ); + } + $result[$exprString] = $converted; + } + + return $result; + } + + /** A comparable, side-independent rendering of any value. */ + public function norm(mixed $value, int $depth = 0): mixed + { + if (is_array($value)) { + $out = []; + foreach ($value as $k => $v) { + $out[$k] = $this->norm($v, $depth + 1); + } + return $out; + } + if (!is_object($value)) { + return $value; + } + if ($value instanceof ExpressionTypeHolder || $value instanceof \PHPStanTurbo\ExpressionTypeHolder) { + return ['H', $this->exprRef($value->getExpr()), $this->norm($value->getType()), $this->norm($value->getCertainty())]; + } + if ($value instanceof \PHPStan\TrinaryLogic || $value instanceof \PHPStanTurbo\TrinaryLogic) { + return ['T', $value->describe()]; + } + if ($value instanceof Type) { + return ['Y', $this->className($value), $value->describe(VerbosityLevel::precise())]; + } + if ($value instanceof ConditionalExpressionHolder || $value instanceof \PHPStanTurbo\ConditionalExpressionHolder) { + return ['C', $value->getKey()]; + } + if ($value instanceof MutatingScope || $value instanceof \PHPStanTurbo\MutatingScope) { + if ($value === $this->currentScope) { + return ['S', 'this']; + } + return ['S', $this->className($value), spl_object_id($value)]; + } + if ($value instanceof \PHPStan\Analyser\ExpressionResult) { + // an on-demand walk answers with a result over its own copy of the node + return ['R', $this->norm($value->getExpr()), $value->canResolveOwnType()]; + } + if ($value instanceof \PHPStan\Analyser\Generics\TemplateArgumentConstraints) { + // each side builds its own empty constraints + return ['TAC', $value->isEmpty()]; + } + if ($value instanceof \PHPStan\Php\PhpVersions) { + return ['PV', $this->norm($value->getType())]; + } + if ($value instanceof ScopeContext) { + // the enter* family builds a fresh one on each side + return ['SC', $value->getFile(), $value->getClassReflection()?->getName(), $value->getTraitReflection()?->getName()]; + } + if ($value instanceof \PHPStan\Analyser\SpecifiedTypes) { + return ['ST', $this->norm($value->getSureTypes()), $this->norm($value->getSureNotTypes()), $value->shouldOverwrite(), $this->norm($value->getRootExpr())]; + } + if ($value instanceof Node\Expr) { + return ['X', $this->className($value), $this->key($value)]; + } + if ($value instanceof \Throwable && getenv('SF_BARRIER_TRACE') !== false) { + echo 'THROW: ', get_class($value), ': ', $value->getMessage(), "\n"; + } + if ($value instanceof \TypeError && ( + preg_match('~must be of type \??PHPStan\\\\[A-Za-z\\\\]+, (PHPStanTurbo\\\\|ScopeFamily\\\\NativeScope)~', $value->getMessage()) === 1 + || preg_match('~^phpstan_turbo: .*\\(\\) must return PHPStanTurbo\\\\~', $value->getMessage()) === 1 + )) { + return self::BARRIER; + } + if ($value instanceof \PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection) { + // the enter* family builds a fresh one on each side + return [ + 'FR', + $this->className($value), + $value->getName(), + $this->norm($value->getParameters()), + $value->isVariadic(), + $this->norm($value->getReturnType()), + $this->norm($value->getPhpDocReturnType()), + $this->norm($value->getNativeReturnType()), + $this->norm($value->getThrowType()), + $value->isDeprecated()->describe(), + $value->getDeprecatedDescription(), + $value->isInternal()->describe(), + $this->norm($value->isPure()), + $value->acceptsNamedArguments()->describe(), + $this->norm($value->getAttributes()), + $value->getDocComment(), + ]; + } + if ($value instanceof \PHPStan\Reflection\AttributeReflection) { + return ['A', $value->getName(), $this->norm($value->getArgumentTypes())]; + } + if ($value instanceof \PHPStan\Reflection\ParameterReflection) { + // each side builds its own parameter reflections for the call stack + return [ + 'P', + $value->getName(), + $this->norm($value->getType()), + $value->isOptional(), + $value->isVariadic(), + $value->passedByReference()->createsNewVariable(), + $this->norm($value->getDefaultValue()), + $value instanceof \PHPStan\Reflection\ExtendedParameterReflection ? $this->norm($value->getNativeType()) : null, + $value instanceof \PHPStan\Reflection\ExtendedParameterReflection ? $this->norm($value->getAttributes()) : null, + ]; + } + if ($value instanceof \Throwable) { + return ['E', $this->className($value), strtr($value->getMessage(), $this->classNorm)]; + } + + return ['O', $this->className($value), spl_object_id($value)]; + } + + public function key(Node\Expr $expr): string + { + return $this->exprPrinter->printExpr($expr); + } + + /** An expression shared by both sides by its identity, one a body built by its key. */ + public function exprRef(Node\Expr $expr): mixed + { + $id = spl_object_id($expr); + + return isset($this->sharedExprIds[$id]) ? $id : ['fresh', $this->className($expr), $this->key($expr)]; + } + +} + +} + +namespace { + +use ScopeFamily\Harness; +use ScopeFamily\NativeScope; +use ScopeFamily\PhpScope; +use ScopeFamily\RecordingScopeFactory; + +$sfManifest = json_decode(file_get_contents(dirname(__DIR__, 2) . '/vendor/turbo-shadowed-classes.json'), true, 8, JSON_THROW_ON_ERROR); + +// a container of its own: the fixture must be an analysed path for the +// reflection provider to find its classes +$sfFile = __DIR__ . '/scope-family-fixture.php'; +$sfContainerFactory = new \PHPStan\DependencyInjection\ContainerFactory(dirname(__DIR__, 2)); +$scContainer = $sfContainerFactory->create(sys_get_temp_dir() . '/phpstan-turbo-smoke-scope', [$sfContainerFactory->getConfigDirectory() . '/config.level8.neon', ...(PHP_VERSION_ID < 80400 ? [__DIR__ . '/php84-syntax.neon'] : [])], [$sfFile]); +$sfHarness = new Harness($sfManifest, $scContainer); + +// ---- collect the walk scopes of a real analysis of the fixture ---- +$sfResolver = $scContainer->getByType(\PHPStan\Analyser\NodeScopeResolver::class); +$sfResolver->setAnalysedFiles([$sfFile]); +$sfResolver->resetPerFileAnalysisState(); +$sfScopeFactory = $scContainer->getByType(\PHPStan\Analyser\ScopeFactory::class); +/** @var array, \PHPStan\Analyser\ExpressionResultStorage|null}> $sfScopes */ +$sfScopes = []; +$sfCallback = static function (\PhpParser\Node $node, \PHPStan\Analyser\Scope $scope) use (&$sfScopes): void { + $walkScope = $scope->toWalkScope(); + $id = spl_object_id($walkScope); + // the storage of the analysis in progress here, re-pushed when the + // scope's type answers are observed after the walk + $sfScopes[$id] ??= [$walkScope, [], $walkScope->getCurrentExpressionResultStorage()]; + if ($node instanceof \PhpParser\Node\Expr && count($sfScopes[$id][1]) < 6) { + $sfScopes[$id][1][] = $node; + } +}; +$sfResolver->processNodes( + $scContainer->getService('defaultAnalysisParser')->parseFile($sfFile), + $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile), $sfCallback), + $sfCallback, +); +check(count($sfScopes) >= 30, 'scope-family: the fixture walk produced enough distinct scopes (' . count($sfScopes) . ')'); + +// one scope in a trait context, entered by hand (the walk enters traits +// through the using class) +$sfReflectionProvider = $scContainer->getByType(\PHPStan\Reflection\ReflectionProvider::class); +$sfTraitContext = \PHPStan\Analyser\ScopeContext::create($sfFile) + ->enterClass($sfReflectionProvider->getClass(\ScopeFamilyFixture\Holder::class)) + ->enterTrait($sfReflectionProvider->getClass(\ScopeFamilyFixture\HelperTrait::class)); +$sfTraitScope = $sfScopeFactory->create($sfTraitContext)->enterNamespace('ScopeFamilyFixture')->assignVariable('t', new \PHPStan\Type\IntegerType(), new \PHPStan\Type\IntegerType(), \PHPStan\TrinaryLogic::createYes()); +$sfScopes[spl_object_id($sfTraitScope)] = [$sfTraitScope, [new \PhpParser\Node\Expr\Variable('t')], null]; +// a class with custom serialization, for rememberConstructorScope() +$sfCustomScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)->enterClass($sfReflectionProvider->getClass(\ScopeFamilyFixture\Custom::class))) + ->enterNamespace('ScopeFamilyFixture') + ->assignVariable('this', new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Custom::class), new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Custom::class), \PHPStan\TrinaryLogic::createYes()) + ->assignExpression(new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'value'), new \PHPStan\Type\Constant\ConstantIntegerType(5), new \PHPStan\Type\IntegerType()) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('class_exists'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('X'))]), new \PHPStan\Type\Constant\ConstantBooleanType(true), new \PHPStan\Type\BooleanType()) + ->assignExpression(new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('ANSWER')), new \PHPStan\Type\Constant\ConstantIntegerType(42), new \PHPStan\Type\IntegerType()); +$sfScopes[spl_object_id($sfCustomScope)] = [$sfCustomScope, [new \PhpParser\Node\Expr\Variable('this')], null]; +// tracked qualified and unqualified function calls, for the +// afterClearstatcacheCall() / afterOpenSslCall() key matching +$sfTrue = new \PHPStan\Type\Constant\ConstantBooleanType(true); +$sfBool = new \PHPStan\Type\BooleanType(); +$sfCallsScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name\FullyQualified('file_exists'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('p'))]), $sfTrue, $sfBool) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('is_dir'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('p'))]), $sfTrue, $sfBool) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('is_writeable_not'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('p'))]), $sfTrue, $sfBool) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name\FullyQualified('openssl_error_string')), new \PHPStan\Type\StringType(), new \PHPStan\Type\StringType()) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name\FullyQualified('class_exists'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('Nope'))]), new \PHPStan\Type\Constant\ConstantBooleanType(false), $sfBool) + // expressionTypeIsUnchangeable(): a qualified existence check that holds, + // over a constant string, with no variable inside + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name\FullyQualified('class_exists'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('ScopeFamilyFixture\\Holder'))]), $sfTrue, $sfBool) + ->assignExpression(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name\FullyQualified('interface_exists'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('p'))]), $sfTrue, $sfBool); +$sfScopes[spl_object_id($sfCallsScope)] = [$sfCallsScope, [new \PhpParser\Node\Expr\Variable('p')], null]; +// two scopes tracking PHP_VERSION_ID: getPhpVersion() then answers with the +// walk's own (PHP) type on both sides, which is what makes the variadic +// parameter shapes of getFunctionType() comparable under the prefix — over a +// native fallback type (a native ConstantIntegerType / IntegerRangeType) the +// PHP IntegerRangeType::isSuperTypeOf() inside PhpVersions answers "no" +// whatever the version is +// a scope inside a class_exists() call: isInClassExists()'s stack scan +$sfClassExistsScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)) + ->pushInFunctionCall($sfReflectionProvider->getFunction(new \PhpParser\Node\Name('class_exists'), null), null, false); +$sfScopes[spl_object_id($sfClassExistsScope)] = [$sfClassExistsScope, [new \PhpParser\Node\Expr\Variable('x')], null]; +// PHP_VERSION_ID as the overall analysable range: getPhpVersion() ignores it +// and falls back to the configured version. The range is each side's own +// class — the native body's `instanceof IntegerRangeType` is the native class +// under the prefix, where a PHP twin range would not be recognized +$sfOverallVersionFetch = new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_VERSION_ID')); +$sfOverallVersionScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)) + ->assignExpression($sfOverallVersionFetch, \PHPStan\Type\IntegerRangeType::fromInterval(\PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, null), new \PHPStan\Type\IntegerType()); +$sfScopes[spl_object_id($sfOverallVersionScope)] = [$sfOverallVersionScope, [$sfOverallVersionFetch], null]; +$sfOverallVersionTables = static function (string $side) use ($sfHarness, $sfOverallVersionFetch): array { + $native = $side === 'native'; + $range = $native + ? \PHPStanTurbo\IntegerRangeType::fromInterval(\PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, null) + : \PHPStan\Type\IntegerRangeType::fromInterval(\PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, null); + $holder = $native + ? new \PHPStanTurbo\ExpressionTypeHolder($sfOverallVersionFetch, $range, \PHPStanTurbo\TrinaryLogic::createYes()) + : new \PHPStan\Analyser\ExpressionTypeHolder($sfOverallVersionFetch, $range, \PHPStan\TrinaryLogic::createYes()); + $table = [$sfHarness->key($sfOverallVersionFetch) => $holder]; + + return [$table, $table, []]; +}; +foreach ([80500, 70400] as $sfVersionId) { + $sfVersionScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)->enterClass($sfReflectionProvider->getClass(\ScopeFamilyFixture\Holder::class))) + ->assignExpression(new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_VERSION_ID')), new \PHPStan\Type\Constant\ConstantIntegerType($sfVersionId), new \PHPStan\Type\IntegerType()); + $sfScopes[spl_object_id($sfVersionScope)] = [$sfVersionScope, [new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_VERSION_ID'))], null]; +} +// a scope whose tables carry types built on each side's own classes: under +// the prefix a PHP twin Type's describe() rejects the native VerbosityLevel +// singleton, so getClosureScopeCacheKey() over the walk's (PHP) types is a +// barrier hit — these tables are that method's real coverage (the VirtualNode +// skip, the root filter, the parameter stack) +$sfSyntheticScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)); +$sfScopes[spl_object_id($sfSyntheticScope)] = [$sfSyntheticScope, [new \PhpParser\Node\Expr\Variable('a'), new \PhpParser\Node\Expr\Variable('b')], null]; +// the expressions are shared by both sides (the holders are compared by +// their expression's identity), the types and holders are each side's own +$sfSyntheticExprs = [ + [new \PhpParser\Node\Expr\Variable('a'), \PHPStan\Type\IntegerType::class, [], true], + [new \PhpParser\Node\Expr\Variable('b'), \PHPStan\Type\Constant\ConstantStringType::class, ['x'], false], + [new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('a'), 'p'), \PHPStan\Type\ArrayType::class, [\PHPStan\Type\IntegerType::class, \PHPStan\Type\StringType::class], true], + [new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('b'), new \PhpParser\Node\Scalar\Int_(0)), \PHPStan\Type\Constant\ConstantIntegerType::class, [5], true], + // no ObjectType: its cache-level description embeds static::class, which the prefix renames + [new \PhpParser\Node\Expr\Variable('ab'), \PHPStan\Type\BooleanType::class, [], true], + [new \PHPStan\Node\Expr\PropertyInitializationExpr('p'), \PHPStan\Type\NullType::class, [], true], + // the ArrayDimFetch arm of specifyExpressionTypeInPlace() tests the dim type + // against ConstantIntegerType/ConstantStringType and the var type against + // MixedType — each side's own classes, so both must come from the tables + // (getScopeStateType() answers a tracked Variable from the holder) + [new \PhpParser\Node\Expr\Variable('arr'), \PHPStan\Type\ArrayType::class, [\PHPStan\Type\IntegerType::class, \PHPStan\Type\StringType::class], true], + [new \PhpParser\Node\Expr\Variable('i'), \PHPStan\Type\Constant\ConstantIntegerType::class, [0], true], + [new \PhpParser\Node\Expr\Variable('k'), \PHPStan\Type\Constant\ConstantStringType::class, ['k'], true], + [new \PhpParser\Node\Expr\Variable('m'), \PHPStan\Type\MixedType::class, [], true], + // a tracked static expression (invalidateStaticExpressions() drops it) and a + // tracked method call on $a (invalidateMethodsOnExpression() drops that) + [new \PhpParser\Node\Expr\StaticPropertyFetch(new \PhpParser\Node\Name('Holder'), 'shared'), \PHPStan\Type\IntegerType::class, [], true], + [new \PhpParser\Node\Expr\MethodCall(new \PhpParser\Node\Expr\Variable('a'), 'm'), \PHPStan\Type\StringType::class, [], true], +]; +$sfNativeOnlyExpr = new \PhpParser\Node\Expr\Variable('nativeOnly'); +$sfSyntheticTables = static function (string $side) use ($sfHarness, $sfSyntheticExprs, $sfNativeOnlyExpr): array { + $native = $side === 'native'; + $type = static function (string $phpClass, mixed ...$ctorArgs) use ($native): \PHPStan\Type\Type { + $class = $native ? 'PHPStanTurbo\\' . substr($phpClass, strrpos($phpClass, '\\') + 1) : $phpClass; + return new $class(...$ctorArgs); + }; + $holder = static fn (\PhpParser\Node\Expr $expr, \PHPStan\Type\Type $t, bool $yes = true): object => $native + ? new \PHPStanTurbo\ExpressionTypeHolder($expr, $t, $yes ? \PHPStanTurbo\TrinaryLogic::createYes() : \PHPStanTurbo\TrinaryLogic::createMaybe()) + : new \PHPStan\Analyser\ExpressionTypeHolder($expr, $t, $yes ? \PHPStan\TrinaryLogic::createYes() : \PHPStan\TrinaryLogic::createMaybe()); + $string = $type(\PHPStan\Type\StringType::class); + $tables = []; + foreach ($sfSyntheticExprs as [$expr, $typeClass, $ctorArgs, $yes]) { + // a class-string argument is a nested type + $ctorArgs = array_map(static fn (mixed $arg): mixed => is_string($arg) && class_exists($arg) ? $type($arg) : $arg, $ctorArgs); + $tables[$sfHarness->key($expr)] = $holder($expr, $type($typeClass, ...$ctorArgs), $yes); + } + // one entry whose native flavour is wider than its phpdoc one: the readers + // that pick a flavour (getStateType() on a native-promoted scope, + // addTypeToExpression()) are indistinguishable over two equal tables + $nativeTables = $tables; + $nativeTables[$sfHarness->key($sfSyntheticExprs[1][0])] = $holder($sfSyntheticExprs[1][0], $string, false); + // the same expression node on both sides (a holder is compared by its + // expression's identity), only the type differs + $arrExpr = $sfSyntheticExprs[6][0]; + $nativeTables[$sfHarness->key($arrExpr)] = $holder($arrExpr, $type(\PHPStan\Type\ArrayType::class, $type(\PHPStan\Type\IntegerType::class), $type(\PHPStan\Type\IntegerType::class))); + // an entry the native table tracks and the phpdoc one does not: the + // current-type fallback must not overwrite a tracked native flavour + $nativeTables[$sfHarness->key($sfNativeOnlyExpr)] = $holder($sfNativeOnlyExpr, $string); + $parameter = $native + ? new \PHPStanTurbo\NativeParameterReflection('p', false, $string, \PHPStan\Reflection\PassedByReference::createNo(), false, null) + : new \PHPStan\Reflection\Native\NativeParameterReflection('p', false, $string, \PHPStan\Reflection\PassedByReference::createNo(), false, null); + + return [$tables, $nativeTables, [[null, null], [null, $parameter]]]; +}; + +// a scope bound to closure scope classes, followed by a plain one: the +// second one's $other is this one, which is what +// restoreOriginalScopeAfterClosureBind() / restoreThis() read +$sfBindScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)->enterClass($sfReflectionProvider->getClass(\ScopeFamilyFixture\Holder::class))) + ->enterNamespace('ScopeFamilyFixture') + ->assignVariable('this', new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), \PHPStan\TrinaryLogic::createYes()) + ->assignExpression(new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'name'), new \PHPStan\Type\Constant\ConstantStringType('n'), new \PHPStan\Type\StringType()) + ->assignVariable('other', new \PHPStan\Type\IntegerType(), new \PHPStan\Type\IntegerType(), \PHPStan\TrinaryLogic::createYes()) + ->withClosureBindScopeClasses(['ScopeFamilyFixture\\Holder', 'Other']); +$sfScopes[spl_object_id($sfBindScope)] = [$sfBindScope, [new \PhpParser\Node\Expr\Variable('this')], null]; +$sfAfterBindScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)) + ->assignVariable('x', new \PHPStan\Type\IntegerType(), new \PHPStan\Type\IntegerType(), \PHPStan\TrinaryLogic::createYes()); +$sfScopes[spl_object_id($sfAfterBindScope)] = [$sfAfterBindScope, [new \PhpParser\Node\Expr\Variable('x')], null]; + +// a scope in the middle of an assignment: the currently-* tables the +// expression-assign family reads (an empty one cannot tell exitExpressionAssign() +// or isInWriteExpressionAssign() apart from a no-op) +$sfAssignedPropertyFetch = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'name'); +$sfAssignedDimFetch = new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('items'), new \PhpParser\Node\Scalar\Int_(0)); +$sfAssignScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)->enterClass($sfReflectionProvider->getClass(\ScopeFamilyFixture\Holder::class))) + ->enterNamespace('ScopeFamilyFixture') + ->assignVariable('this', new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('items', new \PHPStan\Type\ArrayType(new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()), new \PHPStan\Type\ArrayType(new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()), \PHPStan\TrinaryLogic::createYes()) + ->enterExpressionAssign($sfAssignedPropertyFetch) + ->enterExpressionAssign($sfAssignedDimFetch, false) + ->setAllowedUndefinedExpression($sfAssignedDimFetch); +$sfScopes[spl_object_id($sfAssignScope)] = [$sfAssignScope, [$sfAssignedPropertyFetch, $sfAssignedDimFetch], null]; + +// a scope carrying conditional expressions keyed on names the probe closure +// uses, on one it does not, and one whose condition is not a use: the three +// arms of enterAnonymousFunctionWithoutReflection()'s conditional filter +$sfCondHolder = static fn (string $conditionKey, string $conditionName, string $targetName): \PHPStan\Analyser\ConditionalExpressionHolder => new \PHPStan\Analyser\ConditionalExpressionHolder( + [$conditionKey => \PHPStan\Analyser\ExpressionTypeHolder::createYes(new \PhpParser\Node\Expr\Variable($conditionName), new \PHPStan\Type\IntegerType())], + \PHPStan\Analyser\ExpressionTypeHolder::createYes(new \PhpParser\Node\Expr\Variable($targetName), new \PHPStan\Type\StringType()), +); +$sfConditionalScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)) + ->assignVariable('p', new \PHPStan\Type\IntegerType(), new \PHPStan\Type\IntegerType(), \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('byRefUse', new \PHPStan\Type\IntegerType(), new \PHPStan\Type\IntegerType(), \PHPStan\TrinaryLogic::createYes()) + ->addConditionalExpressions('$p', [$sfCondHolder('$byRefUse', 'byRefUse', 'p'), $sfCondHolder('$notAUse', 'notAUse', 'p')]) + ->addConditionalExpressions('$notAUse', [$sfCondHolder('$p', 'p', 'notAUse')]); +$sfScopes[spl_object_id($sfConditionalScope)] = [$sfConditionalScope, [new \PhpParser\Node\Expr\Variable('p')], null]; + +// the scopes whose tables are built per side (each side's own Type classes) +$sfScopes[spl_object_id($sfSyntheticScope)][3] = $sfSyntheticTables; +$sfScopes[spl_object_id($sfOverallVersionScope)][3] = $sfOverallVersionTables; +$sfPromotedArgs = $sfHarness->constructorArgs($sfSyntheticScope); +$sfPromotedArgs['nativeTypesPromoted'] = true; +$sfPromotedScope = new \PHPStan\Analyser\MutatingScope(...array_values($sfPromotedArgs)); +$sfScopes[spl_object_id($sfPromotedScope)] = [$sfPromotedScope, [new \PhpParser\Node\Expr\Variable('a'), new \PhpParser\Node\Expr\Variable('b')], null, $sfSyntheticTables]; + +// two adjacent scopes for the conditional bookkeeping and the merges: their +// tables AND their conditional expressions are built per side, so every type +// a native body combines is a native one (a foreign class is an atom to the +// native TypeCombinator). The second one's $other is the first. +$sfSideType = static function (string $side, string $phpClass, mixed ...$ctorArgs): \PHPStan\Type\Type { + $class = $side === 'native' ? 'PHPStanTurbo\\' . substr($phpClass, strrpos($phpClass, '\\') + 1) : $phpClass; + + return new $class(...$ctorArgs); +}; +$sfSideHolder = static function (string $side, \PhpParser\Node\Expr $expr, \PHPStan\Type\Type $type, string $certainty = 'yes'): object { + if ($side === 'native') { + return new \PHPStanTurbo\ExpressionTypeHolder($expr, $type, $certainty === 'yes' ? \PHPStanTurbo\TrinaryLogic::createYes() : ($certainty === 'maybe' ? \PHPStanTurbo\TrinaryLogic::createMaybe() : \PHPStanTurbo\TrinaryLogic::createNo())); + } + + return new \PHPStan\Analyser\ExpressionTypeHolder($expr, $type, $certainty === 'yes' ? \PHPStan\TrinaryLogic::createYes() : ($certainty === 'maybe' ? \PHPStan\TrinaryLogic::createMaybe() : \PHPStan\TrinaryLogic::createNo())); +}; +$sfSideConditional = static function (string $side, array $conditions, object $typeHolder): object { + return $side === 'native' + ? new \PHPStanTurbo\ConditionalExpressionHolder($conditions, $typeHolder) + : new \PHPStan\Analyser\ConditionalExpressionHolder($conditions, $typeHolder); +}; +/** @param list $holders */ +$sfByKey = static function (array $holders): array { + $result = []; + foreach ($holders as $holder) { + $result[$holder->getKey()] = $holder; + } + + return $result; +}; +$sfMergeG = new \PhpParser\Node\Expr\Variable('g'); +$sfMergeT2 = new \PhpParser\Node\Expr\Variable('t2'); +$sfMergeT3 = new \PhpParser\Node\Expr\Variable('t3'); +$sfMergeT4 = new \PhpParser\Node\Expr\Variable('t4'); +$sfMergeT5 = new \PhpParser\Node\Expr\Variable('t5'); +$sfMergeD = new \PhpParser\Node\Expr\Variable('d'); +// a class-constant fetch that resolves to its declared value: +// withoutPreciseClassConstantFetches() drops it from the differing keys, and +// the late-bound static:: one it keeps +$sfMergeConst = new \PhpParser\Node\Expr\ClassConstFetch(new \PhpParser\Node\Name('Holder'), new \PhpParser\Node\Identifier('SOME')); +$sfMergeStaticConst = new \PhpParser\Node\Expr\ClassConstFetch(new \PhpParser\Node\Name('static'), new \PhpParser\Node\Identifier('SOME')); +// Memcached::HAVE_JSON is a configured dynamic class constant, so it stays a differing key +$sfMergeDynamicConst = new \PhpParser\Node\Expr\ClassConstFetch(new \PhpParser\Node\Name\FullyQualified('Memcached'), new \PhpParser\Node\Identifier('HAVE_JSON')); +$sfMergeProp = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('t4'), 'name'); +$sfMergeInit = new \PHPStan\Node\Expr\PropertyInitializationExpr('merged'); +$sfMergeTablesA = static function (string $side) use ($sfHarness, $sfSideType, $sfSideHolder, $sfMergeG, $sfMergeT2, $sfMergeT3, $sfMergeT5, $sfMergeD, $sfMergeConst, $sfMergeStaticConst, $sfMergeDynamicConst, $sfMergeInit): array { + $table = [ + $sfHarness->key($sfMergeG) => $sfSideHolder($side, $sfMergeG, $sfSideType($side, \PHPStan\Type\IntegerType::class)), + // int against a string consequent: the intersection with the existing + // type is not the consequent type + $sfHarness->key($sfMergeT2) => $sfSideHolder($side, $sfMergeT2, $sfSideType($side, \PHPStan\Type\IntegerType::class), 'maybe'), + $sfHarness->key($sfMergeT3) => $sfSideHolder($side, $sfMergeT3, $sfSideType($side, \PHPStan\Type\StringType::class)), + $sfHarness->key($sfMergeT5) => $sfSideHolder($side, $sfMergeT5, $sfSideType($side, \PHPStan\Type\StringType::class)), + $sfHarness->key($sfMergeD) => $sfSideHolder($side, $sfMergeD, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 1)), + $sfHarness->key($sfMergeConst) => $sfSideHolder($side, $sfMergeConst, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 5)), + $sfHarness->key($sfMergeStaticConst) => $sfSideHolder($side, $sfMergeStaticConst, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 5)), + $sfHarness->key($sfMergeDynamicConst) => $sfSideHolder($side, $sfMergeDynamicConst, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 5)), + $sfHarness->key($sfMergeInit) => $sfSideHolder($side, $sfMergeInit, $sfSideType($side, \PHPStan\Type\NullType::class), 'maybe'), + ]; + + return [$table, $table, []]; +}; +$sfMergeConditionalsA = static function (string $side) use ($sfHarness, $sfSideType, $sfSideHolder, $sfSideConditional, $sfByKey, $sfMergeG, $sfMergeT2, $sfMergeT3, $sfMergeT4, $sfMergeT5, $sfMergeD): array { + $guard = [$sfHarness->key($sfMergeG) => $sfSideHolder($side, $sfMergeG, $sfSideType($side, \PHPStan\Type\IntegerType::class))]; + + return [ + // two matching holders of differing certainty: the batch intersects + // their types and takes their extreme identity + $sfHarness->key($sfMergeT2) => $sfByKey([ + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeT2, $sfSideType($side, \PHPStan\Type\StringType::class))), + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeT2, $sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, 'x'), 'maybe')), + ]), + // a No consequent: the target is dropped from the scope + $sfHarness->key($sfMergeT3) => $sfByKey([ + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeT3, $sfSideType($side, \PHPStan\Type\StringType::class), 'no')), + ]), + // a target the scope does not track: the consequent holder is taken as is + $sfHarness->key($sfMergeT4) => $sfByKey([ + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeT4, $sfSideType($side, \PHPStan\Type\StringType::class))), + ]), + // a Yes-tracked target under a Maybe consequence: maxMin keeps Yes + $sfHarness->key($sfMergeT5) => $sfByKey([ + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeT5, $sfSideType($side, \PHPStan\Type\StringType::class))), + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeT5, $sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, 'y'), 'maybe')), + ]), + // the same guard set as the other scope's holder for the same target: + // mergeSameGuardConditionalExpressions() unions the two consequents + $sfHarness->key($sfMergeD) => $sfByKey([ + $sfSideConditional($side, $guard, $sfSideHolder($side, $sfMergeD, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 1))), + // a guard set that merely contains the other scope's: the two must + // match exactly to be merged + $sfSideConditional($side, $guard + [$sfHarness->key($sfMergeT3) => $sfSideHolder($side, $sfMergeT3, $sfSideType($side, \PHPStan\Type\StringType::class))], $sfSideHolder($side, $sfMergeD, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 4))), + ]), + ]; +}; +$sfMergeTablesB = static function (string $side) use ($sfHarness, $sfSideType, $sfSideHolder, $sfMergeG, $sfMergeT2, $sfMergeD, $sfMergeInit): array { + $table = [ + $sfHarness->key($sfMergeG) => $sfSideHolder($side, $sfMergeG, $sfSideType($side, \PHPStan\Type\StringType::class)), + $sfHarness->key($sfMergeT2) => $sfSideHolder($side, $sfMergeT2, $sfSideType($side, \PHPStan\Type\StringType::class), 'maybe'), + $sfHarness->key($sfMergeD) => $sfSideHolder($side, $sfMergeD, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 2)), + $sfHarness->key($sfMergeInit) => $sfSideHolder($side, $sfMergeInit, $sfSideType($side, \PHPStan\Type\NullType::class)), + ]; + + return [$table, $table, []]; +}; +$sfMergeConditionalsB = static function (string $side) use ($sfHarness, $sfSideType, $sfSideHolder, $sfSideConditional, $sfByKey, $sfMergeG, $sfMergeT2, $sfMergeT3, $sfMergeT4, $sfMergeD, $sfMergeProp): array { + $ourGuard = [$sfHarness->key($sfMergeG) => $sfSideHolder($side, $sfMergeG, $sfSideType($side, \PHPStan\Type\StringType::class))]; + $theirGuard = [$sfHarness->key($sfMergeG) => $sfSideHolder($side, $sfMergeG, $sfSideType($side, \PHPStan\Type\IntegerType::class))]; + + // a guard the other branch's state still allows: such a holder is rescued + // only when the other branch already satisfies its consequent + $possibleGuard = [$sfHarness->key($sfMergeG) => $sfSideHolder($side, $sfMergeG, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 3))]; + + return [ + // a guard the other branch's flat state makes impossible: + // preserveVacuousConditionalExpressions() rescues the holder + $sfHarness->key($sfMergeT2) => $sfByKey([ + $sfSideConditional($side, $ourGuard, $sfSideHolder($side, $sfMergeT2, $sfSideType($side, \PHPStan\Type\IntegerType::class))), + // the other branch tracks $t2 with a weaker certainty than this + // consequent asks for: not rescued + $sfSideConditional($side, $possibleGuard, $sfSideHolder($side, $sfMergeT2, $sfSideType($side, \PHPStan\Type\IntegerType::class))), + ]), + // already satisfied in the other branch: rescued + $sfHarness->key($sfMergeT3) => $sfByKey([ + $sfSideConditional($side, $possibleGuard, $sfSideHolder($side, $sfMergeT3, $sfSideType($side, \PHPStan\Type\StringType::class))), + // an ErrorType consequent is a subtype of everything and would + // always look satisfied: never rescued + $sfSideConditional($side, $possibleGuard, $sfSideHolder($side, $sfMergeT3, $sfSideType($side, \PHPStan\Type\ErrorType::class))), + ]), + // a No consequent survives the impossible-guard rescue only over a + // plain variable + $sfHarness->key($sfMergeT4) => $sfByKey([ + $sfSideConditional($side, $ourGuard, $sfSideHolder($side, $sfMergeT4, $sfSideType($side, \PHPStan\Type\StringType::class), 'no')), + $sfSideConditional($side, $ourGuard, $sfSideHolder($side, $sfMergeProp, $sfSideType($side, \PHPStan\Type\StringType::class), 'no')), + ]), + $sfHarness->key($sfMergeD) => $sfByKey([ + $sfSideConditional($side, $theirGuard, $sfSideHolder($side, $sfMergeD, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 2))), + // the same guard set under a weaker certainty: not merged + $sfSideConditional($side, $theirGuard, $sfSideHolder($side, $sfMergeD, $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, 6), 'maybe')), + ]), + ]; +}; +// a non-empty TemplateArgumentConstraints: addTemplateArgumentConstraints() +// answers with $this for an empty one, so only this makes mergeWith()'s +// pass-through of the other scope's constraints observable +$sfNonEmptyConstraints = (new \ReflectionClass(\PHPStan\Analyser\Generics\TemplateArgumentConstraints::class))->newInstanceWithoutConstructor(); +(new \ReflectionProperty(\PHPStan\Analyser\Generics\TemplateArgumentConstraints::class, 'fact'))->setValue($sfNonEmptyConstraints, [new \stdClass(), null, null, false]); +$sfMergeScopeA = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)); +$sfScopes[spl_object_id($sfMergeScopeA)] = [$sfMergeScopeA, [$sfMergeG, $sfMergeT2], null, $sfMergeTablesA, $sfMergeConditionalsA]; +// afterExtractCall differs between the two: the merge ANDs them +$sfMergeScopeB = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile))->afterExtractCall(); +$sfScopes[spl_object_id($sfMergeScopeB)] = [$sfMergeScopeB, [$sfMergeG, $sfMergeT2], null, $sfMergeTablesB, $sfMergeConditionalsB]; + +// two more adjacent scopes for the generalization: every arm of +// generalizeType() needs both inputs in one side's classes, so their tables +// are built per side too. The second one's $other is the first, and both +// directions are probed. +$sfSideCombinator = static fn (string $side): string => $side === 'native' ? 'PHPStanTurbo\\TypeCombinator' : \PHPStan\Type\TypeCombinator::class; +$sfSideUnion = static fn (string $side, \PHPStan\Type\Type ...$types): \PHPStan\Type\Type => ($sfSideCombinator($side) . '::union')(...$types); +$sfSideIntersect = static fn (string $side, \PHPStan\Type\Type ...$types): \PHPStan\Type\Type => ($sfSideCombinator($side) . '::intersect')(...$types); +$sfSideRange = static fn (string $side, ?int $min, ?int $max): \PHPStan\Type\Type => ($side === 'native' ? 'PHPStanTurbo\\IntegerRangeType' : \PHPStan\Type\IntegerRangeType::class)::fromInterval($min, $max); +/** @param array $pairs */ +$sfSideConstantArray = static function (string $side, array $pairs) use ($sfSideType): \PHPStan\Type\Type { + $builder = ($side === 'native' ? 'PHPStanTurbo\\ConstantArrayTypeBuilder' : \PHPStan\Type\Constant\ConstantArrayTypeBuilder::class)::createEmpty(); + foreach ($pairs as $key => $value) { + $builder->setOffsetValueType($sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, $key), $value); + } + + return $builder->getArray(); +}; +/** @param array $pairs */ +$sfSideSealedArray = static function (string $side, array $pairs) use ($sfSideType): \PHPStan\Type\Type { + $keyTypes = []; + $valueTypes = []; + foreach ($pairs as $key => $value) { + $keyTypes[] = $sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, $key); + $valueTypes[] = $value; + } + $never = $sfSideType($side, \PHPStan\Type\NeverType::class, true); + + return new ($side === 'native' ? 'PHPStanTurbo\\ConstantArrayType' : \PHPStan\Type\Constant\ConstantArrayType::class)($keyTypes, $valueTypes, [0], [], null, [$never, $never]); +}; +$sfGeneralizeVars = []; +foreach (['gInt', 'gIntGreater', 'gIntNeither', 'gIntBoth', 'gRange', 'gRangeSmaller', 'gRangeOpen', 'gString', 'gFloat', 'gBool', 'gScalarMix', 'gShape', 'gShapeKeys', 'gArray', 'gDeep', 'gList', 'gBenevolent', 'gAccessory', 'gSame', 'gRoot', 'gRefTarget', 'gRefAlias', 'gRefAssigned', 'gShapeSize', 'gSealed', 'gAccessoryArray', 'gListUnion'] as $sfGeneralizeName) { + $sfGeneralizeVars[$sfGeneralizeName] = new \PhpParser\Node\Expr\Variable($sfGeneralizeName); +} +// a longer key over $gRoot: once $gRoot generalizes, +// ScopeOps::shouldInvalidateExpression() drops this one from the result +$sfGeneralizeDim = new \PhpParser\Node\Expr\ArrayDimFetch($sfGeneralizeVars['gRoot'], new \PhpParser\Node\Scalar\Int_(0)); +// a reference created before the loop: generalizeWithVariableState() seeds the +// writable set with the intertwined variable and both aliased roots +$sfGeneralizeIntertwined = new \PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr('gRefTarget', $sfGeneralizeVars['gRefAlias'], $sfGeneralizeVars['gRefAssigned']); +$sfGeneralizeTables = static function (bool $first) use ($sfHarness, $sfSideType, $sfSideHolder, $sfSideUnion, $sfSideIntersect, $sfSideRange, $sfSideConstantArray, $sfSideSealedArray, $sfGeneralizeVars, $sfGeneralizeDim, $sfGeneralizeIntertwined): callable { + return static function (string $side) use ($first, $sfHarness, $sfSideType, $sfSideHolder, $sfSideUnion, $sfSideIntersect, $sfSideRange, $sfSideConstantArray, $sfSideSealedArray, $sfGeneralizeVars, $sfGeneralizeDim, $sfGeneralizeIntertwined): array { + $int = $sfSideType($side, \PHPStan\Type\IntegerType::class); + $string = $sfSideType($side, \PHPStan\Type\StringType::class); + $constInt = static fn (int $value): \PHPStan\Type\Type => $sfSideType($side, \PHPStan\Type\Constant\ConstantIntegerType::class, $value); + $constString = static fn (string $value): \PHPStan\Type\Type => $sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, $value); + $array = static fn (\PHPStan\Type\Type $key, \PHPStan\Type\Type $value): \PHPStan\Type\Type => $sfSideType($side, \PHPStan\Type\ArrayType::class, $key, $value); + $types = $first + ? [ + // the constant-integer arm: a wider max, a lower min, neither, both + 'gInt' => $constInt(1), + 'gIntGreater' => $sfSideUnion($side, $constInt(1), $constInt(3)), + 'gIntNeither' => $sfSideUnion($side, $constInt(1), $constInt(9)), + 'gIntBoth' => $sfSideUnion($side, $constInt(2), $constInt(3)), + // the integer-range arm + 'gRange' => $sfSideRange($side, 0, 10), + 'gRangeSmaller' => $sfSideRange($side, 0, 10), + 'gRangeOpen' => $sfSideRange($side, null, 10), + // the constant scalar arms (generalize(moreSpecific())) + 'gString' => $constString('a'), + 'gFloat' => $sfSideType($side, \PHPStan\Type\Constant\ConstantFloatType::class, 1.0), + 'gBool' => $sfSideType($side, \PHPStan\Type\Constant\ConstantBooleanType::class, true), + // one bucket empty on each side + 'gScalarMix' => $constString('a'), + // the constant-array arms: the same key set (the builder path) + // and a differing one (the sealed-shape path) + 'gShape' => $sfSideConstantArray($side, ['a' => $constInt(1), 'b' => $constInt(2)]), + 'gShapeKeys' => $sfSideConstantArray($side, ['a' => $constInt(1)]), + // the general-array arm, its nesting-depth guard and its accessories + 'gArray' => $array($int, $string), + 'gDeep' => $array($int, $array($int, $array($int, $array($int, $string)))), + 'gRoot' => $array($int, $array($int, $array($int, $array($int, $string)))), + 'gRefTarget' => $array($int, $array($int, $array($int, $array($int, $string)))), + 'gRefAlias' => $array($int, $array($int, $array($int, $array($int, $string)))), + 'gRefAssigned' => $array($int, $array($int, $array($int, $array($int, $string)))), + 'gList' => $sfSideIntersect($side, $array($int, $int), $sfSideType($side, \PHPStan\Type\Accessory\AccessoryArrayListType::class), $sfSideType($side, \PHPStan\Type\Accessory\NonEmptyArrayType::class)), + // a BenevolentUnion input is re-wrapped at the end + 'gBenevolent' => $sfSideType($side, \PHPStan\Type\BenevolentUnionType::class, [$int, $string]), + // TypeUtils::getAccessoryTypes($a) is read off the FIRST argument + 'gAccessory' => $sfSideIntersect($side, $string, $sfSideType($side, \PHPStan\Type\Accessory\AccessoryNonEmptyStringType::class)), + // equal on both sides: generalizeType() answers with $a + // a shape whose size range does not cover the other's: the + // literal-shape arm needs both the key types to match AND the + // size comparison to hold + 'gShapeSize' => $sfSideConstantArray($side, ['a' => $constInt(1), 'b' => $constInt(2)]), + // explicitly sealed shapes (the builder only seals under + // bleeding edge): the literal key/value union arm + 'gSealed' => $sfSideSealedArray($side, ['a' => $constInt(1)]), + // accessories are read off the FIRST argument only + 'gAccessoryArray' => $sfSideIntersect($side, $array($int, $string), $sfSideType($side, \PHPStan\Type\Accessory\NonEmptyArrayType::class)), + 'gListUnion' => $sfSideUnion($side, $sfSideIntersect($side, $array($int, $int), $sfSideType($side, \PHPStan\Type\Accessory\AccessoryArrayListType::class)), $sfSideIntersect($side, $array($int, $string), $sfSideType($side, \PHPStan\Type\Accessory\AccessoryArrayListType::class))), + 'gSame' => $int, + ] + : [ + 'gInt' => $constInt(5), + 'gIntGreater' => $constInt(5), + 'gIntNeither' => $constInt(5), + 'gIntBoth' => $sfSideUnion($side, $constInt(1), $constInt(9)), + 'gRange' => $sfSideRange($side, 0, 20), + 'gRangeSmaller' => $sfSideRange($side, -5, 10), + 'gRangeOpen' => $sfSideRange($side, 0, null), + 'gString' => $constString('b'), + 'gFloat' => $sfSideType($side, \PHPStan\Type\Constant\ConstantFloatType::class, 2.5), + 'gBool' => $sfSideType($side, \PHPStan\Type\Constant\ConstantBooleanType::class, false), + 'gScalarMix' => $constInt(1), + 'gShape' => $sfSideConstantArray($side, ['a' => $constString('x'), 'b' => $constInt(7)]), + 'gShapeKeys' => $sfSideConstantArray($side, ['a' => $constInt(1), 'b' => $constInt(2)]), + 'gArray' => $array($int, $int), + 'gDeep' => $array($int, $array($int, $int)), + 'gRoot' => $array($int, $array($int, $int)), + 'gRefTarget' => $array($int, $array($int, $int)), + 'gRefAlias' => $array($int, $array($int, $int)), + 'gRefAssigned' => $array($int, $array($int, $int)), + 'gList' => $sfSideIntersect($side, $array($int, $string), $sfSideType($side, \PHPStan\Type\Accessory\AccessoryArrayListType::class), $sfSideType($side, \PHPStan\Type\Accessory\NonEmptyArrayType::class)), + 'gBenevolent' => $string, + 'gAccessory' => $constString('x'), + 'gShapeSize' => (static function () use ($side, $sfSideType, $constInt): \PHPStan\Type\Type { + $builder = ($side === 'native' ? 'PHPStanTurbo\\ConstantArrayTypeBuilder' : \PHPStan\Type\Constant\ConstantArrayTypeBuilder::class)::createEmpty(); + $builder->setOffsetValueType($sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, 'a'), $constInt(1)); + $builder->setOffsetValueType($sfSideType($side, \PHPStan\Type\Constant\ConstantStringType::class, 'b'), $constInt(2), true); + + return $builder->getArray(); + })(), + 'gSealed' => $sfSideSealedArray($side, ['a' => $constInt(1), 'b' => $constInt(2)]), + 'gAccessoryArray' => $array($int, $int), + 'gListUnion' => $sfSideIntersect($side, $array($int, $sfSideType($side, \PHPStan\Type\FloatType::class)), $sfSideType($side, \PHPStan\Type\Accessory\AccessoryArrayListType::class)), + 'gSame' => $int, + ]; + $table = []; + foreach ($types as $name => $type) { + $table[$sfHarness->key($sfGeneralizeVars[$name])] = $sfSideHolder($side, $sfGeneralizeVars[$name], $type); + } + $table[$sfHarness->key($sfGeneralizeDim)] = $sfSideHolder($side, $sfGeneralizeDim, $first ? $string : $int); + $table[$sfHarness->key($sfGeneralizeIntertwined)] = $sfSideHolder($side, $sfGeneralizeIntertwined, $first ? $string : $int); + + return [$table, $table, []]; + }; +}; +$sfGeneralizeScopeA = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)); +$sfScopes[spl_object_id($sfGeneralizeScopeA)] = [$sfGeneralizeScopeA, [$sfGeneralizeVars['gInt'], $sfGeneralizeVars['gShape']], null, $sfGeneralizeTables(true)]; +$sfGeneralizeScopeB = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)); +$sfScopes[spl_object_id($sfGeneralizeScopeB)] = [$sfGeneralizeScopeB, [$sfGeneralizeVars['gInt'], $sfGeneralizeVars['gShape']], null, $sfGeneralizeTables(false)]; +// the same tables as the first one again: generalizeType() is not symmetric +// (the size comparison, the open range bounds and the accessory types are all +// read off the FIRST argument), and only the receiving scope's factory is the +// one in builder mode - `$other->generalizeWith($scope)` would answer with the +// previous iteration's canned scope. This third scope's $other is the second, +// so the A-against-B direction runs on a receiver that can derive. +$sfGeneralizeScopeC = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)); +$sfScopes[spl_object_id($sfGeneralizeScopeC)] = [$sfGeneralizeScopeC, [$sfGeneralizeVars['gInt'], $sfGeneralizeVars['gShape']], null, $sfGeneralizeTables(true)]; + +// ---- rebuild each scope on both sides and compare method by method ---- +$sfObservations = ['php' => [], 'native' => []]; +$sfProbeVariables = ['argc', 'argv', 'undefinedVariable', '_GET', 'this', 'GLOBALS']; +$sfProbeConstants = [ + new \PhpParser\Node\Name('PHP_VERSION'), + new \PhpParser\Node\Name\FullyQualified('PHP_EOL'), + new \PhpParser\Node\Name('ANSWER'), + new \PhpParser\Node\Name('DEFINITELY_NOT_DEFINED_XYZ'), + new \PhpParser\Node\Name('__COMPILER_HALT_OFFSET__'), +]; +$sfSampleCount = 0; +$sfBarrierHits = 0; +// a protected method (findSettledStoredResult) called from a closure bound +// to the scope's own class +$sfProtected = static fn (object $scope, string $method, mixed ...$a): mixed => \Closure::bind(fn () => $this->$method(...$a), $scope, get_class($scope))(); +// a property of any scope object, whichever class in its hierarchy declares it +// (the native class declares the twin's properties with the twin's visibility, +// so neither side is readable from one bound Closure scope) +$sfProp = static function (object $scope, string $name): mixed { + $class = new \ReflectionClass($scope); + while (!$class->hasProperty($name)) { + $class = $class->getParentClass(); + } + + return $class->getProperty($name)->getValue($scope); +}; +// the mutable state of the scope the RecordingScopeFactory answers with: the +// chaining bodies specify types in place on it, so each producer's run is +// undone before the next one (and before the other side's pass) +$sfMutableProps = ['expressionTypes', 'nativeExpressionTypes', 'conditionalExpressions', 'resolvedTypes']; +$sfSnapshot = static function (object $scope, ?array $props = null) use ($sfProp, $sfMutableProps): array { + $state = []; + foreach ($props ?? $sfMutableProps as $name) { + $state[$name] = $sfProp($scope, $name); + } + + return $state; +}; +$sfRestore = static function (object $scope, array $state): void { + $class = new \ReflectionClass($scope); + foreach ($state as $name => $value) { + $declaring = $class; + while (!$declaring->hasProperty($name)) { + $declaring = $declaring->getParentClass(); + } + $declaring->getProperty($name)->setValue($scope, $value); + } +}; +// the observable state of a scope a chaining producer answered: the scopes +// ScopeOps::scopeWith() clones never reach the factory, so their argument +// lists are no observable — and a raw scope normalizes to its object id +$sfScopeDigest = static function (mixed $result, object $scope, object $dummy) use ($sfHarness, $sfProp): mixed { + if (!$result instanceof \PHPStan\Analyser\MutatingScope && !$result instanceof \PHPStanTurbo\MutatingScope) { + return $sfHarness->norm($result); + } + $parentScope = $sfProp($result, 'parentScope'); + + return [ + 'result' => $result === $scope ? 'this' : ($result === $dummy ? 'factory result' : 'derived'), + 'class' => $sfHarness->className($result), + 'expressionTypes' => $sfHarness->norm($sfProp($result, 'expressionTypes')), + 'nativeExpressionTypes' => $sfHarness->norm($sfProp($result, 'nativeExpressionTypes')), + 'conditionalExpressions' => array_map(static fn (array $holders): array => array_keys($holders), $sfProp($result, 'conditionalExpressions')), + 'inClosureBindScopeClasses' => $sfProp($result, 'inClosureBindScopeClasses'), + 'anonymousFunctionReflection' => $sfHarness->norm($sfProp($result, 'anonymousFunctionReflection')), + 'inFirstLevelStatement' => $sfProp($result, 'inFirstLevelStatement'), + 'currentlyAssignedExpressions' => $sfProp($result, 'currentlyAssignedExpressions'), + 'currentlyAllowedUndefinedExpressions' => $sfProp($result, 'currentlyAllowedUndefinedExpressions'), + 'inFunctionCallsStack' => $sfHarness->norm($sfProp($result, 'inFunctionCallsStack')), + 'afterExtractCall' => $sfProp($result, 'afterExtractCall'), + 'parentScope' => $parentScope === null ? null : $sfHarness->className($parentScope), + 'nativeTypesPromoted' => $sfProp($result, 'nativeTypesPromoted'), + 'namespace' => $sfProp($result, 'namespace'), + 'templateArgumentConstraints' => $sfHarness->norm($sfProp($result, 'templateArgumentConstraints')), + 'resolvedTypes' => array_keys($sfProp($result, 'resolvedTypes')), + ]; +}; +$sfProbeNames = [ + new \PhpParser\Node\Name('self'), + new \PhpParser\Node\Name('static'), + new \PhpParser\Node\Name('parent'), + new \PhpParser\Node\Name('Holder'), + new \PhpParser\Node\Name\FullyQualified('ScopeFamilyFixture\\Holder'), + new \PhpParser\Node\Name('Nope\\Missing'), +]; +$sfProbeValues = [1, 'a', 1.5, true, null, [1, 'a' => 2]]; +// the in-function-call stack entries and the classes of the enter* family +// (built once: both sides push the same objects) +$sfPushReflection = $sfReflectionProvider->getFunction(new \PhpParser\Node\Name('strlen'), null); +$sfPushParameter = new \PHPStan\Reflection\Native\NativeParameterReflection('p', false, new \PHPStan\Type\StringType(), \PHPStan\Reflection\PassedByReference::createNo(), false, null); +$sfHolderReflection = $sfReflectionProvider->getClass(\ScopeFamilyFixture\Holder::class); +$sfCustomReflection = $sfReflectionProvider->getClass(\ScopeFamilyFixture\Custom::class); +$sfTraitReflection = $sfReflectionProvider->getClass(\ScopeFamilyFixture\HelperTrait::class); +$sfBaseReflection = $sfReflectionProvider->getClass(\ScopeFamilyFixture\Base::class); +$sfChildReflection = $sfReflectionProvider->getClass(\ScopeFamilyFixture\Child::class); +// the members of the visibility queries: a public and a private one +// of the class the fixture's scopes live in, and the protected / private / +// asymmetrically-writable ones of a small hierarchy +$sfPublicProperty = $sfHolderReflection->getNativeProperty('name'); +$sfPrivateProperty = $sfHolderReflection->getNativeProperty('inner'); +$sfProtectedProperty = $sfBaseReflection->getNativeProperty('protectedCounter'); +$sfPrivateSetProperty = $sfBaseReflection->getNativeProperty('tag'); +$sfPublicMethod = $sfHolderReflection->getNativeMethod('read'); +$sfProtectedMethod = $sfBaseReflection->getNativeMethod('protectedMethod'); +$sfChildProtectedMethod = $sfChildReflection->getNativeMethod('protectedMethod'); +$sfChildProtectedProperty = $sfChildReflection->getNativeProperty('childCounter'); +$sfChildProtectedConstant = $sfChildReflection->getConstant('CHILD_PROTECTED_CONST'); +$sfPrivateMethod = $sfBaseReflection->getNativeMethod('privateMethod'); +$sfPublicConstant = $sfBaseReflection->getConstant('PUBLIC_CONST'); +$sfProtectedConstant = $sfBaseReflection->getConstant('PROTECTED_CONST'); +$sfPrivateConstant = $sfBaseReflection->getConstant('PRIVATE_CONST'); +$sfProbeClassNames = ['ScopeFamilyFixture\\Holder', '\\ScopeFamilyFixture\\Holder', 'Nope\\Missing', 'X', 'Nope']; +$sfProbeFunctionNames = ['strlen', '\\strlen', 'nope_missing', '\\nope_missing']; +// the function-like nodes of the enter* family +$sfEmptyTemplateTypeMap = \PHPStan\Type\Generic\TemplateTypeMap::createEmpty(); +$sfProbeMethodParams = [ + // the attribute argument is resolved through InitializerExprContext, whose + // class name getParameterAttributes() fills in + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('a'), null, new \PhpParser\Node\Identifier('int'), false, false, [], 0, [ + new \PhpParser\Node\AttributeGroup([new \PhpParser\Node\Attribute(new \PhpParser\Node\Name\FullyQualified('Attribute'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\MagicConst\Class_())])]), + ]), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('b'), new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null')), new \PhpParser\Node\Identifier('string')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('c'), new \PhpParser\Node\Scalar\Int_(3), new \PhpParser\Node\Name('static')), + // no native type: a conditional @param type survives TypehintHelper::decideType() only here + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('d')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('e')), +]; +$sfProbeVariadicParams = array_merge($sfProbeMethodParams, [ + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('rest'), null, new \PhpParser\Node\Identifier('string'), false, true), +]); +$sfProbeClassMethod = new \PhpParser\Node\Stmt\ClassMethod(new \PhpParser\Node\Identifier('doIt'), ['params' => $sfProbeMethodParams, 'returnType' => new \PhpParser\Node\Name('static'), 'stmts' => []]); +$sfProbeVariadicClassMethod = new \PhpParser\Node\Stmt\ClassMethod(new \PhpParser\Node\Identifier('doItVariadic'), ['params' => $sfProbeVariadicParams, 'returnType' => new \PhpParser\Node\Identifier('string'), 'stmts' => []]); +$sfProbeStaticClassMethod = new \PhpParser\Node\Stmt\ClassMethod(new \PhpParser\Node\Identifier('doItStatic'), ['flags' => \PhpParser\Modifiers::STATIC, 'params' => $sfProbeMethodParams, 'returnType' => new \PhpParser\Node\Identifier('void'), 'stmts' => []]); +$sfProbeFunction = new \PhpParser\Node\Stmt\Function_(new \PhpParser\Node\Identifier('doItFn'), ['params' => $sfProbeMethodParams, 'stmts' => []]); +// the anonymous- and arrow-function nodes: a use list mixing a +// by-value capture of a variable the scope knows, one it does not, and a +// by-ref capture (each arm of enterAnonymousFunctionWithoutReflection()) +$sfProbeClosureUses = [ + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('p')), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('neverDefinedUse')), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('byRefUse'), true), +]; +$sfProbeClosureParams = [ + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('a'), null, new \PhpParser\Node\Identifier('int')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('b'), new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null')), new \PhpParser\Node\Identifier('string')), +]; +$sfProbeClosure = new \PhpParser\Node\Expr\Closure(['params' => $sfProbeClosureParams, 'uses' => $sfProbeClosureUses, 'stmts' => []]); +$sfProbeStaticClosure = new \PhpParser\Node\Expr\Closure(['static' => true, 'params' => $sfProbeClosureParams, 'uses' => $sfProbeClosureUses, 'stmts' => []]); +// untyped parameters: getFunctionType() answers a PHP mixed on both sides, so +// intersectButNotNever() stays on one side's classes and the callable-parameter +// arms are comparable under the prefix +$sfProbeUntypedParams = [ + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('u')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('v')), +]; +$sfProbeUntypedClosure = new \PhpParser\Node\Expr\Closure(['params' => $sfProbeUntypedParams, 'uses' => [], 'stmts' => []]); +$sfProbeUntypedVariadicClosure = new \PhpParser\Node\Expr\Closure(['params' => array_merge($sfProbeUntypedParams, [ + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('w'), null, null, false, true), +]), 'uses' => [], 'stmts' => []]); +$sfProbeArrowFunction = new \PhpParser\Node\Expr\ArrowFunction(['params' => $sfProbeClosureParams, 'expr' => new \PhpParser\Node\Expr\Variable('a')]); +$sfProbeStaticArrowFunction = new \PhpParser\Node\Expr\ArrowFunction(['static' => true, 'params' => $sfProbeClosureParams, 'expr' => new \PhpParser\Node\Expr\Variable('a')]); +$sfProbeVariadicArrowFunction = new \PhpParser\Node\Expr\ArrowFunction(['params' => array_merge($sfProbeClosureParams, [ + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('rest'), null, new \PhpParser\Node\Identifier('string'), false, true), +]), 'expr' => new \PhpParser\Node\Expr\Variable('a')]); +$sfPushVariadicParameter = new \PHPStan\Reflection\Native\NativeParameterReflection('rest', true, new \PHPStan\Type\IntegerType(), \PHPStan\Reflection\PassedByReference::createNo(), true, null); +$sfProbeGetHook = new \PhpParser\Node\PropertyHook(new \PhpParser\Node\Identifier('get'), null, ['params' => []]); +$sfProbeSetHook = new \PhpParser\Node\PropertyHook(new \PhpParser\Node\Identifier('set'), null, ['params' => []]); +$sfProbeSetHookWithParam = new \PhpParser\Node\PropertyHook(new \PhpParser\Node\Identifier('set'), null, ['params' => [new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('v'), null, new \PhpParser\Node\Identifier('string'))]]); +$sfProbeUnknownHook = new \PhpParser\Node\PropertyHook(new \PhpParser\Node\Identifier('nope'), null, ['params' => []]); +// the parameters and type nodes of the function-like family +$sfProbeParams = [ + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('x')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('x'), new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null')), new \PhpParser\Node\Identifier('int')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('x'), new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('NULL')), new \PhpParser\Node\Identifier('string')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('x'), new \PhpParser\Node\Scalar\Int_(1), new \PhpParser\Node\Identifier('int')), + new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('x'), null, new \PhpParser\Node\Identifier('string'), false, true), +]; +$sfProbeTypeNodes = [ + null, + new \PhpParser\Node\Identifier('int'), + new \PhpParser\Node\Identifier('static'), + new \PhpParser\Node\Name('self'), + new \PhpParser\Node\Name('static'), + new \PhpParser\Node\Name('parent'), + new \PhpParser\Node\Name('Holder'), + new \PhpParser\Node\Name\FullyQualified('ScopeFamilyFixture\\Holder'), + new \PhpParser\Node\NullableType(new \PhpParser\Node\Identifier('int')), +]; +/** @var array the previous scope of each side, the $other of the two-scope methods */ +$sfPrevious = ['php' => null, 'native' => null]; +foreach ($sfScopes as $sfId => [$sfWalkScope, $sfExprs, $sfStorage]) { + $sfArgs = $sfHarness->constructorArgs($sfWalkScope); + $sfPerSideTables = $sfScopes[$sfId][3] ?? null; + $sfPerSideConditionals = $sfScopes[$sfId][4] ?? null; + $sfSampleCount++; + foreach (['php', 'native'] as $side) { + $observe = static function (string $label, callable $fn) use (&$sfObservations, $side, $sfHarness, $sfId): void { + try { + $sfObservations[$side][$sfId][$label] = $sfHarness->norm($fn()); + } catch (\Throwable $e) { + $sfObservations[$side][$sfId][$label] = $sfHarness->norm($e); + } + }; + + $dummy = $sfWalkScope; + $factory = new RecordingScopeFactory($dummy); + $args = $sfArgs; + $args['scopeFactory'] = $factory; + if ($sfPerSideTables !== null) { + [$args['expressionTypes'], $args['nativeExpressionTypes'], $args['inFunctionCallsStack']] = $sfPerSideTables($side); + } elseif ($side === 'native') { + $args['expressionTypes'] = $sfHarness->nativeHolders($args['expressionTypes']); + $args['nativeExpressionTypes'] = $sfHarness->nativeHolders($args['nativeExpressionTypes']); + } + if ($sfPerSideConditionals !== null) { + $args['conditionalExpressions'] = $sfPerSideConditionals($side); + } elseif ($side === 'native') { + $args['conditionalExpressions'] = $sfHarness->nativeConditionalExpressions($args['conditionalExpressions']); + } + if ($side === 'native') { + $scope = new NativeScope(...array_values($args)); + $scope->twin = $sfWalkScope; + } else { + $scope = new PhpScope(...array_values($args)); + $scope->inner = $sfWalkScope; + } + $other = $sfPrevious[$side] ?? $scope; + $sfPrevious[$side] = $scope; + $sfHarness->currentScope = $scope; + $sfShared = []; + foreach ([$args['expressionTypes'], $args['nativeExpressionTypes']] as $sfTable) { + foreach ($sfTable as $sfHolder) { + $sfShared[spl_object_id($sfHolder->getExpr())] = true; + } + } + $sfHarness->sharedExprIds = $sfShared; + + // the plain getters + foreach ([ + 'getFile', 'getFileDescription', 'isDeclareStrictTypes', 'isInClass', 'isInTrait', 'getClassReflection', 'getTraitReflection', + 'getFunction', 'getFunctionName', 'getNamespace', 'getParentScope', 'canAnyVariableExist', 'isInAnonymousFunction', + 'getAnonymousFunctionReflection', 'getAnonymousFunctionReturnType', 'isInFirstLevelStatement', 'getDefinedVariables', + 'getMaybeDefinedVariables', 'getExprPrinter', 'getCurrentTemplateArgumentFrame', 'getTemplateArgumentConstraints', + 'getCurrentExpressionResultStorage', 'getFunctionCallStack', 'getFunctionCallStackWithParameters', 'isInClosureBind', + ] as $method) { + $observe($method, static fn () => $scope->$method()); + } + $observe('toWalkScope identity', static fn () => $scope->parentToWalkScope() === $scope); + $observe('toWalkScope delegation', static fn () => $scope->toWalkScope() === $sfWalkScope); + $observe('toMutatingScope identity', static fn () => $scope->toMutatingScope() === $scope); + + // variables + $variables = array_unique(array_merge($scope->getDefinedVariables(), $scope->getMaybeDefinedVariables(), $sfProbeVariables)); + foreach ($variables as $variable) { + $observe("hasVariableType($variable)", static fn () => $scope->hasVariableType($variable)); + $observe("getVariableType($variable)", static fn () => $scope->getVariableType($variable)); + } + + // expressions the walk asked about here, and the tracked ones + $exprs = $sfExprs; + foreach (array_slice($args['expressionTypes'], 0, 12) as $holder) { + $exprs[] = $holder->getExpr(); + } + $exprs[] = new \PhpParser\Node\Expr\MethodCall(new \PhpParser\Node\Expr\Variable('this'), 'read'); + $exprs[] = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'name'); + $exprs[] = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'inner'), 'name'); + $exprs[] = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('other'), 'name'); + $exprs[] = new \PhpParser\Node\Expr\NullsafePropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'name'); + $exprs[] = new \PhpParser\Node\Expr\NullsafeMethodCall(new \PhpParser\Node\Expr\Variable('this'), 'read'); + $exprs[] = new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('items'), new \PhpParser\Node\Scalar\Int_(0)); + // untracked, over the per-side tables' differing $arr entry: the flavour + // getStateType() picks follows the scope's nativeTypesPromoted + $exprs[] = new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('arr'), new \PhpParser\Node\Expr\Variable('i')); + $exprs[] = new \PhpParser\Node\Expr\StaticPropertyFetch(new \PhpParser\Node\Name('self'), 'nope'); + $exprs[] = new \PhpParser\Node\Expr\ClassConstFetch(new \PhpParser\Node\Name('Holder'), 'class'); + $exprs[] = new \PhpParser\Node\Scalar\String_('literal'); + $exprs[] = new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_EOL')); + $exprs[] = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('strlen'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('x'))]); + $exprs[] = new \PhpParser\Node\Expr\Match_(new \PhpParser\Node\Expr\Variable('x'), [new \PhpParser\Node\MatchArm(null, new \PhpParser\Node\Scalar\Int_(1))]); + $exprs[] = new \PhpParser\Node\Expr\Closure(['params' => [], 'stmts' => []]); + + // ---- the type resolution core, with the walk's storage + // (or a fresh one for a hand-built scope) as the analysis in progress + $stack = $args['expressionResultStorageStack']; + $stack->push($sfStorage ?? new \PHPStan\Analyser\ExpressionResultStorage()); + try { + foreach ($exprs as $i => $expr) { + $key = $sfHarness->key($expr); + $observe("getNodeKey#$i $key", static fn () => $scope->getNodeKey($expr)); + $observe("hasExpressionType#$i $key", static fn () => $scope->hasExpressionType($expr)); + if ($scope->hasExpressionType($expr)->yes()) { + $observe("getTrackedExpressionType#$i $key", static fn () => $scope->getTrackedExpressionType($expr)); + } + $observe("findPossiblyImpureCallDescriptions#$i $key", static fn () => $scope->findPossiblyImpureCallDescriptions($expr)); + if ($expr instanceof \PhpParser\Node\Expr\PropertyFetch) { + $observe("isReadonlyPropertyFetch(this)#$i $key", static fn () => $scope->isReadonlyPropertyFetch($expr, true)); + $observe("isReadonlyPropertyFetch(any)#$i $key", static fn () => $scope->isReadonlyPropertyFetch($expr, false)); + } + $observe("getType#$i $key", static fn () => $scope->getType($expr)); + $observe("getType again#$i $key", static fn () => $scope->getType($expr)); + $observe("getScopeType#$i $key", static fn () => $scope->getScopeType($expr)); + $observe("getNativeType#$i $key", static fn () => $scope->getNativeType($expr)); + $observe("getScopeNativeType#$i $key", static fn () => $scope->getScopeNativeType($expr)); + $observe("getKeepVoidType#$i $key", static fn () => $scope->getKeepVoidType($expr)); + $observe("obtainResultForNode#$i $key", static fn () => $scope->obtainResultForNode($expr)); + $observe("findSettledStoredResult#$i $key", static fn () => $sfProtected($scope, 'findSettledStoredResult', $expr)); + $observe("specifyTypesOfNewWorldHandlerNode(truthy)#$i $key", static fn () => $scope->specifyTypesOfNewWorldHandlerNode($expr, \PHPStan\Analyser\TypeSpecifierContext::createTruthy())); + $observe("specifyTypesOfNewWorldHandlerNode(falsey)#$i $key", static fn () => $scope->specifyTypesOfNewWorldHandlerNode($expr, \PHPStan\Analyser\TypeSpecifierContext::createFalsey())); + // ---- the state readers and the currently-* queries + $observe("getStateType#$i $key", static fn () => $scope->getStateType($expr)); + $observe("isInExpressionAssign#$i $key", static fn () => $scope->isInExpressionAssign($expr)); + $observe("isInWriteExpressionAssign#$i $key", static fn () => $scope->isInWriteExpressionAssign($expr)); + $observe("isUndefinedExpressionAllowed#$i $key", static fn () => $scope->isUndefinedExpressionAllowed($expr)); + } + + // the getType() memo (a public property of the twin) + $observe('resolvedTypes keys', static fn () => array_keys($scope->resolvedTypes)); + + // the guard diagnostics: a real, unprocessed node + foreach ([$exprs[0], end($exprs)] as $i => $guardExpr) { + $observe("getType under guard#$i", static function () use ($scope, $guardExpr) { + \PHPStan\Analyser\NodeScopeResolver::$guardNewWorld = true; + \PHPStan\Analyser\NodeScopeResolver::$guardRealExprIds[spl_object_id($guardExpr)] = true; + try { + return $scope->getType($guardExpr); + } finally { + \PHPStan\Analyser\NodeScopeResolver::$guardNewWorld = false; + \PHPStan\Analyser\NodeScopeResolver::$guardRealExprIds = []; + } + }); + $observe("obtainResultForNode under guard#$i", static function () use ($scope, $guardExpr) { + \PHPStan\Analyser\NodeScopeResolver::$guardNewWorld = true; + \PHPStan\Analyser\NodeScopeResolver::$guardRealExprIds[spl_object_id($guardExpr)] = true; + try { + return $scope->obtainResultForNode($guardExpr); + } finally { + \PHPStan\Analyser\NodeScopeResolver::$guardNewWorld = false; + \PHPStan\Analyser\NodeScopeResolver::$guardRealExprIds = []; + } + }); + } + $observe('getType under guard, processed', static function () use ($scope, $exprs) { + $guardExpr = $exprs[0]; + \PHPStan\Analyser\NodeScopeResolver::$guardNewWorld = true; + \PHPStan\Analyser\NodeScopeResolver::$guardRealExprIds[spl_object_id($guardExpr)] = true; + \PHPStan\Analyser\NodeScopeResolver::$guardProcessedExprIds[spl_object_id($guardExpr)] = true; + try { + return $scope->getType($guardExpr); + } finally { + \PHPStan\Analyser\NodeScopeResolver::$guardNewWorld = false; + \PHPStan\Analyser\NodeScopeResolver::$guardRealExprIds = []; + \PHPStan\Analyser\NodeScopeResolver::$guardProcessedExprIds = []; + } + }); + + // the closure cache key + $definedRoots = array_map(static fn (string $v): string => '$' . $v, array_slice($scope->getDefinedVariables(), 0, 2)); + $observe('getClosureScopeCacheKey()', static fn () => $scope->getClosureScopeCacheKey()); + $observe('getClosureScopeCacheKey([])', static fn () => $scope->getClosureScopeCacheKey([])); + $observe('getClosureScopeCacheKey([$this])', static fn () => $scope->getClosureScopeCacheKey(['$this'])); + $observe('getClosureScopeCacheKey(defined)', static fn () => $scope->getClosureScopeCacheKey($definedRoots)); + $observe('getClosureScopeCacheKey(prefix)', static fn () => $scope->getClosureScopeCacheKey(['$t', '$o', '$'])); + + // names and values + foreach ($sfProbeNames as $i => $name) { + $observe("resolveName#$i " . $name->toString(), static fn () => $scope->resolveName($name)); + $observe("resolveTypeByName#$i " . $name->toString(), static fn () => $scope->resolveTypeByName($name)); + } + foreach ($sfProbeValues as $i => $value) { + $observe("getTypeFromValue#$i", static fn () => $scope->getTypeFromValue($value)); + } + + // ---- the in-function-call stack and the enter* family + foreach ($sfProbeClassNames as $i => $className) { + $observe("isInClassExists#$i $className", static fn () => $scope->isInClassExists($className)); + } + foreach ($sfProbeFunctionNames as $i => $functionName) { + $observe("isInFunctionExists#$i $functionName", static fn () => $scope->isInFunctionExists($functionName)); + } + $observe('getPhpVersion', static fn () => $scope->getPhpVersion()); + foreach ($sfProbeParams as $i => $parameter) { + $observe("isParameterValueNullable#$i", static fn () => $scope->isParameterValueNullable($parameter)); + } + // the variadic shapes only where PHP_VERSION_ID is tracked (see the + // scopes built for it above) + // ... and only where getPhpVersion() answers with that tracked type + // itself (the overall-range scope falls back to a native type, which + // the PHP PhpVersions does not recognize) + $sfPhpVersionIdFetch = new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_VERSION_ID')); + $sfTracksPhpVersionId = $scope->hasExpressionType($sfPhpVersionIdFetch)->yes() + && $scope->getPhpVersion()->getType() === $scope->getType($sfPhpVersionIdFetch); + foreach ($sfProbeTypeNodes as $i => $typeNode) { + foreach ([[false, false], [true, false], [false, true], [true, true]] as $j => [$nullable, $variadic]) { + if ($variadic && !$sfTracksPhpVersionId) { + continue; + } + // an implicit mixed item type is the twin's `list` and the native + // IntersectionType's `list` under the prefix: its + // `$valueType instanceof MixedType` test sees the PHP MixedType the + // PHP InitializerExprTypeResolver built (one class under a real name) + if ($variadic && $scope->getFunctionType($typeNode, $nullable, false)->describe(\PHPStan\Type\VerbosityLevel::precise()) === 'mixed') { + continue; + } + $observe("getFunctionType#$i/$j", static fn () => $scope->getFunctionType($typeNode, $nullable, $variadic)); + } + } + // $functionScope->resolvedTypes = $this->resolvedTypes, on the + // factory's result (restored: the factory answers with the walk scope) + $observe('in-function-call memo', static function () use ($scope, $dummy) { + $saved = $dummy->resolvedTypes; + try { + $dummy->resolvedTypes = []; + $scope->pushInFunctionCall(null, null, true); + $remembered = array_keys($dummy->resolvedTypes); + $dummy->resolvedTypes = []; + $scope->pushInFunctionCall(null, null, false); + $forgotten = array_keys($dummy->resolvedTypes); + $dummy->resolvedTypes = []; + $scope->popInFunctionCall(); + $popped = array_keys($dummy->resolvedTypes); + + return [$remembered, $forgotten, $popped]; + } finally { + $dummy->resolvedTypes = $saved; + } + }); + + // the storage stack + $extraStorage = new \PHPStan\Analyser\ExpressionResultStorage(); + $observe('push/pop ExpressionResultStorage', static function () use ($scope, $extraStorage) { + $before = $scope->getCurrentExpressionResultStorage(); + $scope->pushExpressionResultStorage($extraStorage); + $pushed = $scope->getCurrentExpressionResultStorage() === $extraStorage; + $scope->popExpressionResultStorage(); + return [$pushed, $scope->getCurrentExpressionResultStorage() === $before]; + }); + + // the two-scope methods + $observe('getDifferingVariableRoots(self)', static fn () => $scope->getDifferingVariableRoots($scope)); + $observe('getDifferingVariableRoots(other)', static fn () => $scope->getDifferingVariableRoots($other)); + $observe('getDifferingVariableRoots(other, reversed)', static fn () => $other->getDifferingVariableRoots($scope)); + + // the scope-producing methods: the factory's recorded argument + // lists, and whether the result is the factory's or $this + $emptyConstraints = \PHPStan\Analyser\Generics\TemplateArgumentConstraints::createEmpty(); + $producers = [ + 'enterDeclareStrictTypes' => static fn () => $scope->enterDeclareStrictTypes(), + 'rememberConstructorScope' => static fn () => $scope->rememberConstructorScope(), + 'afterExtractCall' => static fn () => $scope->afterExtractCall(), + 'afterClearstatcacheCall' => static fn () => $scope->afterClearstatcacheCall(), + 'afterOpenSslCall(openssl_encrypt)' => static fn () => $scope->afterOpenSslCall('openssl_encrypt'), + 'afterOpenSslCall(openssl_nope)' => static fn () => $scope->afterOpenSslCall('openssl_nope'), + 'invalidateVolatileExpressions' => static fn () => $scope->invalidateVolatileExpressions(), + 'invalidateExistenceCheckExpressions(class_exists)' => static fn () => $scope->invalidateExistenceCheckExpressions(['class_exists'], null), + 'invalidateExistenceCheckExpressions(function_exists,nope_missing)' => static fn () => $scope->invalidateExistenceCheckExpressions(['function_exists'], 'nope_missing'), + 'withAnonymousFunctionReflection' => static fn () => $scope->withAnonymousFunctionReflection($side === 'native' ? new \PHPStanTurbo\ClosureType() : new \PHPStan\Type\ClosureType()), + 'toNodeCallbackScope' => static fn () => $scope->toNodeCallbackScope(), + 'toNodeCallbackScope again' => static fn () => $scope->toNodeCallbackScope(), + 'duplicateWith' => static fn () => $scope->duplicateWith($args['expressionTypes'], $args['nativeExpressionTypes'], $args['conditionalExpressions'], ['$x' => true], ['$y' => true], [], !$args['inFirstLevelStatement'], true), + 'withoutMemoizedTypes' => static fn () => $scope->withoutMemoizedTypes(), + 'withTemplateArgumentFrame' => static fn () => $scope->withTemplateArgumentFrame($args['templateArgumentFrame']), + 'withTemplateArgumentConstraints(same)' => static fn () => $scope->withTemplateArgumentConstraints($args['templateArgumentConstraints']), + 'withTemplateArgumentConstraints(other)' => static function () use ($scope, $args, $emptyConstraints, $sfHarness) { + $constraints = $args['templateArgumentConstraints'] === null ? $emptyConstraints : null; + $clone = $scope->withTemplateArgumentConstraints($constraints); + return [ + 'is this' => $clone === $scope, + 'class' => $sfHarness->className($clone), + 'constraints' => $clone->getTemplateArgumentConstraints() === $constraints, + 'frame kept' => $clone->getCurrentTemplateArgumentFrame() === $scope->getCurrentTemplateArgumentFrame(), + 'variables kept' => $clone->getDefinedVariables() === $scope->getDefinedVariables(), + 'callback scope memo reset' => $clone->toNodeCallbackScope() !== $scope->toNodeCallbackScope(), + ]; + }, + 'addTemplateArgumentConstraints(null)' => static fn () => $scope->addTemplateArgumentConstraints(null), + 'addTemplateArgumentConstraints(empty)' => static fn () => $scope->addTemplateArgumentConstraints($emptyConstraints), + 'doNotTreatPhpDocTypesAsCertain' => static fn () => $scope->doNotTreatPhpDocTypesAsCertain(), + 'doNotTreatPhpDocTypesAsCertain again' => static fn () => $scope->doNotTreatPhpDocTypesAsCertain(), + 'withRecordedStatementDelta(other, this)' => static fn () => $scope->withRecordedStatementDelta($other, $scope), + 'withRecordedStatementDelta(this, other)' => static fn () => $scope->withRecordedStatementDelta($scope, $other), + 'pushInFunctionCall(null)' => static fn () => $scope->pushInFunctionCall(null, null, false), + 'pushInFunctionCall(strlen, p)' => static fn () => $scope->pushInFunctionCall($sfPushReflection, $sfPushParameter, false), + 'pushInFunctionCall(strlen, remember)' => static function () use ($scope, $dummy, $sfPushReflection) { + $saved = $dummy->resolvedTypes; + try { + return $scope->pushInFunctionCall($sfPushReflection, null, true); + } finally { + $dummy->resolvedTypes = $saved; + } + }, + 'popInFunctionCall' => static function () use ($scope, $dummy) { + $saved = $dummy->resolvedTypes; + try { + return $scope->popInFunctionCall(); + } finally { + $dummy->resolvedTypes = $saved; + } + }, + 'enterClass(Holder)' => static fn () => $scope->enterClass($sfHolderReflection), + 'enterClass(Custom)' => static fn () => $scope->enterClass($sfCustomReflection), + 'enterTrait(HelperTrait)' => static fn () => $scope->enterTrait($sfTraitReflection), + 'enterTrait(Holder)' => static fn () => $scope->enterTrait($sfHolderReflection), + 'enterClassMethod' => static fn () => $scope->enterClassMethod($sfProbeClassMethod, $sfEmptyTemplateTypeMap, ['a' => new \PHPStan\Type\IntegerType(), 'b' => new \PHPStan\Type\StringType(), 'd' => new \PHPStan\Type\StaticType($sfHolderReflection)], new \PHPStan\Type\StaticType($sfHolderReflection), new \PHPStan\Type\ObjectType(\Throwable::class), 'gone', true, false, false), + 'enterClassMethod(conditional)' => static function () use ($scope, $side, $sfProbeClassMethod, $sfEmptyTemplateTypeMap) { + // the conditional wrapper is each side's own class (the native + // enterFunctionLike() tests `instanceof ConditionalTypeForParameter` + // against the native class, which under the prefix a PHP twin is + // not); its inner types stay PHP ones — they meet the parameter's + // own (PHP) type in TypeCombinator::intersect() + $conditional = $side === 'native' + ? new \PHPStanTurbo\ConditionalTypeForParameter('$e', new \PHPStanTurbo\StringType(), new \PHPStanTurbo\IntegerType(), new \PHPStanTurbo\NullType(), false) + : new \PHPStan\Type\ConditionalTypeForParameter('$e', new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType(), new \PHPStan\Type\NullType(), false); + $string = $side === 'native' ? new \PHPStanTurbo\StringType() : new \PHPStan\Type\StringType(); + return $scope->enterClassMethod($sfProbeClassMethod, $sfEmptyTemplateTypeMap, ['d' => $conditional, 'e' => $string], null, null, null, false, false, false); + }, + 'enterClassMethod(conditional, negated)' => static function () use ($scope, $side, $sfProbeClassMethod, $sfEmptyTemplateTypeMap) { + $conditional = $side === 'native' + ? new \PHPStanTurbo\ConditionalTypeForParameter('$e', new \PHPStanTurbo\StringType(), new \PHPStanTurbo\IntegerType(), new \PHPStanTurbo\NullType(), true) + : new \PHPStan\Type\ConditionalTypeForParameter('$e', new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType(), new \PHPStan\Type\NullType(), true); + $string = $side === 'native' ? new \PHPStanTurbo\StringType() : new \PHPStan\Type\StringType(); + return $scope->enterClassMethod($sfProbeClassMethod, $sfEmptyTemplateTypeMap, ['d' => $conditional, 'e' => $string], null, null, null, false, false, false); + }, + 'enterClassMethod(conditional, unknown target)' => static function () use ($scope, $side, $sfProbeClassMethod, $sfEmptyTemplateTypeMap) { + $conditional = $side === 'native' + ? new \PHPStanTurbo\ConditionalTypeForParameter('$nope', new \PHPStanTurbo\StringType(), new \PHPStanTurbo\IntegerType(), new \PHPStanTurbo\NullType(), false) + : new \PHPStan\Type\ConditionalTypeForParameter('$nope', new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType(), new \PHPStan\Type\NullType(), false); + $string = $side === 'native' ? new \PHPStanTurbo\StringType() : new \PHPStan\Type\StringType(); + return $scope->enterClassMethod($sfProbeClassMethod, $sfEmptyTemplateTypeMap, ['d' => $conditional, 'e' => $string], null, null, null, false, false, false); + }, + 'enterClassMethod(static)' => static fn () => $scope->enterClassMethod($sfProbeStaticClassMethod, $sfEmptyTemplateTypeMap, [], null, null, null, false, true, true, true, false, null, new \PHPStan\Type\ObjectWithoutClassType(), 'doc', ['a' => new \PHPStan\Type\IntegerType()], ['a' => true], [], true), + 'enterFunction' => static fn () => $scope->enterFunction($sfProbeFunction, $sfEmptyTemplateTypeMap, ['a' => new \PHPStan\Type\IntegerType()], new \PHPStan\Type\StringType(), null, null, false, false), + 'enterPropertyHook(get)' => static fn () => $scope->enterPropertyHook($sfProbeGetHook, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), [], null, null, false, null, null), + 'enterPropertyHook(set)' => static fn () => $scope->enterPropertyHook($sfProbeSetHook, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), [], null, 'gone', true, false, 'doc'), + 'enterPropertyHook(set, param)' => static fn () => $scope->enterPropertyHook($sfProbeSetHookWithParam, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), [], null, null, false, true, null), + 'enterPropertyHook(set, param typed)' => static fn () => $scope->enterPropertyHook($sfProbeSetHookWithParam, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), ['v' => new \PHPStan\Type\Constant\ConstantStringType('x')], null, null, false, null, null), + 'enterPropertyHook(get, no type)' => static fn () => $scope->enterPropertyHook($sfProbeGetHook, 'name', null, null, [], null, null, false, true, null), + 'enterPropertyHook(nope)' => static fn () => $scope->enterPropertyHook($sfProbeUnknownHook, 'name', null, null, [], null, null, false, null, null), + 'enterNamespace(Foo)' => static fn () => $scope->enterNamespace('Foo\\Bar'), + 'enterNamespace()' => static fn () => $scope->enterNamespace(''), + 'enterClosureBind(null)' => static fn () => $scope->enterClosureBind(null, null, []), + 'enterClosureBind(static)' => static fn () => $scope->enterClosureBind(new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), new \PHPStan\Type\ObjectWithoutClassType(), ['static']), + 'enterClosureBind(Holder)' => static fn () => $scope->enterClosureBind(new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), null, ['ScopeFamilyFixture\\Holder', 'Other']), + 'restoreOriginalScopeAfterClosureBind(other)' => static fn () => $scope->restoreOriginalScopeAfterClosureBind($other), + 'restoreOriginalScopeAfterClosureBind(this)' => static fn () => $scope->restoreOriginalScopeAfterClosureBind($scope), + 'restoreThis(other)' => static fn () => $scope->restoreThis($other), + 'restoreThis(this)' => static fn () => $scope->restoreThis($scope), + 'enterClosureCall' => static fn () => $scope->enterClosureCall(new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Holder::class), new \PHPStan\Type\ObjectType(\ScopeFamilyFixture\Custom::class)), + 'withClosureBindScopeClasses' => static fn () => $scope->withClosureBindScopeClasses(['ScopeFamilyFixture\\Holder']), + 'withClosureBindScopeClasses([])' => static fn () => $scope->withClosureBindScopeClasses([]), + ]; + if ($sfTracksPhpVersionId) { + // the variadic parameter shapes (see getFunctionType above) + $producers['enterClassMethod(variadic)'] = static fn () => $scope->enterClassMethod($sfProbeVariadicClassMethod, $sfEmptyTemplateTypeMap, [], null, null, null, false, false, false); + } + foreach ($producers as $label => $fn) { + $before = count($factory->calls); + $observe($label, static function () use ($fn, $scope, $dummy) { + $result = $fn(); + return $result === $scope ? 'this' : ($result === $dummy ? 'factory result' : $result); + }); + // a producer the barrier cut short never reached its create() call: + // its argument lists are that same barrier hit, not a difference + $barrier = $sfObservations[$side][$sfId][$label] === Harness::BARRIER; + $calls = $barrier ? Harness::BARRIER : array_slice($factory->calls, $before); + $observe("$label calls", static fn () => $calls); + $callbackFactoryCalls = $barrier ? Harness::BARRIER : ($factory->nodeCallbackScopeFactory?->calls ?? []); + $observe("$label callback-factory calls", static fn () => $callbackFactoryCalls); + } + + // ---- the anonymous/arrow-function entries, the + // assignment and invalidation family, the specification machinery. + // Their results are observed as the resulting scope's state (a + // digest): invalidateExpression() & co. answer with a + // ScopeOps::scopeWith() clone that never reaches the factory, and a + // raw scope normalizes to its object id. Every run is undone on + // $dummy afterwards - the in-place specification writes into it. + $scopeClass = $side === 'native' ? \PHPStanTurbo\MutatingScope::class : \PHPStan\Analyser\MutatingScope::class; + $type = static fn (string $phpClass, mixed ...$args): \PHPStan\Type\Type => new ($side === 'native' ? 'PHPStanTurbo\\' . substr($phpClass, strrpos($phpClass, '\\') + 1) : $phpClass)(...$args); + $yes = \PHPStan\TrinaryLogic::createYes(); + $maybe = \PHPStan\TrinaryLogic::createMaybe(); + $int = $type(\PHPStan\Type\IntegerType::class); + $string = $type(\PHPStan\Type\StringType::class); + $never = $type(\PHPStan\Type\NeverType::class); + $thisVar = new \PhpParser\Node\Expr\Variable('this'); + $assignVar = new \PhpParser\Node\Expr\Variable('assignedHere'); + $propFetch = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'name'); + // Holder::$inner is private: the only fetch isPrivatePropertyOfDifferentClass() gets past its visibility guard + $privatePropFetch = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'inner'); + // $a carries a tracked method call in the per-side tables: + // assignExpression() on a property of it invalidates that call + $trackedReceiverPropFetch = new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('a'), 'p'); + // untracked over $arr, whose native flavour is array and + // whose phpdoc one array: the readers that pick a + // flavour answer differently here + $differingDimFetch = new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('arr'), new \PhpParser\Node\Expr\Variable('i')); + $staticPropFetch = new \PhpParser\Node\Expr\StaticPropertyFetch(new \PhpParser\Node\Name('Holder'), 'shared'); + $dimFetch = new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('items'), new \PhpParser\Node\Scalar\Int_(0)); + $iteratee = new \PhpParser\Node\Expr\Variable('items'); + $arrayType = $type(\PHPStan\Type\ArrayType::class, $type(\PHPStan\Type\IntegerType::class), $type(\PHPStan\Type\StringType::class)); + $chainingRuns = [ + 'intersectButNotNever(int, string)' => static fn () => ($scopeClass . '::intersectButNotNever')($int, $string), + 'intersectButNotNever(int, int)' => static fn () => ($scopeClass . '::intersectButNotNever')($int, $int), + 'intersectButNotNever(?int, int)' => static fn () => ($scopeClass . '::intersectButNotNever')((($side === 'native' ? 'PHPStanTurbo\\TypeCombinator' : \PHPStan\Type\TypeCombinator::class) . '::addNull')($int), $int), + 'enterExpressionAssign' => static fn () => $scope->enterExpressionAssign($propFetch), + 'enterExpressionAssign(not plain)' => static fn () => $scope->enterExpressionAssign($propFetch, false), + 'exitExpressionAssign' => static fn () => $scope->exitExpressionAssign($propFetch), + 'setAllowedUndefinedExpression' => static fn () => $scope->setAllowedUndefinedExpression($dimFetch), + 'setAllowedUndefinedExpression(static prop)' => static fn () => $scope->setAllowedUndefinedExpression($staticPropFetch), + 'unsetAllowedUndefinedExpression' => static fn () => $scope->unsetAllowedUndefinedExpression($dimFetch), + 'specifyExpressionType(var, yes)' => static fn () => $scope->specifyExpressionType($assignVar, $int, $int, $yes), + 'specifyExpressionType(var, maybe)' => static fn () => $scope->specifyExpressionType($assignVar, $int, $string, $maybe), + 'specifyExpressionType(scalar noop)' => static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Scalar\Int_(1), $int, $int, $yes), + 'specifyExpressionType(null noop)' => static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null')), $int, $int, $yes), + 'specifyExpressionType(is_file false noop)' => static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('is_file'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('x'))]), $type(\PHPStan\Type\Constant\ConstantBooleanType::class, false), $type(\PHPStan\Type\Constant\ConstantBooleanType::class, false), $yes), + 'specifyExpressionType(alwaysRemembered)' => static fn () => $scope->specifyExpressionType(new \PHPStan\Node\Expr\AlwaysRememberedExpr($assignVar, $int, $int), $int, $int, $yes), + 'assignExpression(var)' => static fn () => $scope->assignExpression($assignVar, $int, $string), + 'assignExpression(this)' => static fn () => $scope->assignExpression($thisVar, $int, $int), + 'assignExpression(property)' => static fn () => $scope->assignExpression($propFetch, $int, $int), + 'assignExpression(static property)' => static fn () => $scope->assignExpression($staticPropFetch, $int, $int), + 'assignExpression(dim fetch)' => static fn () => $scope->assignExpression($dimFetch, $int, $int), + 'assignExpression(property of tracked receiver)' => static fn () => $scope->assignExpression($trackedReceiverPropFetch, $int, $int), + 'assignVariable(yes)' => static fn () => $scope->assignVariable('assignedHere', $int, $string, $yes), + 'assignVariable(maybe)' => static fn () => $scope->assignVariable('assignedHere', $int, $string, $maybe), + 'assignVariable(this)' => static fn () => $scope->assignVariable('this', $int, $int, $yes), + 'assignVariable(propagated)' => static fn () => $scope->assignVariable('assignedHere', $int, $int, $yes, ['assignedHere', 'other']), + // TypeUtils::findThisType() tests `instanceof ThisType` against + // each side's own class, so the probe's $this type must be that + // side's too (the walk's own type is a PHP one on both sides) + 'assignInitializedProperty(this)' => static fn () => $scope->assignInitializedProperty($type(\PHPStan\Type\ThisType::class, $sfHolderReflection), 'name'), + 'assignInitializedProperty(unknown property)' => static fn () => $scope->assignInitializedProperty($type(\PHPStan\Type\ThisType::class, $sfHolderReflection), 'neverDeclared'), + 'assignInitializedProperty(int)' => static fn () => $scope->assignInitializedProperty($int, 'name'), + 'invalidateExpression(var)' => static fn () => $scope->invalidateExpression($thisVar), + 'invalidateExpression(var, more characters)' => static fn () => $scope->invalidateExpression($thisVar, true), + 'invalidateExpression(var, keep property fetches)' => static fn () => $scope->invalidateExpression($thisVar, false, null, true), + 'invalidateExpression(property)' => static fn () => $scope->invalidateExpression($propFetch), + 'invalidateExpression(property, invalidating class)' => static fn () => $scope->invalidateExpression($propFetch, false, $sfHolderReflection), + 'invalidateExpression(unknown)' => static fn () => $scope->invalidateExpression(new \PhpParser\Node\Expr\Variable('neverTracked')), + 'isPrivatePropertyOfDifferentClass(property)' => static fn () => $scope->isPrivatePropertyOfDifferentClass($propFetch, $sfHolderReflection), + 'isPrivatePropertyOfDifferentClass(static property)' => static fn () => $scope->isPrivatePropertyOfDifferentClass($staticPropFetch, $sfCustomReflection), + 'isPrivatePropertyOfDifferentClass(var)' => static fn () => $scope->isPrivatePropertyOfDifferentClass($thisVar, $sfHolderReflection), + 'isPrivatePropertyOfDifferentClass(private, same class)' => static fn () => $scope->isPrivatePropertyOfDifferentClass($privatePropFetch, $sfHolderReflection), + 'isPrivatePropertyOfDifferentClass(private, other class)' => static fn () => $scope->isPrivatePropertyOfDifferentClass($privatePropFetch, $sfCustomReflection), + 'addTypeToExpression(var)' => static fn () => $scope->addTypeToExpression($assignVar, $int), + 'addTypeToExpression(this)' => static fn () => $scope->addTypeToExpression($thisVar, $int), + // the per-side tables track $b and $arr with a wider native + // flavour: the readers that pick one are blind over equal tables + 'addTypeToExpression(differing flavours)' => static fn () => $scope->addTypeToExpression($differingDimFetch, $string), + 'removeTypeFromExpression(differing flavours)' => static fn () => $scope->removeTypeFromExpression($differingDimFetch, $int), + 'specifyExpressionType(differing flavours)' => static fn () => $scope->specifyExpressionType($differingDimFetch, $string, $int, $yes), + 'removeTypeFromExpression(var)' => static fn () => $scope->removeTypeFromExpression($assignVar, $string), + 'removeTypeFromExpression(never)' => static fn () => $scope->removeTypeFromExpression($assignVar, $never), + 'enterCatchType(null)' => static fn () => $scope->enterCatchType($type(\PHPStan\Type\ObjectType::class, \Throwable::class), null), + 'enterCatchType(e)' => static fn () => $scope->enterCatchType($type(\PHPStan\Type\ObjectType::class, \LogicException::class), 'e'), + 'enterCatchType(not throwable)' => static fn () => $scope->enterCatchType($type(\PHPStan\Type\ObjectType::class, \stdClass::class), 'e'), + 'enterCatchType(interface)' => static fn () => $scope->enterCatchType($type(\PHPStan\Type\ObjectType::class, \Countable::class), 'e'), + 'enterMatch(variable cond)' => static fn () => $scope->enterMatch(new \PhpParser\Node\Expr\Match_(new \PhpParser\Node\Expr\Variable('x'), [new \PhpParser\Node\MatchArm(null, new \PhpParser\Node\Scalar\Int_(1))]), $int, $int), + 'enterMatch(scalar cond)' => static fn () => $scope->enterMatch(new \PhpParser\Node\Expr\Match_(new \PhpParser\Node\Scalar\Int_(2), [new \PhpParser\Node\MatchArm(null, new \PhpParser\Node\Scalar\Int_(1))]), $int, $int), + 'enterMatch(call cond)' => static fn () => $scope->enterMatch(new \PhpParser\Node\Expr\Match_(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('strlen'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('x'))]), [new \PhpParser\Node\MatchArm(null, new \PhpParser\Node\Scalar\Int_(1))]), $int, $string), + 'enterMatch(remembered cond)' => static fn () => $scope->enterMatch(new \PhpParser\Node\Expr\Match_(new \PHPStan\Node\Expr\AlwaysRememberedExpr(new \PhpParser\Node\Expr\Variable('y'), $int, $int), [new \PhpParser\Node\MatchArm(null, new \PhpParser\Node\Scalar\Int_(1))]), $int, $int), + 'enterMatch(cond node)' => static function () use ($scope, $int, $string, $sfHarness) { + $matchNode = new \PhpParser\Node\Expr\Match_(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('strlen'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_('x'))]), [new \PhpParser\Node\MatchArm(null, new \PhpParser\Node\Scalar\Int_(1))]); + $scope->enterMatch($matchNode, $int, $string); + + return [$sfHarness->className($matchNode->cond), $sfHarness->key($matchNode->cond)]; + }, + 'enterForeachKey' => static fn () => $scope->enterForeachKey($other, $iteratee, $arrayType, $arrayType, 'k'), + 'enterForeachKey(not array)' => static fn () => $scope->enterForeachKey($other, $iteratee, $string, $string, 'k'), + 'enterForeach' => static fn () => $scope->enterForeach($other, $iteratee, $arrayType, $arrayType, 'v', null, false), + 'enterForeach(key)' => static fn () => $scope->enterForeach($other, $iteratee, $arrayType, $arrayType, 'v', 'k', false), + 'enterForeach(by ref)' => static fn () => $scope->enterForeach($other, $iteratee, $arrayType, $arrayType, 'v', null, true), + 'enterForeach(by ref, key)' => static fn () => $scope->enterForeach($other, $iteratee, $arrayType, $arrayType, 'v', 'k', true), + 'enterForeach(constant array by ref)' => static fn () => $scope->enterForeach($other, $iteratee, \PHPStan\Type\Constant\ConstantArrayTypeBuilder::createEmpty()->getArray(), $arrayType, 'v', 'k', true), + 'enterAnonymousFunctionWithoutReflection' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeClosure, null, null), + 'enterAnonymousFunctionWithoutReflection(static)' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeStaticClosure, null, null), + 'enterAnonymousFunctionWithoutReflection(callable params)' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeClosure, [$sfPushParameter], [$sfPushParameter]), + 'enterAnonymousFunctionWithoutReflection(empty callable params)' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeClosure, [], []), + 'enterAnonymousFunctionWithoutReflection(untyped, variadic tail)' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeUntypedClosure, [$sfPushVariadicParameter], null), + 'enterAnonymousFunctionWithoutReflection(untyped, no callable params)' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeUntypedClosure, [], null), + 'enterAnonymousFunctionWithoutReflection(untyped variadic param)' => static fn () => $scope->enterAnonymousFunctionWithoutReflection($sfProbeUntypedVariadicClosure, [$sfPushVariadicParameter], null), + 'enterArrowFunctionWithoutReflection' => static fn () => $scope->enterArrowFunctionWithoutReflection($sfProbeArrowFunction, null, null), + 'enterArrowFunctionWithoutReflection(static)' => static fn () => $scope->enterArrowFunctionWithoutReflection($sfProbeStaticArrowFunction, null, null), + 'enterArrowFunctionWithoutReflection(callable params)' => static fn () => $scope->enterArrowFunctionWithoutReflection($sfProbeArrowFunction, [$sfPushParameter], null), + 'enterArrowFunctionWithoutReflection(variadic callable params)' => static fn () => $scope->enterArrowFunctionWithoutReflection($sfProbeVariadicArrowFunction, [$sfPushParameter, $sfPushVariadicParameter], null), + // the ClosureTypeResolver's MutatingScope parameter is the real + // class name: these two cross the prefix type barrier + 'enterAnonymousFunction' => static fn () => $scope->enterAnonymousFunction($sfProbeClosure, null), + 'enterArrowFunction' => static fn () => $scope->enterArrowFunction($sfProbeArrowFunction, null), + ]; + // the ArrayDimFetch arm of specifyExpressionTypeInPlace() tests the + // dim and var types against each side's own classes - only the + // scope whose tables are built per side can answer them that way + if ($scope->hasExpressionType(new \PhpParser\Node\Expr\Variable('i'))->yes()) { + $chainingRuns['specifyExpressionType(dim fetch, int dim)'] = static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('arr'), new \PhpParser\Node\Expr\Variable('i')), $string, $string, $yes); + $chainingRuns['specifyExpressionType(dim fetch, string dim)'] = static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('arr'), new \PhpParser\Node\Expr\Variable('k')), $string, $string, $yes); + $chainingRuns['specifyExpressionType(dim fetch, mixed var)'] = static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('m'), new \PhpParser\Node\Expr\Variable('i')), $string, $string, $yes); + $chainingRuns['specifyExpressionType(dim fetch, string var)'] = static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('b'), new \PhpParser\Node\Expr\Variable('i')), $string, $string, $yes); + $chainingRuns['specifyExpressionType(dim fetch, inc dim)'] = static fn () => $scope->specifyExpressionType(new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('arr'), new \PhpParser\Node\Expr\PreInc(new \PhpParser\Node\Expr\Variable('i'))), $string, $string, $yes); + } + + // ---- the narrowing application, the conditional-expression + // bookkeeping and the scope merges. The same digest observable + // as the chaining runs, but every run is undone on the scope + // under test as well: the batch's conditional bookkeeping + // (processConditionalExpressionsAfterSpecifying) writes into the + // scope it was applied on when no derivation intervened. + $sfKey = static fn (\PhpParser\Node\Expr $expr): string => $sfHarness->key($expr); + $specified = static fn (array $sure = [], array $sureNot = []): \PHPStan\Analyser\SpecifiedTypes => new \PHPStan\Analyser\SpecifiedTypes($sure, $sureNot); + // the alternative-form entries only SpecifiedTypes::intersectWith() produces + $withAlternatives = static function (\PHPStan\Analyser\SpecifiedTypes $types, array $alternatives): \PHPStan\Analyser\SpecifiedTypes { + $clone = clone $types; + (new \ReflectionProperty(\PHPStan\Analyser\SpecifiedTypes::class, 'alternativeTypes'))->setValue($clone, $alternatives); + + return $clone; + }; + // PropertyInitializationExpr is the one Expr class with no ExprHandler: + // TypeSpecifier::specifyTypesInCondition() then takes the default + // narrowing on both sides (with a handler it dispatches + // specifyTypesOfNewWorldHandlerNode() only on a real MutatingScope, + // which the prefixed native class is not). A first-class callable + // returns before the scope is consulted at all. + // Only the truthy direction is comparable: its `mixed minus falsey()` + // goes through MixedType::subtract(), while the falsey direction's + // `mixed minus truthy()` asks the TypeCombinator to decompose a PHP + // union, which the native one (the native scope's) treats as atomic. + // A native truthy/falsey mix-up still shows: it would answer never. + $initializationExpr = new \PHPStan\Node\Expr\PropertyInitializationExpr('p'); + $firstClassCallable = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('strlen'), [new \PhpParser\Node\VariadicPlaceholder()]); + $issetExpr = new \PHPStan\Node\IssetExpr($assignVar); + $issetTrackedExpr = new \PHPStan\Node\IssetExpr($thisVar); + $byRefUseVar = new \PhpParser\Node\Expr\Variable('byRefUse'); + $conditionalHolder = static function (string $conditionKey, \PhpParser\Node\Expr $conditionExpr, \PHPStan\Type\Type $conditionType, \PhpParser\Node\Expr $targetExpr, \PHPStan\Type\Type $targetType) use ($side, $sfHarness): object { + $holder = new \PHPStan\Analyser\ConditionalExpressionHolder( + [$conditionKey => \PHPStan\Analyser\ExpressionTypeHolder::createYes($conditionExpr, $conditionType)], + \PHPStan\Analyser\ExpressionTypeHolder::createYes($targetExpr, $targetType), + ); + + return $side === 'native' ? $sfHarness->nativeConditionalExpressions(['x' => ['k' => $holder]])['x']['k'] : $holder; + }; + $newConditionalHolder = $conditionalHolder('$byRefUse', $byRefUseVar, $int, $assignVar, $string); + $narrowingRuns = [ + 'filterByTruthyValue(property initialization)' => static fn () => $scope->filterByTruthyValue($initializationExpr), + 'filterByTruthyValue(first-class callable)' => static fn () => $scope->filterByTruthyValue($firstClassCallable), + 'filterByFalseyValue(first-class callable)' => static fn () => $scope->filterByFalseyValue($firstClassCallable), + 'applySpecifiedTypes(empty)' => static fn () => $scope->applySpecifiedTypes($specified()), + 'applySpecifiedTypes(sure, untracked variable)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($assignVar) => [$assignVar, $int]])), + 'applySpecifiedTypes(sure, this)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($thisVar) => [$thisVar, $int]])), + 'applySpecifiedTypes(sure, dim fetch)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($dimFetch) => [$dimFetch, $string]])), + 'applySpecifiedTypes(sure not, this)' => static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($thisVar) => [$thisVar, $string]])), + 'applySpecifiedTypes(sure not, untracked variable)' => static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($assignVar) => [$assignVar, $string]])), + 'applySpecifiedTypes(sure not, never)' => static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($thisVar) => [$thisVar, $never]])), + 'applySpecifiedTypes(overwrite)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($assignVar) => [$assignVar, $int]])->setAlwaysOverwriteTypes()), + 'applySpecifiedTypes(overwrite, tracked)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($thisVar) => [$thisVar, $int]])->setAlwaysOverwriteTypes()), + // the three expression shapes the batch never specifies, and the + // unary minus of something else, which it does. Overwriting: the + // unary minus resolves to a union of the walk's (PHP) types, + // which a narrowing intersection would hand to the native + // TypeCombinator as an atom + 'applySpecifiedTypes(scalar, array, unary minus)' => static fn () => $scope->applySpecifiedTypes($specified([ + '1' => [new \PhpParser\Node\Scalar\Int_(1), $int], + '[]' => [new \PhpParser\Node\Expr\Array_([]), $int], + '-1' => [new \PhpParser\Node\Expr\UnaryMinus(new \PhpParser\Node\Scalar\Int_(1)), $int], + '-$assignedHere' => [new \PhpParser\Node\Expr\UnaryMinus($assignVar), $int], + ])->setAlwaysOverwriteTypes()), + 'applySpecifiedTypes(isset, sure)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($issetExpr) => [$issetExpr, $int]])), + 'applySpecifiedTypes(isset, sure not)' => static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($issetExpr) => [$issetExpr, $int]])), + 'applySpecifiedTypes(isset, tracked, sure)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($issetTrackedExpr) => [$issetTrackedExpr, $int]])), + 'applySpecifiedTypes(isset, tracked, sure not)' => static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($issetTrackedExpr) => [$issetTrackedExpr, $int]])), + 'applySpecifiedTypes(alternative, sure terms)' => static fn () => $scope->applySpecifiedTypes($withAlternatives($specified(), [$sfKey($thisVar) => [$thisVar, [[$int, null], [$string, null]]]])), + 'applySpecifiedTypes(alternative, current minus)' => static fn () => $scope->applySpecifiedTypes($withAlternatives($specified(), [$sfKey($thisVar) => [$thisVar, [[null, $string]]]])), + 'applySpecifiedTypes(alternative, untracked current)' => static fn () => $scope->applySpecifiedTypes($withAlternatives($specified(), [$sfKey($assignVar) => [$assignVar, [[null, $string]]]])), + 'applySpecifiedTypes(alternative, mixed terms)' => static fn () => $scope->applySpecifiedTypes($withAlternatives($specified(), [$sfKey($thisVar) => [$thisVar, [[$int, $string], [null, null]]]])), + 'applySpecifiedTypes(new conditional holders)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($assignVar) => [$assignVar, $int]])->setNewConditionalExpressionHolders(['$assignedHere' => ['k' => $newConditionalHolder]])), + 'applySpecifiedTypes(recipe)' => static fn () => $scope->applySpecifiedTypes($specified()->setConditionalExpressionHolderRecipes([new \ScopeFamily\TestRecipe(['$assignedHere' => ['k' => $newConditionalHolder]])])), + 'applySpecifiedTypes(recipe over existing key)' => static fn () => $scope->applySpecifiedTypes($specified() + ->setNewConditionalExpressionHolders(['$assignedHere' => ['k' => $newConditionalHolder]]) + ->setConditionalExpressionHolderRecipes([new \ScopeFamily\TestRecipe(['$assignedHere' => ['other' => $newConditionalHolder]])])), + 'applySpecifiedTypes(deferred augment)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($thisVar) => [$thisVar, $int]])->withDeferredAugment(new \ScopeFamily\TestAugment($specified([$sfKey($assignVar) => [$assignVar, $string]])))), + 'applySpecifiedTypes(deferred augment, null)' => static fn () => $scope->applySpecifiedTypes($specified()->withDeferredAugment(new \ScopeFamily\TestAugment(null))), + 'applySpecifiedTypes(nested deferred augment)' => static fn () => $scope->applySpecifiedTypes($specified()->withDeferredAugment( + new \ScopeFamily\TestAugment($specified([$sfKey($assignVar) => [$assignVar, $string]])->withDeferredAugment(new \ScopeFamily\TestAugment($specified([$sfKey($dimFetch) => [$dimFetch, $int]])))), + )), + // matches the conditional expressions the conditional scope carries + 'applySpecifiedTypes(matching conditional)' => static fn () => $scope->applySpecifiedTypes($specified([$sfKey($byRefUseVar) => [$byRefUseVar, $int]])), + 'applySpecifiedTypes(matching conditional, sure not)' => static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($byRefUseVar) => [$byRefUseVar, $string]])), + 'addConditionalExpressions(new key)' => static fn () => $scope->addConditionalExpressions('$freshKey', [$newConditionalHolder]), + 'addConditionalExpressions(existing key)' => static fn () => $scope->addConditionalExpressions('$p', [$newConditionalHolder]), + 'addConditionalExpressions(empty)' => static fn () => $scope->addConditionalExpressions('$freshKey', []), + 'exitFirstLevelStatements' => static function () use ($scope, $sfHarness, $dummy) { + $first = $scope->exitFirstLevelStatements(); + $second = $scope->exitFirstLevelStatements(); + + return [ + 'memoized' => $first === $second, + 'is this' => $first === $scope, + 'class' => $sfHarness->className($first), + 'first level' => $first->isInFirstLevelStatement(), + 'memo' => array_keys($first->resolvedTypes), + ]; + }, + 'mergeWith(null)' => static fn () => $scope->mergeWith(null), + 'mergeWith(this)' => static fn () => $scope->mergeWith($scope), + 'mergeWith(other)' => static fn () => $scope->mergeWith($other), + 'mergeWith(other, preserve vacuous)' => static fn () => $scope->mergeWith($other, true), + 'mergeWith(other with constraints)' => static fn () => $scope->mergeWith($other->withTemplateArgumentConstraints($sfNonEmptyConstraints)), + 'mergeInitializedProperties(this)' => static fn () => $scope->mergeInitializedProperties($scope), + 'mergeInitializedProperties(other)' => static fn () => $scope->mergeInitializedProperties($other), + 'processFinallyScope(other, this)' => static fn () => $scope->processFinallyScope($other, $scope), + 'processFinallyScope(this, other)' => static fn () => $scope->processFinallyScope($scope, $other), + 'processFinallyScope(other, other)' => static fn () => $scope->processFinallyScope($other, $other), + ]; + // the merge scopes carry conditional expressions guarded on $g: a + // specification that matches their guard drives + // processConditionalExpressionsAfterSpecifying() + $gVar = new \PhpParser\Node\Expr\Variable('g'); + if ($scope->hasExpressionType($gVar)->yes()) { + // the batch entry is keyed on the guard but carries a `null` + // ConstFetch, whose specification is a no-op: the batch then + // never derives a scope, so the conditional bookkeeping runs on + // the scope under test itself (an unpublished working copy is + // the PHP class on both sides - the factory's return type names + // it - and would take the native body's private call by name) + $noopExpr = new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null')); + $narrowingRuns['applySpecifiedTypes(conditional guard)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($gVar) => [$noopExpr, $scope->getTrackedExpressionType($gVar)]])); + // a narrower specification: only the supertype pass matches + $narrowingRuns['applySpecifiedTypes(conditional guard, supertype)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($gVar) => [$noopExpr, $type(\PHPStan\Type\Constant\ConstantIntegerType::class, 3)]])); + $narrowingRuns['applySpecifiedTypes(conditional guard, sure not)'] = static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($gVar) => [$noopExpr, $type(\PHPStan\Type\Constant\ConstantIntegerType::class, 3)]])); + // the batch's holders join the scope's existing entry for the + // same expression rather than replacing it + $narrowingRuns['applySpecifiedTypes(new holders over existing conditionals)'] = static fn () => $scope->applySpecifiedTypes($specified()->setNewConditionalExpressionHolders(['$t2' => ['k' => $newConditionalHolder]])); + } + // the narrowing that really combines types can only be compared over + // the per-side tables: a type of this side meeting the walk's (PHP) + // one goes through the native TypeCombinator, which treats a foreign + // class as atomic + if ($scope->hasExpressionType(new \PhpParser\Node\Expr\Variable('i'))->yes()) { + $aVar = new \PhpParser\Node\Expr\Variable('a'); + $bVar = new \PhpParser\Node\Expr\Variable('b'); + $mVar = new \PhpParser\Node\Expr\Variable('m'); + $arrVar = new \PhpParser\Node\Expr\Variable('arr'); + $narrowingRuns['applySpecifiedTypes(sure, tracked int)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($aVar) => [$aVar, $int]])); + $narrowingRuns['applySpecifiedTypes(sure, tracked int against string)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($aVar) => [$aVar, $string]])); + $narrowingRuns['applySpecifiedTypes(sure, tracked mixed)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($mVar) => [$mVar, $string]])); + $narrowingRuns['applySpecifiedTypes(sure not, tracked mixed)'] = static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($mVar) => [$mVar, $string]])); + $narrowingRuns['applySpecifiedTypes(sure not, tracked int)'] = static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($aVar) => [$aVar, $string]])); + // $b and $arr are wider in the native table than in the phpdoc one + $narrowingRuns['applySpecifiedTypes(sure, differing flavours)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($bVar) => [$bVar, $string]])); + $narrowingRuns['applySpecifiedTypes(sure not, differing flavours)'] = static fn () => $scope->applySpecifiedTypes($specified([], [$sfKey($arrVar) => [$arrVar, $arrayType]])); + $narrowingRuns['applySpecifiedTypes(overwrite, tracked)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($aVar) => [$aVar, $string]])->setAlwaysOverwriteTypes()); + // the usort(): shorter keys first, sure before sure-not + $narrowingRuns['applySpecifiedTypes(sort order)'] = static fn () => $scope->applySpecifiedTypes($specified( + [$sfKey($aVar) => [$aVar, $int], $sfKey($mVar) => [$mVar, $string]], + [$sfKey($bVar) => [$bVar, $string], $sfKey($arrVar) => [$arrVar, $arrayType]], + )); + $narrowingRuns['applySpecifiedTypes(alternative, tracked terms)'] = static fn () => $scope->applySpecifiedTypes($withAlternatives($specified(), [$sfKey($mVar) => [$mVar, [[$int, null], [null, $string]]]])); + // the native flavour of an alternative entry reads the native current type + $narrowingRuns['applySpecifiedTypes(alternative, differing flavours)'] = static fn () => $scope->applySpecifiedTypes($withAlternatives($specified(), [$sfKey($arrVar) => [$arrVar, [[null, $string]]]])); + // only the native table tracks $nativeOnly: the current-type + // fallback must leave that flavour alone + $nativeOnlyVar = new \PhpParser\Node\Expr\Variable('nativeOnly'); + $narrowingRuns['applySpecifiedTypes(sure, native-only tracked)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey($nativeOnlyVar) => [$nativeOnlyVar, $int]])); + // the batch sort's tie-break: at equal key length a sure + // specification runs before a sure-not one, so the unset of + // $m happens after it was narrowed + $narrowingRuns['applySpecifiedTypes(sure before sure not at equal length)'] = static fn () => $scope->applySpecifiedTypes($specified( + [$sfKey($mVar) => [$mVar, $string]], + ['ZZ' => [new \PHPStan\Node\IssetExpr($mVar), $int]], + )); + $narrowingRuns['applySpecifiedTypes(isset, tracked, sure)'] = static fn () => $scope->applySpecifiedTypes($specified([$sfKey(new \PHPStan\Node\IssetExpr($aVar)) => [new \PHPStan\Node\IssetExpr($aVar), $int]])); + } + + + // ---- the closure and loop scopes, the generalization, the scope + // comparison, the member-access queries and the remaining + // readers. Same digest observable as the two groups above. + $combinator = $side === 'native' ? 'PHPStanTurbo\\TypeCombinator' : \PHPStan\Type\TypeCombinator::class; + $union = static fn (\PHPStan\Type\Type ...$types): \PHPStan\Type\Type => ($combinator . '::union')(...$types); + // a reflection lookup's result by what it names, not by its identity + $reflectionDigest = static function (mixed $reflection) use ($sfHarness): mixed { + if (!is_object($reflection)) { + return $reflection; + } + $digest = ['RF', $sfHarness->className($reflection)]; + if (method_exists($reflection, 'getName')) { + $digest[] = $reflection->getName(); + } + if (method_exists($reflection, 'getDeclaringClass')) { + $digest[] = $reflection->getDeclaringClass()->getName(); + } + + return $digest; + }; + // the union filters of the member lookups only reach their + // `instanceof UnionType` arm over a union of THIS side's classes + $iterableUnion = $union($arrayType, $string); + $scalarUnion = $union($int, $string); + $holderUnion = $union($type(\PHPStan\Type\ObjectType::class, \ScopeFamilyFixture\Holder::class), $type(\PHPStan\Type\NullType::class)); + // an interface member answers Maybe to hasMethod()/hasProperty(): + // the filter predicate keeps only the Yes ones + $maybeUnion = $union($type(\PHPStan\Type\ObjectType::class, \ScopeFamilyFixture\Holder::class), $type(\PHPStan\Type\ObjectType::class, \Countable::class)); + $holderType = $type(\PHPStan\Type\ObjectType::class, \ScopeFamilyFixture\Holder::class); + // a use the scopes track, one they do not, and the keys of the + // generalize pair whose two types generalizeType() widens rather + // than merely unites + $byRefUses = [ + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('g'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('neverDefinedUse'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('gDeep'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('gShapeKeys'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('gArray'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('gList'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('gAccessory'), true), + new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('gBenevolent'), true), + ]; + $closureLoopRuns = [ + 'processClosureScope(no uses)' => static fn () => $scope->processClosureScope($scope, null, []), + 'processClosureScope(this, null)' => static fn () => $scope->processClosureScope($scope, null, $byRefUses), + 'processClosureScope(other, null)' => static fn () => $scope->processClosureScope($other, null, $byRefUses), + 'processClosureScope(this, other)' => static fn () => $scope->processClosureScope($scope, $other, $byRefUses), + 'processClosureScope(other, this)' => static fn () => $scope->processClosureScope($other, $scope, $byRefUses), + 'processAlwaysIterableForeachScopeWithoutPollute(this)' => static fn () => $scope->processAlwaysIterableForeachScopeWithoutPollute($scope), + 'processAlwaysIterableForeachScopeWithoutPollute(other)' => static fn () => $scope->processAlwaysIterableForeachScopeWithoutPollute($other), + 'generalizeWith(this)' => static fn () => $scope->generalizeWith($scope), + 'generalizeWith(other)' => static fn () => $scope->generalizeWith($other), + 'generalizeWith(other, nothing writable)' => static fn () => $scope->generalizeWith($other, []), + 'generalizeWith(other, writable)' => static fn () => $scope->generalizeWith($other, ['g' => true, 'a' => true, 'out' => true]), + 'generalizeWith(other, reversed)' => static fn () => $other->generalizeWith($scope), + // addTemplateArgumentConstraints() answers with $this for an + // empty (or null) set, so only a non-empty one makes + // generalizeWith()'s second half observable + 'generalizeWith(other with constraints)' => static fn () => $scope->generalizeWith($other->withTemplateArgumentConstraints($sfNonEmptyConstraints)), + 'equals(this)' => static fn () => $scope->equals($scope), + 'equals(other)' => static fn () => $scope->equals($other), + 'equals(other, reversed)' => static fn () => $other->equals($scope), + // a clone that differs from the scope in exactly one of the + // three things equals() compares + 'equals(clone)' => static fn () => $scope->equals(clone $scope), + 'equals(clone, native types only)' => static function () use ($scope, $sfRestore) { + $clone = clone $scope; + $sfRestore($clone, ['nativeExpressionTypes' => []]); + + return [$scope->equals($clone), $clone->equals($scope)]; + }, + 'equals(clone, one certainty)' => static function () use ($scope, $side, $sfProp, $sfRestore) { + $clone = clone $scope; + $types = $sfProp($clone, 'expressionTypes'); + if ($types === []) { + return 'no expression types'; + } + $key = array_key_first($types); + $holder = $types[$key]; + $weaker = $holder->getCertainty()->yes(); + $types[$key] = $side === 'native' + ? new \PHPStanTurbo\ExpressionTypeHolder($holder->getExpr(), $holder->getType(), $weaker ? \PHPStanTurbo\TrinaryLogic::createMaybe() : \PHPStanTurbo\TrinaryLogic::createYes()) + : new \PHPStan\Analyser\ExpressionTypeHolder($holder->getExpr(), $holder->getType(), $weaker ? \PHPStan\TrinaryLogic::createMaybe() : \PHPStan\TrinaryLogic::createYes()); + $sfRestore($clone, ['expressionTypes' => $types, 'nativeExpressionTypes' => $types]); + + return [$scope->equals($clone), $clone->equals($scope)]; + }, + 'equals(clone, one conditional holder fewer)' => static function () use ($scope, $sfProp, $sfRestore) { + $clone = clone $scope; + $conditionals = $sfProp($clone, 'conditionalExpressions'); + foreach ($conditionals as $key => $holders) { + if (count($holders) <= 1) { + continue; + } + array_pop($conditionals[$key]); + $sfRestore($clone, ['conditionalExpressions' => $conditionals]); + + return [$scope->equals($clone), $clone->equals($scope)]; + } + + return 'no multi-holder conditional'; + }, + 'debug' => static fn () => $scope->debug(), + 'canAccessProperty(public)' => static fn () => $scope->canAccessProperty($sfPublicProperty), + 'canAccessProperty(private, Holder)' => static fn () => $scope->canAccessProperty($sfPrivateProperty), + 'canReadProperty(public)' => static fn () => $scope->canReadProperty($sfPublicProperty), + 'canReadProperty(protected, Base)' => static fn () => $scope->canReadProperty($sfProtectedProperty), + // a protected member of a subclass: only the last arm of the + // closure (the declaring class being a subclass of the scope's) + // answers for a scope inside Base + 'canReadProperty(protected, Child)' => static fn () => $scope->canReadProperty($sfChildProtectedProperty), + 'canWriteProperty(public)' => static fn () => $scope->canWriteProperty($sfPublicProperty), + 'canWriteProperty(protected, Base)' => static fn () => $scope->canWriteProperty($sfProtectedProperty), + 'canWriteProperty(private set)' => static fn () => $scope->canWriteProperty($sfPrivateSetProperty), + 'canCallMethod(public)' => static fn () => $scope->canCallMethod($sfPublicMethod), + 'canCallMethod(protected, Base)' => static fn () => $scope->canCallMethod($sfProtectedMethod), + 'canCallMethod(protected, Child)' => static fn () => $scope->canCallMethod($sfChildProtectedMethod), + 'canCallMethod(private, Base)' => static fn () => $scope->canCallMethod($sfPrivateMethod), + 'canAccessConstant(public)' => static fn () => $scope->canAccessConstant($sfPublicConstant), + 'canAccessConstant(protected)' => static fn () => $scope->canAccessConstant($sfProtectedConstant), + 'canAccessConstant(private)' => static fn () => $scope->canAccessConstant($sfPrivateConstant), + 'canAccessConstant(protected, Child)' => static fn () => $scope->canAccessConstant($sfChildProtectedConstant), + 'filterTypeWithMethod(union, read)' => static fn () => $scope->filterTypeWithMethod($holderUnion, 'read'), + 'filterTypeWithMethod(union, nope)' => static fn () => $scope->filterTypeWithMethod($holderUnion, 'nope'), + 'filterTypeWithMethod(object, read)' => static fn () => $scope->filterTypeWithMethod($holderType, 'read'), + 'filterTypeWithMethod(object, nope)' => static fn () => $scope->filterTypeWithMethod($holderType, 'nope'), + 'filterTypeWithMethod(scalar union)' => static fn () => $scope->filterTypeWithMethod($scalarUnion, 'read'), + 'filterTypeWithMethod(maybe union)' => static fn () => $scope->filterTypeWithMethod($maybeUnion, 'read'), + 'getPropertyReflection(maybe union)' => static fn () => $reflectionDigest($scope->getPropertyReflection($maybeUnion, 'name')), + 'getInstancePropertyReflection(maybe union)' => static fn () => $reflectionDigest($scope->nativeGetInstancePropertyReflection($maybeUnion, 'name')), + 'getMethodReflection(union, read)' => static fn () => $reflectionDigest($scope->nativeGetMethodReflection($holderUnion, 'read')), + 'getMethodReflection(object, read)' => static fn () => $reflectionDigest($scope->nativeGetMethodReflection($holderType, 'read')), + 'getMethodReflection(object, nope)' => static fn () => $reflectionDigest($scope->nativeGetMethodReflection($holderType, 'nope')), + 'getNakedMethod(union, read)' => static fn () => $reflectionDigest($scope->getNakedMethod($holderUnion, 'read')), + 'getNakedMethod(object, nope)' => static fn () => $reflectionDigest($scope->getNakedMethod($holderType, 'nope')), + 'getPropertyReflection(union, name)' => static fn () => $reflectionDigest($scope->getPropertyReflection($holderUnion, 'name')), + 'getPropertyReflection(object, name)' => static fn () => $reflectionDigest($scope->getPropertyReflection($holderType, 'name')), + 'getPropertyReflection(object, nope)' => static fn () => $reflectionDigest($scope->getPropertyReflection($holderType, 'nope')), + 'getInstancePropertyReflection(union, name)' => static fn () => $reflectionDigest($scope->nativeGetInstancePropertyReflection($holderUnion, 'name')), + 'getInstancePropertyReflection(object, name)' => static fn () => $reflectionDigest($scope->nativeGetInstancePropertyReflection($holderType, 'name')), + 'getInstancePropertyReflection(object, nope)' => static fn () => $reflectionDigest($scope->nativeGetInstancePropertyReflection($holderType, 'nope')), + 'getStaticPropertyReflection(union, name)' => static fn () => $reflectionDigest($scope->nativeGetStaticPropertyReflection($holderUnion, 'name')), + 'getStaticPropertyReflection(object, nope)' => static fn () => $reflectionDigest($scope->nativeGetStaticPropertyReflection($holderType, 'nope')), + 'getConstantReflection(union, PUBLIC_CONST)' => static fn () => $reflectionDigest($scope->getConstantReflection($holderUnion, 'PUBLIC_CONST')), + 'getConstantReflection(object, nope)' => static fn () => $reflectionDigest($scope->getConstantReflection($holderType, 'NOPE_CONST')), + 'getConstantExplicitTypeFromConfig(PHP_EOL)' => static fn () => $scope->getConstantExplicitTypeFromConfig('PHP_EOL', $string), + 'getConstantExplicitTypeFromConfig(unknown)' => static fn () => $scope->getConstantExplicitTypeFromConfig('NOPE_XYZ', $int), + 'getConstantExplicitTypeFromConfig(dynamic, constant value)' => static fn () => $scope->getConstantExplicitTypeFromConfig('PHP_VERSION', $type(\PHPStan\Type\Constant\ConstantStringType::class, '8.5.0')), + 'getConstantExplicitTypeFromConfig(unknown, constant value)' => static fn () => $scope->getConstantExplicitTypeFromConfig('NOPE_XYZ', $type(\PHPStan\Type\Constant\ConstantStringType::class, '8.5.0')), + 'getIterableKeyType(union)' => static fn () => $scope->getIterableKeyType($iterableUnion), + 'getIterableValueType(union)' => static fn () => $scope->getIterableValueType($iterableUnion), + 'getIterableKeyType(non-iterable union)' => static fn () => $scope->getIterableKeyType($scalarUnion), + 'getIterableValueType(non-iterable union)' => static fn () => $scope->getIterableValueType($scalarUnion), + 'getIterableKeyType(array)' => static fn () => $scope->getIterableKeyType($arrayType), + 'getIterableValueType(array)' => static fn () => $scope->getIterableValueType($arrayType), + 'node callback' => static function () use ($scope, $sfHarness, $sfProp, $sfRestore) { + $saved = $sfProp($scope, 'nodeCallback'); + $seen = []; + $callback = static function (\PhpParser\Node $node, object $answerer) use (&$seen, $sfHarness): void { + $seen[] = [$sfHarness->className($node), $sfHarness->className($answerer)]; + }; + try { + $sfRestore($scope, ['nodeCallback' => $callback]); + $scope->invokeNodeCallback(new \PhpParser\Node\Expr\Variable('cb')); + $scope->emitCollectedData('ScopeFamily\\SomeCollector', ['x' => 1]); + + return $seen; + } finally { + $sfRestore($scope, ['nodeCallback' => $saved]); + } + }, + 'invokeNodeCallback(no callback)' => static function () use ($scope, $sfProp, $sfRestore) { + $saved = $sfProp($scope, 'nodeCallback'); + try { + $sfRestore($scope, ['nodeCallback' => null]); + $scope->invokeNodeCallback(new \PhpParser\Node\Expr\Variable('cb')); + + return 'no throw'; + } finally { + $sfRestore($scope, ['nodeCallback' => $saved]); + } + }, + 'emitCollectedData(no callback)' => static function () use ($scope, $sfProp, $sfRestore) { + $saved = $sfProp($scope, 'nodeCallback'); + try { + $sfRestore($scope, ['nodeCallback' => null]); + $scope->emitCollectedData('ScopeFamily\\SomeCollector', null); + + return 'no throw'; + } finally { + $sfRestore($scope, ['nodeCallback' => $saved]); + } + }, + ]; + + // the factory builds a real scope of this side for the chaining runs: + // the chained bodies derive scope from scope, and the twin's + // ScopeOps::scopeWith() reaches the factory (duplicateWith) where + // the native one clones — the two agree on the scope they produce, + // not on how many create() calls it took, so the resulting scope's + // state is the observable here, not the argument lists + // always the PHP-side class: InternalScopeFactory::create() is typed + // with the twin's class name, which the prefixed native class is not + // — so the native side's chain continues through PHP scopes from the + // first create() on (the same barrier toWalkScope() crosses) + $factory->builder = static function (array $createArgs) use ($args, $factory, $sfWalkScope): \PHPStan\Analyser\MutatingScope { + $ctor = $args; + $ctor['scopeFactory'] = $factory; + foreach ([ + 'context', 'declareStrictTypes', 'function', 'namespace', 'expressionTypes', 'nativeExpressionTypes', + 'conditionalExpressions', 'inClosureBindScopeClasses', 'anonymousFunctionReflection', 'inFirstLevelStatement', + 'currentlyAssignedExpressions', 'currentlyAllowedUndefinedExpressions', 'inFunctionCallsStack', + 'afterExtractCall', 'parentScope', 'nativeTypesPromoted', 'templateArgumentFrame', 'templateArgumentConstraints', + ] as $i => $name) { + $ctor[$name] = $createArgs[$i]; + } + // the one native collaborator a chaining body builds itself: the + // twin's constructor is typed with the real class name + if ($ctor['anonymousFunctionReflection'] instanceof \PHPStanTurbo\ClosureType) { + $ctor['anonymousFunctionReflection'] = new \PHPStan\Type\ClosureType(); + } + // $this as the new scope's parent: the twin's typed parameter + // rejects the prefixed class, and the walk scope stands in (the + // digest renders a parent by its class name) + if ($ctor['parentScope'] instanceof \PHPStanTurbo\MutatingScope) { + $ctor['parentScope'] = $sfWalkScope; + } + $built = new PhpScope(...array_values($ctor)); + $built->inner = $sfWalkScope; + + return $built; + }; + // the scope under test is restored too from the narrowing runs on: the + // narrowing batch's conditional bookkeeping writes into it, and + // exitFirstLevelStatements() memoizes on it + $sfRestoredScopeProps = array_merge($sfMutableProps, ['scopeOutOfFirstLevelStatement']); + try { + foreach ($chainingRuns as $label => $fn) { + $state = $sfSnapshot($dummy); + $observe($label, static fn () => $sfScopeDigest($fn(), $scope, $dummy)); + $sfRestore($dummy, $state); + } + foreach ($narrowingRuns as $label => $fn) { + $state = $sfSnapshot($dummy); + $scopeState = $sfSnapshot($scope, $sfRestoredScopeProps); + $observe($label, static fn () => $sfScopeDigest($fn(), $scope, $dummy)); + $sfRestore($dummy, $state); + $sfRestore($scope, $scopeState); + } + foreach ($closureLoopRuns as $label => $fn) { + $state = $sfSnapshot($dummy); + $scopeState = $sfSnapshot($scope, $sfRestoredScopeProps); + $observe($label, static fn () => $sfScopeDigest($fn(), $scope, $dummy)); + $sfRestore($dummy, $state); + $sfRestore($scope, $scopeState); + } + } finally { + $factory->builder = null; + } + } finally { + $stack->pop(); + } + } +} + +if (getenv('SF_DUMP_LABEL') !== false) { + $sfDumpLabel = getenv('SF_DUMP_LABEL'); + foreach ($sfObservations['php'] as $sfId => $sfPhpObservations) { + if (!array_key_exists($sfDumpLabel, $sfPhpObservations)) { + continue; + } + echo "scope $sfId\n php: ", json_encode($sfPhpObservations[$sfDumpLabel]), "\n native: ", json_encode($sfObservations['native'][$sfId][$sfDumpLabel] ?? ''), "\n"; + } +} +foreach ($sfObservations['php'] as $sfId => $sfPhpObservations) { + $sfNativeObservations = $sfObservations['native'][$sfId] ?? []; + foreach ($sfPhpObservations as $label => $expected) { + $actual = array_key_exists($label, $sfNativeObservations) ? $sfNativeObservations[$label] : ''; + if ($actual === Harness::BARRIER && $expected !== Harness::BARRIER) { + $sfBarrierHits++; + if (getenv('SF_BARRIER_DEBUG') !== false) { + echo "BARRIER: (scope $sfId) $label\n"; + } + continue; + } + check($expected === $actual, sprintf('MutatingScope parity (scope %d) %s: %s vs %s', $sfId, $label, json_encode($expected), json_encode($actual))); + } + check(array_keys($sfPhpObservations) === array_keys($sfNativeObservations), "MutatingScope parity (scope $sfId): the same observations on both sides"); +} +$sfObservationCount = array_sum(array_map('count', $sfObservations['php'])); +check($sfObservationCount > 2000, "scope-family: enough observations ($sfObservationCount over $sfSampleCount scopes)"); +// the barrier is a known cost of the prefix; it must stay a small share +check($sfBarrierHits < $sfObservationCount / 20, "scope-family: the prefix type barrier cut $sfBarrierHits of $sfObservationCount observations short"); + +if (isset($scopeFamilyStandalone)) { + echo $failures === 0 ? "ALL OK ($sfObservationCount observations over $sfSampleCount scopes, $sfBarrierHits barrier hits skipped)\n" : "$failures FAILURES\n"; + exit($failures === 0 ? 0 : 1); +} + +} diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index d2becd92b38..3e286b7ca6e 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -379,6 +379,40 @@ function check(bool $cond, string $msg): void } + +// ---- ExpressionResultStorageStack ---- +$covered[\PHPStan\Analyser\ExpressionResultStorageStack::class] = true; +foreach (['php' => \PHPStan\Analyser\ExpressionResultStorageStack::class, 'native' => \PHPStanTurbo\ExpressionResultStorageStack::class] as $label => $stackClass) { + $stack = new $stackClass(); + check($stack->getCurrent() === null, "ERSS $label: empty stack has no current storage"); + + $storageA = new \PHPStan\Analyser\ExpressionResultStorage(); + $storageB = new \PHPStan\Analyser\ExpressionResultStorage(); + $stack->push($storageA); + check($stack->getCurrent() === $storageA, "ERSS $label: getCurrent answers the pushed storage"); + $stack->push($storageB); + check($stack->getCurrent() === $storageB, "ERSS $label: getCurrent answers the top of the stack"); + $stack->pop(); + check($stack->getCurrent() === $storageA, "ERSS $label: pop uncovers the one below"); + $stack->push($storageA); + check($stack->getCurrent() === $storageA, "ERSS $label: the same storage may be pushed twice"); + $stack->pop(); + $stack->pop(); + check($stack->getCurrent() === null, "ERSS $label: the emptied stack has no current storage"); + + $popped = null; + try { + $stack->pop(); + } catch (\PHPStan\ShouldNotHappenException $e) { + $popped = $e->getMessage(); + } + check($popped === 'Unbalanced ExpressionResultStorageStack pop.', "ERSS $label: popping an empty stack throws"); + + // the stack survives the failed pop and keeps working + $stack->push($storageB); + check($stack->getCurrent() === $storageB, "ERSS $label: usable again after the failed pop"); +} + // ---- NodeScanner ---- $covered[\PHPStan\Node\NodeScanner::class] = true; $smokeParserFactory = new \PhpParser\ParserFactory(); @@ -2365,6 +2399,732 @@ enum SmokeTurboBacked: string } check($lruResults['php'] === $lruResults['native'], 'LruCache parity: ' . json_encode($lruResults['php']) . ' vs ' . json_encode($lruResults['native'])); $covered[\PHPStan\Internal\LruCache::class] = true; +$covered[\PHPStan\Analyser\VolatileExpressionHelper::class] = true; +$covered[\PHPStan\Analyser\VariableFlow::class] = true; +$covered[\PHPStan\Analyser\VariableFlowBuilder::class] = true; +$covered[\PHPStan\Analyser\VariableLivenessResolver::class] = true; + +// ---- VolatileExpressionHelper ---- +// The tables are the by-reference copies a MutatingScope hands in: holders of +// the side's own class over shared expression nodes; the results are the +// return value and the surviving keys of both tables. +$vehScopeFactory = $scContainer->getByType(\PHPStan\Analyser\ScopeFactory::class); +$vehScope = $vehScopeFactory->create(\PHPStan\Analyser\ScopeContext::create(__FILE__)); +$vehInputs = static function (string $side): array { + $holder = $side === 'php' + ? static fn ($expr, $type, $certainty) => new \PHPStan\Analyser\ExpressionTypeHolder($expr, $type, $certainty) + : static fn ($expr, $type, $certainty) => new \PHPStanTurbo\ExpressionTypeHolder($expr, $type, $certainty); + $yes = $side === 'php' ? \PHPStan\TrinaryLogic::createYes() : \PHPStanTurbo\TrinaryLogic::createYes(); + $maybe = $side === 'php' ? \PHPStan\TrinaryLogic::createMaybe() : \PHPStanTurbo\TrinaryLogic::createMaybe(); + $int = new \PHPStanTurbo\IntegerType(); + $string = new \PHPStanTurbo\StringType(); + $true = new \PHPStanTurbo\ConstantBooleanType(true); + $false = new \PHPStanTurbo\ConstantBooleanType(false); + $bool = new \PHPStanTurbo\BooleanType(); + $funcCall = static fn (string $name, array $args = [], bool $fullyQualified = false) => new \PhpParser\Node\Expr\FuncCall( + $fullyQualified ? new \PhpParser\Node\Name\FullyQualified($name) : new \PhpParser\Node\Name($name), + $args, + ); + $arg = static fn (\PhpParser\Node\Expr $value) => new \PhpParser\Node\Arg($value); + $expressionTypes = [ + 'ob_get_level()' => $holder($funcCall('ob_get_level'), $int, $yes), + '\openssl_error_string()' => $holder($funcCall('openssl_error_string', [], true), $string, $yes), + '$_GET' => $holder(new \PhpParser\Node\Expr\Variable('_GET'), $int, $yes), + '$_GET[\'x\']' => $holder(new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('_GET'), new \PhpParser\Node\Scalar\String_('x')), $string, $yes), + '$_SERVERx' => $holder(new \PhpParser\Node\Expr\Variable('_SERVERx'), $int, $yes), + '$a' => $holder(new \PhpParser\Node\Expr\Variable('a'), $int, $yes), + 'class_exists(\'Foo\')' => $holder($funcCall('class_exists', [$arg(new \PhpParser\Node\Scalar\String_('Foo'))]), $false, $yes), + 'function_exists(\'bar\')' => $holder($funcCall('function_exists', [$arg(new \PhpParser\Node\Scalar\String_('bar'))]), $bool, $maybe), + '\class_exists(\'Baz\')' => $holder($funcCall('class_exists', [$arg(new \PhpParser\Node\Scalar\String_('Baz'))], true), $true, $yes), + 'class_exists(...)' => $holder($funcCall('class_exists', [new \PhpParser\Node\VariadicPlaceholder()]), $false, $yes), + 'enum_exists($x)' => $holder($funcCall('enum_exists', [$arg(new \PhpParser\Node\Expr\Variable('x'))]), $false, $yes), + 'interface_exists()' => $holder($funcCall('interface_exists'), $false, $yes), + '$f(\'x\')' => $holder(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Expr\Variable('f'), [$arg(new \PhpParser\Node\Scalar\String_('x'))]), $false, $yes), + 'strlen(\'x\')' => $holder($funcCall('strlen', [$arg(new \PhpParser\Node\Scalar\String_('x'))]), $int, $yes), + ]; + $nativeExpressionTypes = [ + 'ob_get_level()' => $expressionTypes['ob_get_level()'], + '$_GET' => $expressionTypes['$_GET'], + '$_GET[\'x\']' => $expressionTypes['$_GET[\'x\']'], + 'class_exists(\'Foo\')' => $expressionTypes['class_exists(\'Foo\')'], + '$b' => $holder(new \PhpParser\Node\Expr\Variable('b'), $int, $yes), + 'openssl_error_string()' => $holder($funcCall('openssl_error_string'), $string, $yes), + ]; + + return [$expressionTypes, $nativeExpressionTypes]; +}; +$vehResults = []; +foreach (['php' => \PHPStan\Analyser\VolatileExpressionHelper::class, 'native' => \PHPStanTurbo\VolatileExpressionHelper::class] as $side => $vehClass) { + $r = []; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateVolatileFunctionCalls($e, $n), array_keys($e), array_keys($n)]; + $e1 = []; + $n1 = []; + $r[] = [$vehClass::invalidateVolatileFunctionCalls($e1, $n1), $e1, $n1]; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateSuperglobals($e, $n), array_keys($e), array_keys($n)]; + [$e, $n] = $vehInputs($side); + unset($e['$_GET'], $n['$_GET']); + $r[] = [$vehClass::invalidateSuperglobals($e, $n), array_keys($e), array_keys($n)]; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateNegativeExistenceChecks($vehScope, $e, $n), array_keys($e), array_keys($n)]; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateNegativeExistenceChecks($vehScope, $e, $n, ['function_exists']), array_keys($e), array_keys($n)]; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateNegativeExistenceChecks($vehScope, $e, $n, ['class_exists', 'enum_exists'], '\FOO'), array_keys($e), array_keys($n)]; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateNegativeExistenceChecks($vehScope, $e, $n, ['class_exists'], 'Other'), array_keys($e), array_keys($n)]; + [$e, $n] = $vehInputs($side); + $r[] = [$vehClass::invalidateNegativeExistenceChecks($vehScope, $e, $n, ['strlen']), array_keys($e), array_keys($n)]; + // the by-reference contract: the caller's copies change, the originals stay + [$e, $n] = $vehInputs($side); + $copyE = $e; + $copyN = $n; + $r[] = [$vehClass::invalidateVolatileFunctionCalls($copyE, $copyN), array_keys($e), array_keys($n), array_keys($copyE), array_keys($copyN)]; + $vehResults[$side] = $r; +} +check($vehResults['php'] === $vehResults['native'], 'VolatileExpressionHelper parity: ' . json_encode($vehResults['php']) . ' vs ' . json_encode($vehResults['native'])); +check($vehResults['php'][0][0] === true && $vehResults['php'][2][0] === true && $vehResults['php'][4][0] === true && $vehResults['php'][3][0] === false, 'VolatileExpressionHelper: the fixture exercises removals and no-ops'); + +// ---- VariableFlow ---- +// The factories build the PHP flow classes over shared nodes and writes; +// flows are compared structurally (class names modulo the prefix). +$vfDescribeWrite = static fn (?\PHPStan\Node\Variable\VariableWrite $write): ?array => $write === null ? null : [ + $write->getVariableName(), + spl_object_id($write->getNode()), + $write->getId(), + $write->getKind(), + $write->isOffsetWrite(), + $write->getOffset(), + $write->getParentId(), + $write->replacesOffset(), +]; +$vfDescribe = static function ($flow) use (&$vfDescribe, $vfDescribeWrite, $turboNorm) { + if ($flow === null) { + return null; + } + if (!$flow instanceof \PHPStan\Analyser\VariableFlow) { + return 'not a flow: ' . get_debug_type($flow); + } + $d = ['class' => $turboNorm(get_class($flow)), 'kind' => $flow->kind]; + if ($flow instanceof \PHPStan\Analyser\VariableAccessFlow) { + $d += [ + 'name' => $flow->name, + 'write' => $vfDescribeWrite($flow->write), + 'type' => $flow->type?->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'targetId' => $flow->targetId, + 'container' => $flow->container, + 'offset' => $flow->offset, + ]; + } elseif ($flow instanceof \PHPStan\Analyser\VariableSequenceFlow) { + $d['children'] = array_map($vfDescribe, $flow->children); + } elseif ($flow instanceof \PHPStan\Analyser\VariableControlFlow) { + $d += [ + 'children' => array_map($vfDescribe, $flow->children), + 'name' => $flow->name, + 'type' => $flow->type?->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'level' => $flow->level, + 'atLeastOnce' => $flow->atLeastOnce, + 'canExit' => $flow->canExit, + 'catches' => array_map(static fn (array $catch) => [$catch[0]->describe(\PHPStan\Type\VerbosityLevel::precise()), $vfDescribe($catch[1])], $flow->catches), + 'arrow' => $flow->arrow !== null ? spl_object_id($flow->arrow) : null, + 'cases' => array_map(static fn (array $case) => [$vfDescribe($case[0]), $vfDescribe($case[1]), $case[2]], $flow->cases), + 'canRepeat' => $flow->canRepeat, + 'canContainAnyThrowable' => $flow->canContainAnyThrowable, + 'stmt' => $flow->stmt !== null ? spl_object_id($flow->stmt) : null, + 'bindings' => array_map($vfDescribeWrite, $flow->bindings), + 'ownWrites' => array_map($vfDescribeWrite, $flow->ownWrites), + ]; + } elseif ($flow instanceof \PHPStan\Analyser\VariableInputFlow) { + $d += ['writeId' => $flow->writeId, 'targetId' => $flow->targetId]; + } + + return $d; +}; +check((new ReflectionClass(\PHPStanTurbo\VariableFlow::class))->isAbstract(), 'VariableFlow: the native class is abstract'); +check((new ReflectionClass(\PHPStanTurbo\VariableFlow::class))->getConstants() === (new ReflectionClass(\PHPStan\Analyser\VariableFlow::class))->getConstants(), 'VariableFlow: the kind constants'); +$vfNativeSubclass = new class('x') extends \PHPStanTurbo\VariableFlow { + + public function __construct(string $kind) + { + parent::__construct($kind); + } + + public function again(string $kind): void + { + parent::__construct($kind); + } + +}; +check($vfNativeSubclass->kind === 'x', 'VariableFlow: the protected constructor fills the readonly $kind slot'); +try { + $vfNativeSubclass->again('y'); + check(false, 'VariableFlow: a second construction must throw'); +} catch (\Error $e) { + check(str_contains($e->getMessage(), 'readonly property'), 'VariableFlow: readonly $kind: ' . $e->getMessage()); +} +try { + $vfNativeSubclass->kind = 'z'; + check(false, 'VariableFlow: $kind is readonly'); +} catch (\Error $e) { + check(true, ''); +} +$vfNodeA = new \PhpParser\Node\Expr\Variable('a'); +$vfNodeB = new \PhpParser\Node\Expr\Variable('b'); +$vfArrow = new \PhpParser\Node\Expr\ArrowFunction(['expr' => $vfNodeA]); +$vfForeach = new \PhpParser\Node\Stmt\Foreach_($vfNodeA, $vfNodeB); +$vfWriteA = new \PHPStan\Node\Variable\VariableWrite('a', $vfNodeA, 11, \PHPStan\Node\Variable\VariableWrite::KIND_ASSIGN); +$vfWriteItem = new \PHPStan\Node\Variable\VariableWrite('b', $vfNodeB, 12, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_LITERAL_ITEM, false, null, 11); +$vfWriteOffset = new \PHPStan\Node\Variable\VariableWrite('a', $vfNodeA, 13, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_DIM_WRITE, true, 'k', null, false); +$vfInt = new \PHPStanTurbo\IntegerType(); +$vfString = new \PHPStanTurbo\StringType(); +$vfResults = []; +foreach (['php' => \PHPStan\Analyser\VariableFlow::class, 'native' => \PHPStanTurbo\VariableFlow::class] as $side => $vf) { + $r = []; + $readA = $vf::read('a'); + $readB = $vf::read('b', 7, true, 'k'); + $r[] = [$vfDescribe($readA), $vfDescribe($readB), $vfDescribe($vf::read('b', null, false, 3)), $vf::read('this'), $vf::read('_GET'), $vf::read('GLOBALS', 1)]; + $r[] = [$vf::sequence(), $vf::sequence(null, null), $vf::sequence(null, $readA) === $readA, $vfDescribe($vf::sequence($readA, null, $readB)), $vfDescribe($vf::sequence(...[$readA, $readB, $readA]))]; + $r[] = [$vf::choice(), $vf::choice($readA) === $readA, $vf::choice($readA, $readA) === $readA, $vf::choice(null, null), $vfDescribe($vf::choice($readA, null)), $vfDescribe($vf::choice($readA, $readB, null))]; + $r[] = [$vfDescribe($vf::arrow($vfArrow, $readA, null)), $vfDescribe($vf::arrow($vfArrow, null, $readB))]; + $r[] = [$vfDescribe($vf::conditional($readA, $readB, null, true)), $vfDescribe($vf::conditional($readA, $readB, $readA, false)), $vfDescribe($vf::conditional(null, $readB, $readA, null)), $vf::conditional(null, null, null, null), $vfDescribe($vf::conditional($readA, null, null, true))]; + $r[] = [$vfDescribe($vf::switch($readA, [[$readB, $readA, false], [null, null, true]], true)), $vfDescribe($vf::switch(null, [], false))]; + $r[] = [$vfDescribe($vf::write($vfWriteA)), $vfDescribe($vf::write($vfWriteItem, $vfInt)), $vfDescribe($vf::write($vfWriteOffset, null)), $vfDescribe($vf::discard($vfWriteA)), $vfDescribe($vf::discard($vfWriteItem))]; + $r[] = [$vfDescribe($vf::inputs(11, null)), $vfDescribe($vf::inputs(12, 7))]; + $r[] = [$vfDescribe($vf::escape('a')), $vfDescribe($vf::escape('this')), $vfDescribe($vf::mention('b')), $vfDescribe($vf::all($vf::READ_ALL)), $vfDescribe($vf::all($vf::MENTION_ALL)), $vfDescribe($vf::all($vf::OPAQUE))]; + $r[] = [$vfDescribe($vf::exit($vf::RETURN)), $vfDescribe($vf::exit($vf::BREAK, 2)), $vfDescribe($vf::exit($vf::CONTINUE, 1, 'x')), $vfDescribe($vf::exit($vf::STOP, 3, null))]; + $r[] = [$vfDescribe($vf::throwing($vfInt, true)), $vfDescribe($vf::throwing($vfString, false, true))]; + $r[] = [$vf::dead(null), $vfDescribe($vf::dead($readA))]; + $r[] = [$vfDescribe($vf::loop($readA, $readB, null, true, false)), $vfDescribe($vf::loop(null, null, $readA, false, true, false))]; + $r[] = [$vf::loopStatement($vfForeach, $readA, [], [$vfWriteA]) === $readA, $vf::loopStatement($vfForeach, null, [], []), $vfDescribe($vf::loopStatement($vfForeach, $readA, [$vfWriteA], [$vfWriteA, $vfWriteOffset]))]; + $r[] = [$vfDescribe($vf::tryCatch($readA, [[$vfInt, $readB], [$vfString, null]], null)), $vfDescribe($vf::tryCatch(null, [], $readB))]; + $vfResults[$side] = $r; +} +check($vfResults['php'] === $vfResults['native'], 'VariableFlow parity: ' . json_encode($vfResults['php']) . ' vs ' . json_encode($vfResults['native'])); +// a VariableWrite that skipped its constructor: the twin's getters throw +$vfRawWrite = (new ReflectionClass(\PHPStan\Node\Variable\VariableWrite::class))->newInstanceWithoutConstructor(); +$vfRawResults = []; +foreach (['php' => \PHPStan\Analyser\VariableFlow::class, 'native' => \PHPStanTurbo\VariableFlow::class] as $side => $vf) { + try { + $vf::write($vfRawWrite); + $vfRawResults[$side] = 'no error'; + } catch (\Error $e) { + $vfRawResults[$side] = [get_class($e), $e->getMessage()]; + } +} +check($vfRawResults['php'] === $vfRawResults['native'], 'VariableFlow: write() over an unconstructed VariableWrite: ' . json_encode($vfRawResults)); + +// ---- VariableFlowBuilder ---- +// Shared nodes and a scope; per side a storage of the side's class holding +// ExpressionResults that carry a flow and a type (set through reflection — +// the results' construction is not what is under test), and an ArgsResult. +$vfbScope = $vehScope + ->assignVariable('arr', new \PHPStan\Type\ArrayType($vfInt, $vfString), new \PHPStan\Type\ArrayType($vfInt, $vfString), \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('str', $vfString, $vfString, \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('int', $vfInt, $vfInt, \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('maybe', $vfInt, $vfInt, \PHPStan\TrinaryLogic::createMaybe()); +$vfbResultReflection = new ReflectionClass(\PHPStan\Analyser\ExpressionResult::class); +$vfbMakeResult = static function (?\PHPStan\Analyser\VariableFlow $flow, ?\PHPStan\Type\Type $type = null) use ($vfbResultReflection): \PHPStan\Analyser\ExpressionResult { + $result = $vfbResultReflection->newInstanceWithoutConstructor(); + $vfbResultReflection->getProperty('variableFlow')->setValue($result, $flow); + $vfbResultReflection->getProperty('cachedType')->setValue($result, $type ?? new \PHPStanTurbo\MixedType()); + return $result; +}; +$vfbN = [ + 'a' => new \PhpParser\Node\Expr\Variable('a'), + 'b' => new \PhpParser\Node\Expr\Variable('b'), + 'this' => new \PhpParser\Node\Expr\Variable('this'), + 'get' => new \PhpParser\Node\Expr\Variable('_GET'), + 'arr' => new \PhpParser\Node\Expr\Variable('arr'), + 'str' => new \PhpParser\Node\Expr\Variable('str'), + 'int' => new \PhpParser\Node\Expr\Variable('int'), + 'maybe' => new \PhpParser\Node\Expr\Variable('maybe'), + 'unknown' => new \PhpParser\Node\Expr\Variable('unknown'), + 'varVar' => new \PhpParser\Node\Expr\Variable(new \PhpParser\Node\Expr\Variable('name')), + 'k' => new \PhpParser\Node\Scalar\String_('k'), + 'one' => new \PhpParser\Node\Scalar\Int_(1), + 'call' => new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('f')), + 'closure' => new \PhpParser\Node\Expr\Closure(), + 'arrow' => new \PhpParser\Node\Expr\ArrowFunction(['expr' => new \PhpParser\Node\Scalar\Int_(2)]), + 'name' => new \PhpParser\Node\Name('Foo'), +]; +$vfbN['dimArrK'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['arr'], $vfbN['k']); +$vfbN['dimArrNested'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['dimArrK'], $vfbN['one']); +$vfbN['dimArrNull'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['arr'], null); +$vfbN['dimStr'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['str'], $vfbN['one']); +$vfbN['dimInt'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['int'], $vfbN['one']); +$vfbN['dimMaybe'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['maybe'], $vfbN['k']); +$vfbN['dimUnknown'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['unknown'], $vfbN['k']); +$vfbN['dimThis'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['this'], $vfbN['k']); +$vfbN['dimGet'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['get'], $vfbN['k']); +$vfbN['dimCall'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['call'], $vfbN['k']); +$vfbN['dimVarVar'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['varVar'], $vfbN['k']); +$vfbN['prop'] = new \PhpParser\Node\Expr\PropertyFetch($vfbN['a'], 'p'); +$vfbN['propExpr'] = new \PhpParser\Node\Expr\PropertyFetch($vfbN['a'], $vfbN['b']); +$vfbN['nullsafeProp'] = new \PhpParser\Node\Expr\NullsafePropertyFetch($vfbN['a'], 'p'); +$vfbN['staticProp'] = new \PhpParser\Node\Expr\StaticPropertyFetch($vfbN['name'], 'p'); +$vfbN['staticPropExpr'] = new \PhpParser\Node\Expr\StaticPropertyFetch($vfbN['a'], $vfbN['b']); +$vfbN['dimProp'] = new \PhpParser\Node\Expr\ArrayDimFetch($vfbN['prop'], $vfbN['k']); +$vfbN['list'] = new \PhpParser\Node\Expr\List_([ + new \PhpParser\Node\ArrayItem($vfbN['a'], $vfbN['k']), + null, + new \PhpParser\Node\ArrayItem($vfbN['b'], null, true), + new \PhpParser\Node\ArrayItem($vfbN['dimArrK']), + new \PhpParser\Node\ArrayItem(new \PhpParser\Node\Expr\List_([new \PhpParser\Node\ArrayItem($vfbN['str'])])), +]); +$vfbN['array'] = new \PhpParser\Node\Expr\Array_([new \PhpParser\Node\ArrayItem($vfbN['int'])]); +$vfbN['emptyList'] = new \PhpParser\Node\Expr\List_([]); +$vfbN['callArgs'] = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('g'), [ + new \PhpParser\Node\Arg($vfbN['a']), + new \PhpParser\Node\Arg($vfbN['b'], true), + new \PhpParser\Node\Arg($vfbN['dimArrK']), + new \PhpParser\Node\Arg($vfbN['closure']), + new \PhpParser\Node\Arg($vfbN['call']), + new \PhpParser\Node\Arg($vfbN['arrow']), +]); +$vfbN['callArgs']->setAttribute('startFilePos', 10); +$vfbN['callArgs']->setAttribute('endFilePos', 20); +$vfbN['samePos'] = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('h')); +$vfbN['samePos']->setAttribute('startFilePos', 10); +$vfbN['samePos']->setAttribute('endFilePos', 20); +$vfbN['otherPos'] = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('h')); +$vfbN['otherPos']->setAttribute('startFilePos', 10); +$vfbN['otherPos']->setAttribute('endFilePos', 21); +$vfbN['noPos'] = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('h')); +$vfbN['fcc'] = new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('g'), [new \PhpParser\Node\VariadicPlaceholder()]); +$vfbN['fcc']->setAttribute('startFilePos', 30); +$vfbN['fcc']->setAttribute('endFilePos', 40); +$vfbThrowable = new \PHPStan\Type\ObjectType(\Throwable::class); +$vfbThrowPoints = [ + \PHPStan\Analyser\InternalThrowPoint::createExplicit($vfbScope, $vfInt, $vfbN['callArgs'], false), + \PHPStan\Analyser\InternalThrowPoint::createExplicit($vfbScope, $vfString, $vfbN['closure'], true), + \PHPStan\Analyser\InternalThrowPoint::createExplicit($vfbScope, $vfbThrowable, $vfbN['arrow'], false), + \PHPStan\Analyser\InternalThrowPoint::createExplicit($vfbScope, $vfInt, $vfbN['samePos'], true), + \PHPStan\Analyser\InternalThrowPoint::createExplicit($vfbScope, $vfString, $vfbN['otherPos'], false), + \PHPStan\Analyser\InternalThrowPoint::createExplicit($vfbScope, $vfInt, $vfbN['noPos'], false), + \PHPStan\Analyser\InternalThrowPoint::createImplicit($vfbScope, $vfbN['a']), +]; +$vfbSides = [ + 'php' => [\PHPStan\Analyser\VariableFlowBuilder::class, \PHPStan\Analyser\VariableFlow::class, \PHPStan\Analyser\ExpressionResultStorage::class], + 'native' => [\PHPStanTurbo\VariableFlowBuilder::class, \PHPStanTurbo\VariableFlow::class, \PHPStanTurbo\ExpressionResultStorage::class], + 'native over a PHP storage' => [\PHPStanTurbo\VariableFlowBuilder::class, \PHPStanTurbo\VariableFlow::class, \PHPStan\Analyser\ExpressionResultStorage::class], +]; +$vfbResults = []; +foreach ($vfbSides as $side => [$builder, $vf, $storageClass]) { + $storage = new $storageClass(); + $flowA = $vf::read('a'); + $flowB = $vf::escape('b'); + $flowK = $vf::mention('k'); + $storage->storeExpressionResult($vfbN['a'], $vfbMakeResult($flowA, $vfInt)); + $storage->storeExpressionResult($vfbN['b'], $vfbMakeResult($flowB)); + $storage->storeExpressionResult($vfbN['k'], $vfbMakeResult($flowK, new \PHPStanTurbo\ConstantStringType('k'))); + $storage->storeExpressionResult($vfbN['one'], $vfbMakeResult(null, new \PHPStanTurbo\ConstantIntegerType(1))); + $storage->storeExpressionResult($vfbN['call'], $vfbMakeResult($vf::all($vf::OPAQUE))); + $storage->storeExpressionResult($vfbN['varVar'], $vfbMakeResult($vf::mention('name'))); + $storage->storeExpressionResult($vfbN['dimArrK'], $vfbMakeResult($vf::read('arr', null, false, 'k'), $vfString)); + $storage->storeExpressionResult($vfbN['closure'], $vfbMakeResult($vf::escape('c'))); + $storage->storeExpressionResult($vfbN['prop'], $vfbMakeResult($vf::mention('prop'))); + $argsResult = new \PHPStan\Analyser\ArgsResult( + $vfbMakeResult(null), + null, + [spl_object_id($vfbN['a']) => $vfbMakeResult($vf::read('a', 99)), spl_object_id($vfbN['closure']) => $vfbMakeResult(null)], + [spl_object_id($vfbN['call']) => true], + ); + + $r = []; + $r[] = [$vfDescribe($builder::throws($vfbN['callArgs'], $vfbThrowPoints)), $vfDescribe($builder::throws($vfbN['fcc'], $vfbThrowPoints)), $builder::throws($vfbN['a'], []), $vfDescribe($builder::throws($vfbN['a'], $vfbThrowPoints))]; + $r[] = [$vfDescribe($builder::arguments($vfbN['callArgs'], $argsResult, $storage)), $builder::arguments($vfbN['call'], $argsResult, $storage)]; + $r[] = [$vfDescribe($builder::child($vfbN['a'], $storage)), $builder::child($vfbN['unknown'], $storage), $builder::child($vfbN['name'], $storage), $builder::child(null, $storage), $builder::child($vfbN['one'], $storage)]; + foreach (['a', 'this', 'get', 'varVar', 'list', 'array', 'dimArrK', 'dimArrNested', 'dimArrNull', 'dimCall', 'dimVarVar', 'dimProp', 'prop', 'propExpr', 'nullsafeProp', 'staticProp', 'staticPropExpr', 'call', 'k'] as $key) { + $r[] = [$key, $vfDescribe($builder::targetRead($vfbN[$key], $storage, true)), $vfDescribe($builder::targetRead($vfbN[$key], $storage, false)), $vfDescribe($builder::targetRead($vfbN[$key], $storage, true, 5))]; + } + foreach (['a', 'this', 'get', 'varVar', 'list', 'array', 'emptyList', 'dimArrK', 'dimArrNested', 'dimArrNull', 'dimStr', 'dimInt', 'dimMaybe', 'dimUnknown', 'dimThis', 'dimGet', 'dimCall', 'dimVarVar', 'dimProp', 'prop', 'call'] as $key) { + $write = $builder::targetWrite($vfbN[$key], \PHPStan\Node\Variable\VariableWrite::KIND_ASSIGN, $vfbScope, $storage); + $r[] = [$key, $vfDescribe($write), $vfDescribe($builder::targetWrite($vfbN[$key], \PHPStan\Node\Variable\VariableWrite::KIND_PRE_INC, $vfbScope, $storage, $vfInt)), $vfDescribeWrite($builder::writeSite($vfbN[$key], \PHPStan\Node\Variable\VariableWrite::KIND_ASSIGN, $vfbScope, $storage)), array_map($vfDescribeWrite, $builder::writes($write))]; + } + $r[] = [$builder::writes(null), array_map($vfDescribeWrite, $builder::writes($vf::sequence($vf::write($vfWriteA), $vf::sequence($vf::escape('x'), $vf::write($vfWriteItem)), $vf::dead($vf::write($vfWriteOffset))))), $builder::writes($vf::all($vf::OPAQUE))]; + foreach (['a', 'this', 'varVar', 'dimArrNested', 'dimCall', 'dimVarVar', 'prop', 'call'] as $key) { + $r[] = [$key, $vfDescribe($builder::escapeRoot($vfbN[$key]))]; + } + $vfbResults[$side] = $r; +} +check($vfbResults['php'] === $vfbResults['native'], 'VariableFlowBuilder parity: ' . json_encode($vfbResults['php']) . ' vs ' . json_encode($vfbResults['native'])); +check($vfbResults['php'] === $vfbResults['native over a PHP storage'], 'VariableFlowBuilder parity over a PHP storage: ' . json_encode($vfbResults['php']) . ' vs ' . json_encode($vfbResults['native over a PHP storage'])); + +// ---- VariableLivenessResolver ---- +// Flow trees built once (the PHP flow classes over shared nodes and writes) +// and resolved by both sides; the VariableWritesNode is compared field by +// field (writes and types described, loop statements by object id). A throw +// that can contain any Throwable is only placed outside try/catch: inside, +// the PHP twin instantiates the PHP ObjectType(Throwable) against the native +// catch types, which the prefixed declaration cannot mix (see type-family.php). +$vlrDescribe = static function (\PHPStan\Node\VariableWritesNode $node) use ($vfDescribeWrite): array { + $d = []; + foreach (['writes', 'readWriteIds', 'usedWriteIds', 'coveredWriteIds', 'readVariableNames', 'redundantWriteTypes', 'referencedVariableNames', 'untrackedVariableNames', 'variableOverwritingLoops', 'opaque', 'allVariableNamesReferenced'] as $property) { + $value = (new ReflectionProperty($node, $property))->getValue($node); + if ($property === 'writes') { + $value = array_map($vfDescribeWrite, $value); + } elseif ($property === 'redundantWriteTypes') { + $value = array_map(static fn (\PHPStan\Type\Type $type): string => $type->describe(\PHPStan\Type\VerbosityLevel::precise()), $value); + } elseif ($property === 'variableOverwritingLoops') { + $value = array_map(static fn (object $statement): int => spl_object_id($statement), $value); + } + $d[$property] = $value; + } + $d['functionLike'] = spl_object_id($node->getFunctionLike()); + + return $d; +}; +$vlrF = \PHPStan\Analyser\VariableFlow::class; +$vlrVar = static fn (string $name) => new \PhpParser\Node\Expr\Variable($name); +$vlrWrite = static fn (string $name, int $id, int $kind = \PHPStan\Node\Variable\VariableWrite::KIND_ASSIGN, bool $offsetWrite = false, $offset = null, ?int $parentId = null, bool $replacesOffset = true) => new \PHPStan\Node\Variable\VariableWrite($name, $vlrVar($name), $id, $kind, $offsetWrite, $offset, $parentId, $replacesOffset); +$vlrInt = new \PHPStanTurbo\IntegerType(); +$vlrString = new \PHPStanTurbo\StringType(); +$vlrException = new \PHPStanTurbo\ObjectType(\Exception::class); +$vlrRuntime = new \PHPStanTurbo\ObjectType(\RuntimeException::class); +$vlrThrowable = new \PHPStanTurbo\ObjectType(\Throwable::class); +$vlrForeach = new \PhpParser\Node\Stmt\Foreach_($vlrVar('items'), $vlrVar('k')); +$vlrFor = new \PhpParser\Node\Stmt\For_(); +$vlrArrow = new \PhpParser\Node\Expr\ArrowFunction(['params' => [new \PhpParser\Node\Param($vlrVar('p')), new \PhpParser\Node\Param($vlrVar('q'))], 'expr' => $vlrVar('p')]); +$vlrFunctions = [ + 'function' => new \PhpParser\Node\Stmt\Function_('f', ['params' => [new \PhpParser\Node\Param($vlrVar('a')), new \PhpParser\Node\Param($vlrVar('r'), null, null, true), new \PhpParser\Node\Param($vlrVar('this'))]]), + 'closure by ref' => new \PhpParser\Node\Expr\Closure(['byRef' => true, 'uses' => [new \PhpParser\Node\ClosureUse($vlrVar('u')), new \PhpParser\Node\ClosureUse($vlrVar('ur'), true)]]), + 'method' => new \PhpParser\Node\Stmt\ClassMethod('m', ['params' => [new \PhpParser\Node\Param($vlrVar('promoted'), null, null, false, false, [], \PhpParser\Modifiers::PUBLIC)]]), +]; +$vlrFlows = [ + 'empty' => null, + 'plain' => $vlrF::sequence( + $vlrF::write($vlrWrite('a', 1)), + $vlrF::read('a'), + $vlrF::write($vlrWrite('b', 2), $vlrInt), + $vlrF::write($vlrWrite('c', 3)), + $vlrF::inputs(3, null), + $vlrF::write($vlrWrite('d', 4)), + $vlrF::inputs(4, 5), + $vlrF::write($vlrWrite('e', 5)), + $vlrF::read('e'), + $vlrF::write($vlrWrite('f', 6)), + $vlrF::write($vlrWrite('f', 7)), + $vlrF::read('f'), + $vlrF::discard($vlrWrite('g', 8)), + $vlrF::mention('m'), + $vlrF::escape('h'), + $vlrF::write($vlrWrite('h', 9)), + $vlrF::write($vlrWrite('this', 10)), + $vlrF::write($vlrWrite('_GET', 11)), + $vlrF::read('unknown'), + ), + 'branches' => $vlrF::sequence( + $vlrF::write($vlrWrite('a', 1)), + $vlrF::conditional($vlrF::read('a'), $vlrF::write($vlrWrite('d', 2)), $vlrF::write($vlrWrite('d', 3)), null), + $vlrF::read('d'), + $vlrF::conditional(null, $vlrF::write($vlrWrite('x', 4)), $vlrF::write($vlrWrite('x', 5)), true), + $vlrF::conditional(null, $vlrF::write($vlrWrite('y', 6)), $vlrF::write($vlrWrite('y', 7)), false), + $vlrF::choice($vlrF::read('x'), $vlrF::read('y'), null), + $vlrF::switch($vlrF::read('s'), [[$vlrF::read('c1'), $vlrF::sequence($vlrF::write($vlrWrite('sw', 8)), $vlrF::exit($vlrF::BREAK)), false], [null, $vlrF::sequence($vlrF::read('sw'), $vlrF::write($vlrWrite('sw', 9))), true]], false), + $vlrF::switch($vlrF::read('s'), [[$vlrF::read('c2'), $vlrF::write($vlrWrite('ex', 10)), false]], true), + $vlrF::read('ex'), + $vlrF::dead($vlrF::sequence($vlrF::write($vlrWrite('dead', 11)), $vlrF::read('dead'))), + $vlrF::exit($vlrF::RETURN, 1, 'a'), + $vlrF::write($vlrWrite('after', 12)), + ), + 'loops' => $vlrF::sequence( + $vlrF::write($vlrWrite('i', 1)), + $vlrF::write($vlrWrite('acc', 2)), + $vlrF::loop($vlrF::read('i'), $vlrF::sequence($vlrF::read('acc'), $vlrF::write($vlrWrite('acc', 3)), $vlrF::conditional($vlrF::read('stop'), $vlrF::exit($vlrF::BREAK), $vlrF::exit($vlrF::CONTINUE, 1), null), $vlrF::write($vlrWrite('unreached', 4))), $vlrF::write($vlrWrite('i', 5)), false, true), + $vlrF::read('acc'), + $vlrF::loop(null, $vlrF::sequence($vlrF::write($vlrWrite('w', 6)), $vlrF::exit($vlrF::STOP)), null, true, false, false), + $vlrF::write($vlrWrite('k', 7)), + $vlrF::loopStatement($vlrForeach, $vlrF::loop(null, $vlrF::sequence($vlrF::write($vlrWrite('k', 8, \PHPStan\Node\Variable\VariableWrite::KIND_FOREACH_KEY)), $vlrF::read('k')), null, false, true), [$vlrWrite('k', 8, \PHPStan\Node\Variable\VariableWrite::KIND_FOREACH_KEY)], [$vlrWrite('k', 8, \PHPStan\Node\Variable\VariableWrite::KIND_FOREACH_KEY)]), + $vlrF::read('k'), + $vlrF::write($vlrWrite('j', 9)), + $vlrF::loopStatement($vlrFor, $vlrF::loop($vlrF::read('j'), $vlrF::read('body'), $vlrF::write($vlrWrite('j', 11)), false, true), [$vlrWrite('j', 10)], [$vlrWrite('j', 10), $vlrWrite('j', 11)]), + $vlrF::escape('j'), + $vlrF::loopStatement($vlrForeach, null, [], []), + ), + 'exceptions' => $vlrF::sequence( + $vlrF::write($vlrWrite('t', 1)), + $vlrF::tryCatch( + $vlrF::sequence($vlrF::write($vlrWrite('t', 2)), $vlrF::throwing($vlrRuntime, true), $vlrF::write($vlrWrite('t', 3)), $vlrF::throwing($vlrString, false), $vlrF::write($vlrWrite('never', 4))), + [[$vlrException, $vlrF::sequence($vlrF::read('t'), $vlrF::write($vlrWrite('t', 5)))], [$vlrThrowable, $vlrF::read('caught')]], + $vlrF::sequence($vlrF::read('fin'), $vlrF::write($vlrWrite('fin', 6))), + ), + $vlrF::read('t'), + $vlrF::tryCatch($vlrF::sequence($vlrF::write($vlrWrite('u', 7)), $vlrF::throwing($vlrInt, false)), [[$vlrInt, $vlrF::read('u')]], null), + $vlrF::loop(null, $vlrF::tryCatch($vlrF::sequence($vlrF::write($vlrWrite('l', 8)), $vlrF::exit($vlrF::BREAK, 1), $vlrF::exit($vlrF::CONTINUE, 2)), [], $vlrF::read('l')), null, true, true), + $vlrF::throwing($vlrString, false, true), + $vlrF::write($vlrWrite('unreachable', 9)), + ), + 'arrow and literals' => $vlrF::sequence( + $vlrF::write($vlrWrite('outer', 1)), + $vlrF::write($vlrWrite('p', 2)), + $vlrF::arrow($vlrArrow, $vlrF::sequence($vlrF::read('p'), $vlrF::read('outer'), $vlrF::write($vlrWrite('inner', 3))), $vlrF::read('res')), + $vlrF::read('p'), + $vlrF::write($vlrWrite('list', 4)), + $vlrF::write($vlrWrite('x', 5, \PHPStan\Node\Variable\VariableWrite::KIND_LIST_ITEM, false, null, 4)), + $vlrF::write($vlrWrite('y', 6, \PHPStan\Node\Variable\VariableWrite::KIND_LIST_ITEM, false, null, 4)), + $vlrF::read('x'), + $vlrF::inputs(4, null), + $vlrF::write($vlrWrite('o', 7, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_DIM_WRITE, true, 'k')), + $vlrF::write($vlrWrite('o', 8, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_DIM_WRITE, true, 1, null, false)), + $vlrF::write($vlrWrite('o', 9, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_DIM_WRITE, true, null)), + $vlrF::read('o', null, false, 'k'), + $vlrF::read('o', 12, true), + $vlrF::read('o', 12), + $vlrF::write($vlrWrite('o', 10, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_DIM_WRITE, true, 'k')), + $vlrF::escape('o'), + ), + 'read all' => $vlrF::sequence($vlrF::write($vlrWrite('a', 1)), $vlrF::write($vlrWrite('o', 2, \PHPStan\Node\Variable\VariableWrite::KIND_ARRAY_DIM_WRITE, true, 'k')), $vlrF::all($vlrF::READ_ALL), $vlrF::write($vlrWrite('b', 3)), $vlrF::mention('c')), + 'mention all' => $vlrF::sequence($vlrF::write($vlrWrite('a', 1)), $vlrF::all($vlrF::MENTION_ALL)), + 'opaque' => $vlrF::sequence($vlrF::write($vlrWrite('a', 1)), $vlrF::all($vlrF::OPAQUE), $vlrF::read('a')), +]; +$vlrResults = []; +foreach (['php' => \PHPStan\Analyser\VariableLivenessResolver::class, 'native' => \PHPStanTurbo\VariableLivenessResolver::class] as $side => $resolver) { + $r = []; + foreach ($vlrFunctions as $functionLabel => $function) { + foreach ($vlrFlows as $flowLabel => $flow) { + $r[$functionLabel . ' / ' . $flowLabel] = $vlrDescribe($resolver::resolve($function, $flow)); + } + } + $vlrResults[$side] = $r; +} +foreach ($vlrResults['php'] as $label => $described) { + check($described === $vlrResults['native'][$label], "VariableLivenessResolver parity ($label): " . json_encode($described) . ' vs ' . json_encode($vlrResults['native'][$label])); +} +check(count($vlrResults['php']['function / loops']['variableOverwritingLoops']) === 2 && $vlrResults['php']['function / read all']['readVariableNames'] !== [], 'VariableLivenessResolver: the fixture exercises binding probes and READ_ALL'); + +// ---- MutatingScope ---- +// scope-family.php rebuilds real walk scopes on both sides — the PHP twin +// under its real name, the native class under the prefix — and compares +// every method, every create() argument list and the scopes they answer. +$covered[\PHPStan\Analyser\MutatingScope::class] = true; +require __DIR__ . '/scope-family.php'; + +// ---- ClassReflection ---- +// reflection-family.php rebuilds real class reflections on both sides — +// the PHP twin under its real name, the native class under the prefix — +// and compares every method and every memo slot. +$covered[\PHPStan\Reflection\ClassReflection::class] = true; +require __DIR__ . '/reflection-family.php'; + +// ---- ExpressionResult ---- +// Results built by both sides from the same scopes, expressions, callbacks +// and extension collections; every public method's answer is compared, and a +// result's state is compared field by field through reflection. +$covered[\PHPStan\Analyser\ExpressionResult::class] = true; +$erScope = $vehScope + ->assignVariable('a', $vfInt, $vfInt, \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('s', $vfString, $vfString, \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('m', $vfInt, $vfInt, \PHPStan\TrinaryLogic::createMaybe()); +$erOtherScope = $vehScope + ->assignVariable('a', $vfString, $vfString, \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('s', $vfString, $vfString, \PHPStan\TrinaryLogic::createYes()); +$erWiderScope = $vehScope + ->assignVariable('a', new \PHPStanTurbo\UnionType([$vfInt, $vfString]), new \PHPStanTurbo\UnionType([$vfInt, $vfString]), \PHPStan\TrinaryLogic::createYes()) + ->assignVariable('s', $vfString, $vfString, \PHPStan\TrinaryLogic::createYes()); +$erNoExtensions = new \PHPStan\DependencyInjection\DirectExtensionsCollection([]); +$erExtensionCalls = 0; +$erHitExtensions = new \PHPStan\DependencyInjection\DirectExtensionsCollection([ + new class($erExtensionCalls) implements \PHPStan\Type\ExpressionTypeResolverExtension { + + public function __construct(private int &$calls) + { + } + + public function getType(\PhpParser\Node\Expr $expr, \PHPStan\Analyser\Scope $scope): ?\PHPStan\Type\Type + { + $this->calls++; + return $expr instanceof \PhpParser\Node\Expr\Variable && $expr->name === 'ext' ? new \PHPStanTurbo\ConstantStringType('from extension') : null; + } + + }, +]); +$erN = [ + 'variable a' => new \PhpParser\Node\Expr\Variable('a'), + 'variable unknown' => new \PhpParser\Node\Expr\Variable('nope'), + 'variable ext' => new \PhpParser\Node\Expr\Variable('ext'), + 'variable variable' => new \PhpParser\Node\Expr\Variable(new \PhpParser\Node\Expr\Variable('a')), + 'func call' => new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('f'), [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('a'))]), + 'func call fcc' => new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('f'), [new \PhpParser\Node\VariadicPlaceholder()]), + 'dynamic func call' => new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Expr\Variable('f')), + 'method call' => new \PhpParser\Node\Expr\MethodCall(new \PhpParser\Node\Expr\Variable('a'), 'm', [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('s'))]), + 'nullsafe method call' => new \PhpParser\Node\Expr\NullsafeMethodCall(new \PhpParser\Node\Expr\Variable('m'), 'm'), + 'static call' => new \PhpParser\Node\Expr\StaticCall(new \PhpParser\Node\Name('Foo'), 'm'), + 'closure' => new \PhpParser\Node\Expr\Closure(['uses' => [new \PhpParser\Node\ClosureUse(new \PhpParser\Node\Expr\Variable('a'))], 'stmts' => [new \PhpParser\Node\Stmt\Expression(new \PhpParser\Node\Expr\Variable('inner'))]]), + 'arrow' => new \PhpParser\Node\Expr\ArrowFunction(['expr' => new \PhpParser\Node\Expr\Variable('s')]), + 'dim fetch' => new \PhpParser\Node\Expr\ArrayDimFetch(new \PhpParser\Node\Expr\Variable('a'), new \PhpParser\Node\Expr\Variable('s')), + 'this fetch' => new \PhpParser\Node\Expr\PropertyFetch(new \PhpParser\Node\Expr\Variable('this'), 'p'), + 'nullsafe fetch' => new \PhpParser\Node\Expr\NullsafePropertyFetch(new \PhpParser\Node\Expr\Variable('a'), 'p'), +]; +$erTypes = [ + 'int' => [$vfInt, $vfInt], + 'void' => [new \PHPStanTurbo\VoidType(), new \PHPStanTurbo\VoidType()], + 'string' => [$vfString, $vfString], +]; +$erKnownScopes = ['erScope' => $erScope, 'erOtherScope' => $erOtherScope, 'erWiderScope' => $erWiderScope, 'vehScope' => $vehScope]; +$erDescribeValue = static function ($value) use ($turboNorm, $erKnownScopes, &$erDescribeValue) { + if ($value === null || is_bool($value) || is_int($value) || is_string($value)) { + return $value; + } + if ($value instanceof \PHPStan\Type\Type) { + return 'type:' . $value->describe(\PHPStan\Type\VerbosityLevel::precise()); + } + if ($value instanceof \Closure) { + return 'closure'; + } + if (is_array($value)) { + return array_map($erDescribeValue, $value); + } + if ($value instanceof \PHPStan\Analyser\MutatingScope) { + // the fixture's scopes are shared between the sides: their identity + // matters; a scope derived by a side is described by class only + $known = array_search($value, $erKnownScopes, true); + return $known !== false ? 'scope:' . $known : $turboNorm(get_class($value)); + } + if ($value instanceof \PhpParser\Node) { + // shared between the sides: the identity matters + return $turboNorm(get_class($value)) . '#' . spl_object_id($value); + } + if (is_object($value)) { + return $turboNorm(get_class($value)); + } + return get_debug_type($value); +}; +$erDescribeResult = static function (object $result) use ($erDescribeValue): array { + $d = []; + foreach ((new ReflectionObject($result))->getProperties() as $property) { + $d[$property->getName()] = $property->isInitialized($result) ? $erDescribeValue($property->getValue($result)) : 'uninitialized'; + } + ksort($d); + + return $d; +}; +// the second constructor argument: the service both sides narrow equality checks through +$erDefaultNarrowingHelper = $scContainer->getByType(\PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper::class); +$erSides = ['php' => \PHPStan\Analyser\ExpressionResult::class, 'native' => \PHPStanTurbo\ExpressionResult::class]; +$erResults = []; +foreach ($erSides as $side => $erClass) { + $r = []; + $erExtensionCalls = 0; + $flowA = \PHPStan\Analyser\VariableFlow::read('a'); + foreach ($erN as $exprLabel => $expr) { + foreach ($erTypes as $typeLabel => [$type, $nativeType]) { + $calls = 0; + $typeCallback = static function (bool $native) use ($type, $nativeType, &$calls): \PHPStan\Type\Type { + $calls++; + return $native ? $nativeType : $type; + }; + $specifyCalls = 0; + $specifyCallback = static function (\PHPStan\Analyser\TypeSpecifierContext $context, bool $native) use (&$specifyCalls): \PHPStan\Analyser\SpecifiedTypes { + $specifyCalls++; + return new \PHPStan\Analyser\SpecifiedTypes(); + }; + $extensions = $exprLabel === 'variable ext' ? $erHitExtensions : $erNoExtensions; + $lazy = new $erClass($extensions, $erDefaultNarrowingHelper, $erScope, $erScope, $expr, false, true, [], [], $typeCallback, $specifyCallback, variableFlow: $flowA); + $eager = new $erClass($extensions, $erDefaultNarrowingHelper, $erScope, $erScope, $expr, true, false, [], [], null, $specifyCallback, type: $type, nativeType: $nativeType, containsNullsafe: true); + $row = []; + foreach (['lazy' => $lazy, 'eager' => $eager] as $kind => $result) { + $row[$kind] = [ + 'type' => $result->getType()->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'type again' => $result->getType() === $result->getType(), + 'nativeType' => $result->getNativeType()->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'keepVoid' => $result->getKeepVoidType(false)->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'keepVoidNative' => $result->getKeepVoidType(true)->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'canResolveOwnType' => $result->canResolveOwnType(), + 'hasYield' => $result->hasYield(), + 'isAlwaysTerminating' => $result->isAlwaysTerminating(), + 'containsNullsafe' => $result->containsNullsafe(), + 'scope' => $result->getScope() === $erScope, + 'beforeScope' => $result->getBeforeScope() === $erScope, + 'expr' => $result->getExpr() === $expr, + 'throwPoints' => $result->getThrowPoints(), + 'impurePoints' => $result->getImpurePoints(), + 'variableFlow' => $result->getVariableFlow() === $flowA, + 'argsResult' => $result->getArgsResult(), + 'onScope' => $result->getTypeOnScope($erScope, false)->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'onScopeNative' => $result->getTypeOnScope($erScope, true)->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'onOtherScope' => $result->getTypeOnScope($erOtherScope, false)->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'onPromotedScope' => $result->getTypeOnScope($erOtherScope->doNotTreatPhpDocTypesAsCertain(), false)->describe(\PHPStan\Type\VerbosityLevel::precise()), + 'answersSame' => $result->answersOnScope($erScope, false), + 'answersOther' => $result->answersOnScope($erOtherScope, false), + 'answersOtherNative' => $result->answersOnScope($erOtherScope, true), + 'answersWider' => $result->answersOnScope($erWiderScope, false), + 'askSame' => $result->askScopeVariableStateMatches($erScope, false), + 'askOther' => $result->askScopeVariableStateMatches($erOtherScope, false), + 'askOtherNative' => $result->askScopeVariableStateMatches($erOtherScope, true), + 'askOtherRule' => $result->askScopeVariableStateMatches($erOtherScope, false, true), + 'askWiderRule' => $result->askScopeVariableStateMatches($erWiderScope, false, true), + 'askWider' => $result->askScopeVariableStateMatches($erWiderScope, false), + 'askEmpty' => $result->askScopeVariableStateMatches($vehScope, false, true), + 'specified' => get_class($result->getSpecifiedTypes(\PHPStan\Analyser\TypeSpecifierContext::createTruthy())), + 'specified memo' => $result->getSpecifiedTypes(\PHPStan\Analyser\TypeSpecifierContext::createTruthy()) === $result->getSpecifiedTypes(\PHPStan\Analyser\TypeSpecifierContext::createTruthy()), + 'specified for scope' => get_class($result->getSpecifiedTypesForScope($erScope, \PHPStan\Analyser\TypeSpecifierContext::createFalsey())), + 'created' => $result->getCreatedTypes($vfInt, \PHPStan\Analyser\TypeSpecifierContext::createTruthy()), + 'created for scope' => $result->getCreatedTypesForScope($erScope, $vfInt, \PHPStan\Analyser\TypeSpecifierContext::createTruthy()), + 'truthy' => get_class($result->getTruthyScope()) . ($result->getTruthyScope() === $result->getTruthyScope() ? ' memo' : ''), + 'falsey' => get_class($result->getFalseyScope()) . ($result->getFalseyScope() === $result->getFalseyScope() ? ' memo' : ''), + 'issetability' => $erDescribeResult($result->getIssetabilityResolution($erScope, false)->getLink()), + 'issetability native' => $erDescribeResult($result->getIssetabilityResolution($erScope, true, true)->getLink()), + 'withScope same' => $result->withScope($erScope) === $result, + 'withScope other' => $erDescribeResult($result->withScope($erOtherScope)), + 'finalize' => $erDescribeResult($result->finalize($erOtherScope, true, true, ['t'], ['i'], null)), + 'atAskPosition' => $erDescribeResult($result->atAskPosition($erOtherScope)), + 'atAskPosition same' => $erDescribeResult($result->atAskPosition($erScope)), + 'deviced' => $erDescribeResult($result->onNonNullabilityDevicedScopes($erOtherScope, $erScope)), + 'state' => $erDescribeResult($result), + 'callbackCalls' => $calls, + 'specifyCalls' => $specifyCalls, + ]; + } + $r[$exprLabel . ' / ' . $typeLabel] = $row; + } + } + $r['extension calls'] = $erExtensionCalls; + // the constructor's invariants + foreach ([ + 'callback and type' => static fn () => new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], static fn (bool $n) => $vfInt, static fn () => new \PHPStan\Analyser\SpecifiedTypes(), type: $vfInt, nativeType: $vfInt), + 'nothing' => static fn () => new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], null, static fn () => new \PHPStan\Analyser\SpecifiedTypes()), + 'only resolved type' => static fn () => new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], null, static fn () => new \PHPStan\Analyser\SpecifiedTypes(), resolvedType: $vfInt), + 'type without native' => static fn () => new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], null, static fn () => new \PHPStan\Analyser\SpecifiedTypes(), type: $vfInt), + 'both resolved' => static fn () => new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], null, static fn () => new \PHPStan\Analyser\SpecifiedTypes(), resolvedType: $vfInt, resolvedNativeType: $vfString), + 'not callable' => static fn () => new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], 'no-such-function', static fn () => new \PHPStan\Analyser\SpecifiedTypes()), + ] as $label => $construct) { + try { + $result = $construct(); + $r['invariant ' . $label] = ['ok', $result->getType()->describe(\PHPStan\Type\VerbosityLevel::precise()), $result->getNativeType()->describe(\PHPStan\Type\VerbosityLevel::precise()), $result->canResolveOwnType()]; + } catch (\Throwable $e) { + // a userland TypeError appends ", called in on line " + $r['invariant ' . $label] = [get_class($e), preg_replace('~, called in .*$~', '', str_replace($erClass, 'ExpressionResult', $e->getMessage()))]; + } + } + // the memoized type callback is released once both flavours are resolved + $releaseCalls = 0; + $released = new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['func call'], false, false, [], [], static function (bool $native) use (&$releaseCalls): \PHPStan\Type\Type { + $releaseCalls++; + return $native ? new \PHPStanTurbo\VoidType() : new \PHPStanTurbo\UnionType([new \PHPStanTurbo\VoidType(), new \PHPStanTurbo\IntegerType()]); + }, static fn () => new \PHPStan\Analyser\SpecifiedTypes()); + $r['release'] = [$released->getType()->describe(\PHPStan\Type\VerbosityLevel::precise()), $released->getKeepVoidType(false)->describe(\PHPStan\Type\VerbosityLevel::precise()), $released->getNativeType()->describe(\PHPStan\Type\VerbosityLevel::precise()), $released->getKeepVoidType(true)->describe(\PHPStan\Type\VerbosityLevel::precise()), $releaseCalls, $erDescribeResult($released)]; + // the override results derive the branch scopes lazily + $override = new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erScope, $erScope, $erN['variable a'], false, false, [], [], null, static fn () => new \PHPStan\Analyser\SpecifiedTypes(), type: $vfInt, nativeType: $vfInt); + $overridden = new $erClass($erNoExtensions, $erDefaultNarrowingHelper, $erOtherScope, $erOtherScope, $erN['func call'], false, false, [], [], null, static fn () => new \PHPStan\Analyser\SpecifiedTypes(), truthyScopeOverrideResult: $override, falseyScopeOverrideResult: $override, type: $vfInt, nativeType: $vfInt, createTypesCallback: static fn (\PHPStan\Type\Type $type, \PHPStan\Analyser\TypeSpecifierContext $context, bool $native) => new \PHPStan\Analyser\SpecifiedTypes([spl_object_id($type) => $native])); + $r['override'] = [$overridden->getTruthyScope() === $override->getTruthyScope(), $overridden->getFalseyScope() === $override->getFalseyScope(), $erDescribeResult($overridden->getCreatedTypes($vfInt, \PHPStan\Analyser\TypeSpecifierContext::createTruthy(), true)), $erDescribeResult($overridden->getCreatedTypesForScope($erScope, $vfString, \PHPStan\Analyser\TypeSpecifierContext::createTruthy())), $erDescribeResult($overridden->finalize($erScope, false, false, [], [], null))]; + $erResults[$side] = $r; +} +foreach ($erResults['php'] as $label => $described) { + check($described === ($erResults['native'][$label] ?? null), "ExpressionResult parity ($label): " . json_encode($described) . ' vs ' . json_encode($erResults['native'][$label] ?? null)); +} +check($erResults['php']['extension calls'] > 0 && $erResults['php']['release'][4] === 2, 'ExpressionResult: the fixture exercises the extensions and the callback release'); + +// ---- PhpClassReflectionExtension ---- +// Both sides built from the container's own collaborators with named +// arguments; every public method compared over a fixture of inherited, +// trait, magic, promoted, hooked, attributed, enum, interface and +// signature-mapped members, plus the member-cache memo and eviction. +$covered[\PHPStan\Reflection\Php\PhpClassReflectionExtension::class] = true; +// the fixture declares property hooks and is loaded at run time: PHP 8.4+ +if (PHP_VERSION_ID >= 80400) { + require __DIR__ . '/php-class-reflection-family.php'; +} // ---- differential coverage completeness ---- // Every shadowed class must be exercised by one of the tests/ scripts; the