From 223c9b22b45c5694caca08afcd9cf4fe10f4ba43 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 11 Jul 2026 16:12:19 +0800 Subject: [PATCH] refactor(compiler): move composite type checker and property access methods to traits - Extract composite type checking logic to new CompositeTypeCheckerTrait - Move dynamic property access methods from CompilerBase to PropertyAccessTrait - Add use statement for CompositeTypeCheckerTrait in CompilerBase - Remove duplicate method implementations from CompilerBase - Maintain all existing functionality through trait-based code organization --- src/CompilerBase.php | 360 +------------------ src/Parser/PropertyAccessTrait.php | 179 +++++++++ src/TypeSystem/CompositeTypeCheckerTrait.php | 196 ++++++++++ 3 files changed, 377 insertions(+), 358 deletions(-) create mode 100644 src/TypeSystem/CompositeTypeCheckerTrait.php diff --git a/src/CompilerBase.php b/src/CompilerBase.php index cba8d74e..b16bc82d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -60,6 +60,7 @@ use TypePhp\Resolver\PropertyAssignTypeInfo; use TypePhp\Resolver\PropertyWriteTarget; use TypePhp\Resolver\StaticPropertyFetchResolution; use TypePhp\Resolver\StaticPropertyFetchTarget; +use TypePhp\TypeSystem\CompositeTypeCheckerTrait; use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\ArrayItem; @@ -82,6 +83,7 @@ use PhpParser\PrettyPrinter; class CompilerBase implements PropertyAccessContext { public const string DEFAULT_PHP_VERSION = '8.5'; + use CompositeTypeCheckerTrait; use AstNodeType; use FuncCallOptimizer; use AnonClassGenerator; @@ -5100,184 +5102,6 @@ class CompilerBase implements PropertyAccessContext } } - protected function emitDynamicPropertyRead(string $object, string $property): string - { - return "{$object}.getProperty({$property})"; - } - - protected function emitDynamicPropertyWrite(string $object, string $property, string $value): string - { - $scope = $this->class ? $this->getClassEntryPtr($this->getFullClassName()) : 'nullptr'; - return 'typephp_write_property_scoped(' - . $object . ', ' . $property . ', ' . $value . ', ' . $scope . ')'; - } - - protected function emitDynamicPropertyTargetRead(PropertyWriteTarget $target): string - { - $this->assertDynamicPropertyTarget($target); - - return $this->emitDynamicPropertyRead($target->getDynamicObjectExpr(), $target->getDynamicPropertyExpr()); - } - - protected function emitDynamicPropertyTargetWrite(PropertyWriteTarget $target, string $value): string - { - $this->assertDynamicPropertyTarget($target); - - return $this->emitDynamicPropertyWrite($target->getDynamicObjectExpr(), $target->getDynamicPropertyExpr(), $value); - } - - protected function emitDynamicPropertyTargetUnset(PropertyWriteTarget $target): string - { - $this->assertDynamicPropertyTarget($target); - - return $target->getDynamicObjectExpr() . '.unsetProperty(' . $target->getDynamicPropertyExpr() . ')'; - } - - protected function emitDynamicPropertyTargetRef(PropertyWriteTarget $target): string - { - $this->assertDynamicPropertyTarget($target); - - return $target->getDynamicObjectExpr() . '.attrRef(' . $target->getDynamicPropertyExpr() . ')'; - } - - protected function emitDynamicPropertyTargetAppendArray(PropertyWriteTarget $target, string $value): string - { - $this->assertDynamicPropertyTarget($target); - - return $this->emitDynamicPropertyAppendArray( - $target->getDynamicObjectExpr(), - $target->getDynamicPropertyExpr(), - $value - ); - } - - protected function emitDynamicPropertyTargetUpdateArray(PropertyWriteTarget $target, string $dim, string $value): string - { - $this->assertDynamicPropertyTarget($target); - - return $this->emitDynamicPropertyUpdateArray( - $target->getDynamicObjectExpr(), - $target->getDynamicPropertyExpr(), - $dim, - $value - ); - } - - protected function canEmitDynamicPropertyTarget(?PropertyWriteTarget $target): bool - { - return $target !== null && $target->isDynamicObjectProperty(); - } - - protected function emitDynamicPropertyFetchRead(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string - { - if ($this->canEmitDynamicPropertyTarget($target)) { - return $this->emitDynamicPropertyTargetRead($target); - } - - return $this->emitDynamicPropertyRead( - $this->parseIdentifier($expr->var), - $this->identifierToStr($expr->name, literal: true) - ); - } - - protected function emitDynamicPropertyFetchWrite(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string - { - if ($this->canEmitDynamicPropertyTarget($target)) { - return $this->emitDynamicPropertyTargetWrite($target, $value); - } - - return $this->emitDynamicPropertyWrite( - $this->parseIdentifier($expr->var), - $this->identifierToStr($expr->name, literal: true), - $value - ); - } - - protected function getDynamicPropertyFetchObjectExpr(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string - { - if ($this->canEmitDynamicPropertyTarget($target)) { - return $target->getDynamicObjectExpr(); - } - - return $this->parseIdentifier($expr->var); - } - - protected function emitDynamicPropertyFetchUnset(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string - { - if ($this->canEmitDynamicPropertyTarget($target)) { - return $this->emitDynamicPropertyTargetUnset($target); - } - - return $this->parseIdentifier($expr->var) . '.unsetProperty(' . $this->identifierToStr($expr->name, literal: true) . ')'; - } - - protected function emitDynamicPropertyFetchAppendArray(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string - { - if ($this->canEmitDynamicPropertyTarget($target)) { - return $this->emitDynamicPropertyTargetAppendArray($target, $value); - } - - return $this->emitDynamicPropertyAppendArray( - $this->parseIdentifier($expr->var), - $this->identifierToStr($expr->name, literal: true), - $value - ); - } - - protected function emitDynamicPropertyFetchUpdateArray(Expr\PropertyFetch $expr, string $dim, string $value, ?PropertyWriteTarget $target = null): string - { - if ($this->canEmitDynamicPropertyTarget($target)) { - return $this->emitDynamicPropertyTargetUpdateArray($target, $dim, $value); - } - - return $this->emitDynamicPropertyUpdateArray( - $this->parseIdentifier($expr->var), - $this->identifierToStr($expr->name, literal: true), - $dim, - $value - ); - } - - protected function emitDynamicPropertyAppendArray(string $object, string $property, string $value): string - { - return "{$object}.attr({$property}, true).newItem() = {$value}"; - } - - protected function emitDynamicPropertyUpdateArray(string $object, string $property, string $dim, string $value): string - { - return "{$object}.attr({$property}, true).item({$dim}, true) = {$value}"; - } - - protected function assertDynamicPropertyTarget(PropertyWriteTarget $target): void - { - if (!$target->isDynamicObjectProperty()) { - $this->fatalError($target->node, 'Internal error: property write target is not a dynamic object property'); - } - } - - protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string - { - $target = $this->preparePropertyWriteTarget($expr); - if ($this->canEmitDynamicPropertyTarget($target)) { - $objectExpr = $target->getDynamicObjectExpr(); - if (!$this->hasVar($objectExpr)) { - $this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`'); - } - return $this->emitDynamicPropertyTargetRef($target); - } - - if (!$this->isVarExpr($expr->var)) { - return $this->parseExpr($expr->var) . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; - } - - $objectExpr = $this->parseIdentifier($expr->var); - if (!$this->hasVar($objectExpr)) { - $this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`'); - } - - return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; - } - protected function getChainedFunc(string $op): string { return match ($op) { @@ -5778,186 +5602,6 @@ class CompilerBase implements PropertyAccessContext * The outer descriptor list is a union (OR); an allOf entry represents an * intersection (AND). Nullable is represented by an isNull union member. */ - protected function checkCompositeTypeAssignment( - NodeAbstract $errorNode, - array $typeCheck, - string $typeStr, - NodeAbstract $value, - string $context - ): int { - $relation = $this->compositeTypeRelation($value, $typeCheck); - if ($relation !== self::COMPOSITE_TYPE_MISMATCH) { - return $relation; - } - - $valueType = $this->staticTypeNameOfExpr($value); - $this->fatalError($errorNode, "Cannot assign {$valueType} to {$context} of type `{$typeStr}`"); - } - - protected function compositeTypeRelation(NodeAbstract $value, array $clauses): int - { - // TYPE_VAR means that the expression is dynamic or its result cannot - // be represented by the current scalar type system. It must retain the - // runtime type check. - if ($this->detectTypeOfExpr($value) === self::TYPE_VAR && !$this->isNullExpr($value)) { - return self::COMPOSITE_TYPE_UNKNOWN; - } - - $hasUnknown = false; - foreach ($clauses as $clause) { - $relation = $this->compositeTypeClauseRelation($value, $clause); - if ($relation === self::COMPOSITE_TYPE_MATCH) { - return self::COMPOSITE_TYPE_MATCH; - } - if ($relation === self::COMPOSITE_TYPE_UNKNOWN) { - $hasUnknown = true; - } - } - return $hasUnknown ? self::COMPOSITE_TYPE_UNKNOWN : self::COMPOSITE_TYPE_MISMATCH; - } - - protected function compositeTypeClauseRelation(NodeAbstract $value, array $clause): int - { - if (($clause['kind'] ?? '') === 'allOf') { - $hasUnknown = false; - foreach ($clause['types'] ?? [] as $entry) { - $relation = $this->compositeTypeEntryRelation($value, $entry); - if ($relation === self::COMPOSITE_TYPE_MISMATCH) { - return self::COMPOSITE_TYPE_MISMATCH; - } - if ($relation === self::COMPOSITE_TYPE_UNKNOWN) { - $hasUnknown = true; - } - } - return $hasUnknown ? self::COMPOSITE_TYPE_UNKNOWN : self::COMPOSITE_TYPE_MATCH; - } - return $this->compositeTypeEntryRelation($value, $clause); - } - - protected function compositeTypeEntryRelation(NodeAbstract $value, array $entry): int - { - $kind = $entry['kind'] ?? ''; - if ($kind === 'isNull') { - return $this->isNullExpr($value) ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH; - } - - $type = $this->detectTypeOfExpr($value); - return match ($kind) { - 'isInt' => $this->exactCompositeTypeRelation($type, self::TYPE_INT), - // PHP permits int -> float widening. It is compatible but still - // needs conversion, so retain the runtime normalization path. - 'isFloat' => $type === self::TYPE_INT - ? self::COMPOSITE_TYPE_UNKNOWN - : $this->exactCompositeTypeRelation($type, self::TYPE_FLOAT), - 'isBool' => $this->exactCompositeTypeRelation($type, self::TYPE_BOOL), - 'isString' => $this->exactCompositeTypeRelation($type, self::TYPE_STR), - 'isArray' => $this->exactCompositeTypeRelation($type, self::TYPE_ARRAY), - 'isObject' => $this->exactCompositeTypeRelation($type, self::TYPE_OBJECT), - 'isTrue' => $this->compositeLiteralBoolRelation($value, true), - 'isFalse' => $this->compositeLiteralBoolRelation($value, false), - 'isResource' => $this->exactCompositeTypeRelation($type, self::TYPE_RESOURCE), - 'callable' => $this->compositeCallableRelation($value, $type), - 'iterable' => $this->compositeIterableRelation($value, $type), - 'instanceof' => $this->compositeObjectEntryRelation($value, $entry), - default => self::COMPOSITE_TYPE_UNKNOWN, - }; - } - - protected function exactCompositeTypeRelation(string $actual, string $expected): int - { - return $actual === $expected ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH; - } - - protected function compositeLiteralBoolRelation(NodeAbstract $value, bool $expected): int - { - if ($this->isScalarBool($value)) { - $actual = strcasecmp($value->name->toString(), 'true') === 0; - return $actual === $expected ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH; - } - return $this->detectTypeOfExpr($value) === self::TYPE_BOOL - ? self::COMPOSITE_TYPE_UNKNOWN - : self::COMPOSITE_TYPE_MISMATCH; - } - - protected function compositeCallableRelation(NodeAbstract $value, string $type): int - { - if ($type === self::TYPE_STR || $type === self::TYPE_ARRAY || $type === self::TYPE_OBJECT) { - return self::COMPOSITE_TYPE_UNKNOWN; - } - return self::COMPOSITE_TYPE_MISMATCH; - } - - protected function compositeIterableRelation(NodeAbstract $value, string $type): int - { - if ($type === self::TYPE_ARRAY) { - return self::COMPOSITE_TYPE_MATCH; - } - if ($type !== self::TYPE_OBJECT) { - return self::COMPOSITE_TYPE_MISMATCH; - } - return $this->compositeObjectTypeRelation($value, 'Traversable'); - } - - protected function compositeObjectEntryRelation(NodeAbstract $value, array $entry): int - { - if ($this->detectTypeOfExpr($value) !== self::TYPE_OBJECT) { - return self::COMPOSITE_TYPE_MISMATCH; - } - - return $this->compositeObjectTypeRelation($value, $entry['class'] ?? ''); - } - - protected function compositeObjectTypeRelation(NodeAbstract $value, string $expected): int - { - $class = $this->detectDeclaredClassOfExpr($value); - if ($class === '') { - return self::COMPOSITE_TYPE_UNKNOWN; - } - - if ($expected === '' || $expected === 'static') { - return self::COMPOSITE_TYPE_UNKNOWN; - } - - $actualKnown = $this->hasClass($class) - || $this->hasInterface($class) - || $this->isInternalClass($class) - || $this->isInternalInterface($class); - $expectedKnown = $this->hasClass($expected) - || $this->hasInterface($expected) - || $this->isInternalClass($expected) - || $this->isInternalInterface($expected); - if (!$actualKnown || !$expectedKnown) { - return self::COMPOSITE_TYPE_UNKNOWN; - } - - return $this->isObjectClassStaticallyAssignableTo($class, $expected) - ? self::COMPOSITE_TYPE_MATCH - : self::COMPOSITE_TYPE_MISMATCH; - } - - protected function isNullExpr(NodeAbstract $expr): bool - { - return $expr instanceof Expr\ConstFetch - && strcasecmp($this->parseIdentifier($expr->name), 'null') === 0; - } - - protected function staticTypeNameOfExpr(NodeAbstract $expr): string - { - if ($this->isNullExpr($expr)) { - return 'null'; - } - $type = $this->detectTypeOfExpr($expr); - return match ($type) { - self::TYPE_INT => 'int', - self::TYPE_FLOAT => 'float', - self::TYPE_BOOL => 'bool', - self::TYPE_STR => 'string', - self::TYPE_ARRAY => 'array', - self::TYPE_OBJECT => 'object', - default => 'mixed', - }; - } - protected function mustNoCall(NodeAbstract $node): void { $nodeFinder = new NodeFinder(); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index ce636ec6..be2c2950 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -21,6 +21,185 @@ use TypePhp\Symbol; trait PropertyAccessTrait { + protected function emitDynamicPropertyRead(string $object, string $property): string + { + return "{$object}.getProperty({$property})"; + } + + protected function emitDynamicPropertyWrite(string $object, string $property, string $value): string + { + $scope = $this->class ? $this->getClassEntryPtr($this->getFullClassName()) : 'nullptr'; + return 'typephp_write_property_scoped(' + . $object . ', ' . $property . ', ' . $value . ', ' . $scope . ')'; + } + + protected function emitDynamicPropertyTargetRead(PropertyWriteTarget $target): string + { + $this->assertDynamicPropertyTarget($target); + + return $this->emitDynamicPropertyRead($target->getDynamicObjectExpr(), $target->getDynamicPropertyExpr()); + } + + protected function emitDynamicPropertyTargetWrite(PropertyWriteTarget $target, string $value): string + { + $this->assertDynamicPropertyTarget($target); + + return $this->emitDynamicPropertyWrite($target->getDynamicObjectExpr(), $target->getDynamicPropertyExpr(), $value); + } + + protected function emitDynamicPropertyTargetUnset(PropertyWriteTarget $target): string + { + $this->assertDynamicPropertyTarget($target); + + return $target->getDynamicObjectExpr() . '.unsetProperty(' . $target->getDynamicPropertyExpr() . ')'; + } + + protected function emitDynamicPropertyTargetRef(PropertyWriteTarget $target): string + { + $this->assertDynamicPropertyTarget($target); + + return $target->getDynamicObjectExpr() . '.attrRef(' . $target->getDynamicPropertyExpr() . ')'; + } + + protected function emitDynamicPropertyTargetAppendArray(PropertyWriteTarget $target, string $value): string + { + $this->assertDynamicPropertyTarget($target); + + return $this->emitDynamicPropertyAppendArray( + $target->getDynamicObjectExpr(), + $target->getDynamicPropertyExpr(), + $value + ); + } + + protected function emitDynamicPropertyTargetUpdateArray(PropertyWriteTarget $target, string $dim, string $value): string + { + $this->assertDynamicPropertyTarget($target); + + return $this->emitDynamicPropertyUpdateArray( + $target->getDynamicObjectExpr(), + $target->getDynamicPropertyExpr(), + $dim, + $value + ); + } + + protected function canEmitDynamicPropertyTarget(?PropertyWriteTarget $target): bool + { + return $target !== null && $target->isDynamicObjectProperty(); + } + + protected function emitDynamicPropertyFetchRead(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string + { + if ($this->canEmitDynamicPropertyTarget($target)) { + return $this->emitDynamicPropertyTargetRead($target); + } + + return $this->emitDynamicPropertyRead( + $this->parseIdentifier($expr->var), + $this->identifierToStr($expr->name, literal: true) + ); + } + + protected function emitDynamicPropertyFetchWrite(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string + { + if ($this->canEmitDynamicPropertyTarget($target)) { + return $this->emitDynamicPropertyTargetWrite($target, $value); + } + + return $this->emitDynamicPropertyWrite( + $this->parseIdentifier($expr->var), + $this->identifierToStr($expr->name, literal: true), + $value + ); + } + + protected function getDynamicPropertyFetchObjectExpr(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string + { + if ($this->canEmitDynamicPropertyTarget($target)) { + return $target->getDynamicObjectExpr(); + } + + return $this->parseIdentifier($expr->var); + } + + protected function emitDynamicPropertyFetchUnset(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string + { + if ($this->canEmitDynamicPropertyTarget($target)) { + return $this->emitDynamicPropertyTargetUnset($target); + } + + return $this->parseIdentifier($expr->var) . '.unsetProperty(' . $this->identifierToStr($expr->name, literal: true) . ')'; + } + + protected function emitDynamicPropertyFetchAppendArray(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string + { + if ($this->canEmitDynamicPropertyTarget($target)) { + return $this->emitDynamicPropertyTargetAppendArray($target, $value); + } + + return $this->emitDynamicPropertyAppendArray( + $this->parseIdentifier($expr->var), + $this->identifierToStr($expr->name, literal: true), + $value + ); + } + + protected function emitDynamicPropertyFetchUpdateArray(Expr\PropertyFetch $expr, string $dim, string $value, ?PropertyWriteTarget $target = null): string + { + if ($this->canEmitDynamicPropertyTarget($target)) { + return $this->emitDynamicPropertyTargetUpdateArray($target, $dim, $value); + } + + return $this->emitDynamicPropertyUpdateArray( + $this->parseIdentifier($expr->var), + $this->identifierToStr($expr->name, literal: true), + $dim, + $value + ); + } + + protected function emitDynamicPropertyAppendArray(string $object, string $property, string $value): string + { + return "{$object}.attr({$property}, true).newItem() = {$value}"; + } + + protected function emitDynamicPropertyUpdateArray(string $object, string $property, string $dim, string $value): string + { + return "{$object}.attr({$property}, true).item({$dim}, true) = {$value}"; + } + + protected function assertDynamicPropertyTarget(PropertyWriteTarget $target): void + { + if (!$target->isDynamicObjectProperty()) { + $this->fatalError($target->node, 'Internal error: property write target is not a dynamic object property'); + } + } + + protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string + { + $target = $this->preparePropertyWriteTarget($expr); + if ($this->canEmitDynamicPropertyTarget($target)) { + $objectExpr = $target->getDynamicObjectExpr(); + if (!$this->hasVar($objectExpr)) { + $this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`'); + } + return $this->emitDynamicPropertyTargetRef($target); + } + + if (!$this->isVarExpr($expr->var)) { + return $this->parseExpr($expr->var) . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; + } + + $objectExpr = $this->parseIdentifier($expr->var); + if (!$this->hasVar($objectExpr)) { + $this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`'); + } + + return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; + } + + protected function resolveNativeStaticPropertyFetch(Expr\StaticPropertyFetch $expr): ?StaticPropertyFetchResolution { $target = $this->resolveStaticPropertyFetchTarget($expr); diff --git a/src/TypeSystem/CompositeTypeCheckerTrait.php b/src/TypeSystem/CompositeTypeCheckerTrait.php new file mode 100644 index 00000000..0722b4a9 --- /dev/null +++ b/src/TypeSystem/CompositeTypeCheckerTrait.php @@ -0,0 +1,196 @@ +compositeTypeRelation($value, $typeCheck); + if ($relation !== self::COMPOSITE_TYPE_MISMATCH) { + return $relation; + } + + $valueType = $this->staticTypeNameOfExpr($value); + $this->fatalError($errorNode, "Cannot assign {$valueType} to {$context} of type `{$typeStr}`"); + } + + protected function compositeTypeRelation(NodeAbstract $value, array $clauses): int + { + // TYPE_VAR means that the expression is dynamic or its result cannot + // be represented by the current scalar type system. It must retain the + // runtime type check. + if ($this->detectTypeOfExpr($value) === self::TYPE_VAR && !$this->isNullExpr($value)) { + return self::COMPOSITE_TYPE_UNKNOWN; + } + + $hasUnknown = false; + foreach ($clauses as $clause) { + $relation = $this->compositeTypeClauseRelation($value, $clause); + if ($relation === self::COMPOSITE_TYPE_MATCH) { + return self::COMPOSITE_TYPE_MATCH; + } + if ($relation === self::COMPOSITE_TYPE_UNKNOWN) { + $hasUnknown = true; + } + } + return $hasUnknown ? self::COMPOSITE_TYPE_UNKNOWN : self::COMPOSITE_TYPE_MISMATCH; + } + + protected function compositeTypeClauseRelation(NodeAbstract $value, array $clause): int + { + if (($clause['kind'] ?? '') === 'allOf') { + $hasUnknown = false; + foreach ($clause['types'] ?? [] as $entry) { + $relation = $this->compositeTypeEntryRelation($value, $entry); + if ($relation === self::COMPOSITE_TYPE_MISMATCH) { + return self::COMPOSITE_TYPE_MISMATCH; + } + if ($relation === self::COMPOSITE_TYPE_UNKNOWN) { + $hasUnknown = true; + } + } + return $hasUnknown ? self::COMPOSITE_TYPE_UNKNOWN : self::COMPOSITE_TYPE_MATCH; + } + return $this->compositeTypeEntryRelation($value, $clause); + } + + protected function compositeTypeEntryRelation(NodeAbstract $value, array $entry): int + { + $kind = $entry['kind'] ?? ''; + if ($kind === 'isNull') { + return $this->isNullExpr($value) ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH; + } + + $type = $this->detectTypeOfExpr($value); + return match ($kind) { + 'isInt' => $this->exactCompositeTypeRelation($type, self::TYPE_INT), + // PHP permits int -> float widening. It is compatible but still + // needs conversion, so retain the runtime normalization path. + 'isFloat' => $type === self::TYPE_INT + ? self::COMPOSITE_TYPE_UNKNOWN + : $this->exactCompositeTypeRelation($type, self::TYPE_FLOAT), + 'isBool' => $this->exactCompositeTypeRelation($type, self::TYPE_BOOL), + 'isString' => $this->exactCompositeTypeRelation($type, self::TYPE_STR), + 'isArray' => $this->exactCompositeTypeRelation($type, self::TYPE_ARRAY), + 'isObject' => $this->exactCompositeTypeRelation($type, self::TYPE_OBJECT), + 'isTrue' => $this->compositeLiteralBoolRelation($value, true), + 'isFalse' => $this->compositeLiteralBoolRelation($value, false), + 'isResource' => $this->exactCompositeTypeRelation($type, self::TYPE_RESOURCE), + 'callable' => $this->compositeCallableRelation($value, $type), + 'iterable' => $this->compositeIterableRelation($value, $type), + 'instanceof' => $this->compositeObjectEntryRelation($value, $entry), + default => self::COMPOSITE_TYPE_UNKNOWN, + }; + } + + protected function exactCompositeTypeRelation(string $actual, string $expected): int + { + return $actual === $expected ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH; + } + + protected function compositeLiteralBoolRelation(NodeAbstract $value, bool $expected): int + { + if ($this->isScalarBool($value)) { + $actual = strcasecmp($value->name->toString(), 'true') === 0; + return $actual === $expected ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH; + } + return $this->detectTypeOfExpr($value) === self::TYPE_BOOL + ? self::COMPOSITE_TYPE_UNKNOWN + : self::COMPOSITE_TYPE_MISMATCH; + } + + protected function compositeCallableRelation(NodeAbstract $value, string $type): int + { + if ($type === self::TYPE_STR || $type === self::TYPE_ARRAY || $type === self::TYPE_OBJECT) { + return self::COMPOSITE_TYPE_UNKNOWN; + } + return self::COMPOSITE_TYPE_MISMATCH; + } + + protected function compositeIterableRelation(NodeAbstract $value, string $type): int + { + if ($type === self::TYPE_ARRAY) { + return self::COMPOSITE_TYPE_MATCH; + } + if ($type !== self::TYPE_OBJECT) { + return self::COMPOSITE_TYPE_MISMATCH; + } + return $this->compositeObjectTypeRelation($value, 'Traversable'); + } + + protected function compositeObjectEntryRelation(NodeAbstract $value, array $entry): int + { + if ($this->detectTypeOfExpr($value) !== self::TYPE_OBJECT) { + return self::COMPOSITE_TYPE_MISMATCH; + } + + return $this->compositeObjectTypeRelation($value, $entry['class'] ?? ''); + } + + protected function compositeObjectTypeRelation(NodeAbstract $value, string $expected): int + { + $class = $this->detectDeclaredClassOfExpr($value); + if ($class === '') { + return self::COMPOSITE_TYPE_UNKNOWN; + } + + if ($expected === '' || $expected === 'static') { + return self::COMPOSITE_TYPE_UNKNOWN; + } + + $actualKnown = $this->hasClass($class) + || $this->hasInterface($class) + || $this->isInternalClass($class) + || $this->isInternalInterface($class); + $expectedKnown = $this->hasClass($expected) + || $this->hasInterface($expected) + || $this->isInternalClass($expected) + || $this->isInternalInterface($expected); + if (!$actualKnown || !$expectedKnown) { + return self::COMPOSITE_TYPE_UNKNOWN; + } + + return $this->isObjectClassStaticallyAssignableTo($class, $expected) + ? self::COMPOSITE_TYPE_MATCH + : self::COMPOSITE_TYPE_MISMATCH; + } + + protected function isNullExpr(NodeAbstract $expr): bool + { + return $expr instanceof Expr\ConstFetch + && strcasecmp($this->parseIdentifier($expr->name), 'null') === 0; + } + + protected function staticTypeNameOfExpr(NodeAbstract $expr): string + { + if ($this->isNullExpr($expr)) { + return 'null'; + } + $type = $this->detectTypeOfExpr($expr); + return match ($type) { + self::TYPE_INT => 'int', + self::TYPE_FLOAT => 'float', + self::TYPE_BOOL => 'bool', + self::TYPE_STR => 'string', + self::TYPE_ARRAY => 'array', + self::TYPE_OBJECT => 'object', + default => 'mixed', + }; + } + +} +