- 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 organizationpull/17/head
parent
7d34cad400
commit
223c9b22b4
3 changed files with 377 additions and 358 deletions
@ -0,0 +1,196 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* Evaluates static relations for union, intersection, nullable, and literal types. |
||||
*/ |
||||
|
||||
namespace TypePhp\TypeSystem; |
||||
|
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\NodeAbstract; |
||||
|
||||
trait CompositeTypeCheckerTrait |
||||
{ |
||||
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', |
||||
}; |
||||
} |
||||
|
||||
} |
||||
|
||||
Loading…
Reference in new issue