Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Internal/Diagnostic/TypeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace TypePHP\Internal\Diagnostic;

use TypePHP\Internal\Generics\TemplateManager;
use TypePHP\Internal\Util\Config;

/**
Expand Down Expand Up @@ -77,6 +78,19 @@ public static function formatGivenValue(mixed $value, bool $isSensitive = false)
return $value ? 'bool (true)' : 'bool (false)';
}

if (\is_object($value)) {
$className = $value::class;
$bindings = TemplateManager::getBoundTemplatesForInstance($value);

if ($bindings !== []) {
$typeStrings = array_map(static fn ($node): string => (string) $node, array_values($bindings));

return $className . '<' . implode(', ', $typeStrings) . '>';
}

return get_debug_type($value);
}

return get_debug_type($value);
}
}
132 changes: 121 additions & 11 deletions src/Internal/Generics/TemplateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

namespace TypePHP\Internal\Generics;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
Expand Down Expand Up @@ -919,10 +922,21 @@ private static function bindSingleTemplateArgument(
bool $forceBind
): ?ErrorMessage {
if ($expectedTypeNode instanceof IdentifierTypeNode) {
$isBuiltIn = SpecialTypeResolver::isBuiltInTypeKeyword($expectedTypeNode->name);
$isRealType = self::isRealTypeSymbol($expectedTypeNode->name);
if (str_contains($expectedTypeNode->name, '::')) {
$targetClass = explode('::', $expectedTypeNode->name, 2)[0];
if (! self::isRealTypeSymbol($targetClass)) {
return null;
}
} else {
$isBuiltIn = SpecialTypeResolver::isBuiltInTypeKeyword($expectedTypeNode->name);
$isRealType = self::isRealTypeSymbol($expectedTypeNode->name);

if (! $isBuiltIn && ! $isRealType) {
if (! $isBuiltIn && ! $isRealType) {
return null;
}
}
} elseif ($expectedTypeNode instanceof ConstTypeNode && $expectedTypeNode->constExpr instanceof ConstFetchNode && $expectedTypeNode->constExpr->className !== '') {
if (! self::isRealTypeSymbol($expectedTypeNode->constExpr->className)) {
return null;
}
}
Expand Down Expand Up @@ -1148,14 +1162,25 @@ private static function collectInheritedGenericTagBindings(
$resolved = self::resolveTypeNodeAst($typeArg, $hierClass);

if ($resolved instanceof IdentifierTypeNode) {
$isBuiltIn = SpecialTypeResolver::isBuiltInTypeKeyword($resolved->name);
$isRealType = self::isRealTypeSymbol($resolved->name);

if (! $isBuiltIn && ! $isRealType) {
continue;
if (str_contains($resolved->name, '::')) {
$targetClass = explode('::', $resolved->name, 2)[0];
if (! self::isRealTypeSymbol($targetClass)) {
continue;
}
} else {
$isBuiltIn = SpecialTypeResolver::isBuiltInTypeKeyword($resolved->name);
$isRealType = self::isRealTypeSymbol($resolved->name);

if (! $isBuiltIn && ! $isRealType) {
continue;
}

if (isset($declaredTemplateNames[$resolved->name])) {
continue;
}
}

if (isset($declaredTemplateNames[$resolved->name])) {
} elseif ($resolved instanceof ConstTypeNode && $resolved->constExpr instanceof ConstFetchNode && $resolved->constExpr->className !== '') {
if (! self::isRealTypeSymbol($resolved->constExpr->className)) {
continue;
}
}
Expand Down Expand Up @@ -1627,13 +1652,48 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref):
return new IdentifierTypeNode($parent !== false ? $parent->getName() : 'parent');
}

if (str_contains($n->name, '::')) {
[$className, $memberName] = explode('::', $n->name, 2);
$lowerClass = strtolower($className);
if ($lowerClass === 'self' || $lowerClass === 'static' || $lowerClass === '$this') {
$resolvedClass = $ref->getName();
} elseif ($lowerClass === 'parent') {
$parent = $ref->getParentClass();
$resolvedClass = $parent !== false ? $parent->getName() : 'parent';
} else {
$resolvedClass = SpecialTypeResolver::resolveFqcn($className, $ref);
}

return new IdentifierTypeNode($resolvedClass . '::' . $memberName);
}

return new IdentifierTypeNode(SpecialTypeResolver::resolveFqcn($n->name, $ref));
}

if ($n instanceof ThisTypeNode) {
return new IdentifierTypeNode($ref->getName());
}

if ($n instanceof ConstTypeNode) {
if ($n->constExpr instanceof ConstFetchNode && $n->constExpr->className !== '') {
$className = $n->constExpr->className;
$lowerClassName = strtolower($className);

if ($lowerClassName === 'self' || $lowerClassName === 'static' || $lowerClassName === '$this') {
$resolvedClass = $ref->getName();
} elseif ($lowerClassName === 'parent') {
$parent = $ref->getParentClass();
$resolvedClass = $parent !== false ? $parent->getName() : 'parent';
} else {
$resolvedClass = SpecialTypeResolver::resolveFqcn($className, $ref);
}

return new ConstTypeNode(new ConstFetchNode($resolvedClass, $n->constExpr->name));
}

return $n;
}

if ($n instanceof GenericTypeNode) {
$lower = strtolower($n->type->name);
$baseName = match ($lower) {
Expand Down Expand Up @@ -1680,8 +1740,58 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref):
if ($n instanceof ArrayShapeNode) {
$items = [];
foreach ($n->items as $item) {
$keyName = $item->keyName;

if ($keyName instanceof ConstFetchNode && $keyName->className !== '') {
$className = $keyName->className;
$constName = $keyName->name;
$lowerClassName = strtolower($className);

if ($lowerClassName === 'self' || $lowerClassName === 'static' || $lowerClassName === '$this') {
$resolvedClass = $ref->getName();
} elseif ($lowerClassName === 'parent') {
$parent = $ref->getParentClass();
$resolvedClass = $parent !== false ? $parent->getName() : 'parent';
} else {
$resolvedClass = SpecialTypeResolver::resolveFqcn($className, $ref);
}

$resolvedKeyNode = SpecialTypeResolver::resolveConstantKeyValue($resolvedClass, $constName);
$keyName = $resolvedKeyNode ?? new ConstFetchNode($resolvedClass, $constName);
} elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) {
[$className, $constName] = explode('::', $keyName->name, 2);
$lowerClassName = strtolower($className);

if ($lowerClassName === 'self' || $lowerClassName === 'static' || $lowerClassName === '$this') {
$resolvedClass = $ref->getName();
} elseif ($lowerClassName === 'parent') {
$parent = $ref->getParentClass();
$resolvedClass = $parent !== false ? $parent->getName() : 'parent';
} else {
$resolvedClass = SpecialTypeResolver::resolveFqcn($className, $ref);
}

$resolvedKeyNode = SpecialTypeResolver::resolveConstantKeyValue($resolvedClass, $constName);
$keyName = $resolvedKeyNode ?? new IdentifierTypeNode($resolvedClass . '::' . $constName);
} elseif ($keyName instanceof ConstExprStringNode && str_contains($keyName->value, '::')) {
[$className, $constName] = explode('::', $keyName->value, 2);
$lowerClassName = strtolower($className);

if ($lowerClassName === 'self' || $lowerClassName === 'static' || $lowerClassName === '$this') {
$resolvedClass = $ref->getName();
} elseif ($lowerClassName === 'parent') {
$parent = $ref->getParentClass();
$resolvedClass = $parent !== false ? $parent->getName() : 'parent';
} else {
$resolvedClass = SpecialTypeResolver::resolveFqcn($className, $ref);
}

$resolvedKeyNode = SpecialTypeResolver::resolveConstantKeyValue($resolvedClass, $constName);
$keyName = $resolvedKeyNode ?? new ConstExprStringNode($resolvedClass . '::' . $constName, $keyName->quoteType);
}

$items[] = new ArrayShapeItemNode(
$item->keyName,
$keyName,
$item->optional,
self::resolveTypeNodeAst($item->valueType, $ref)
);
Expand Down
35 changes: 33 additions & 2 deletions src/Internal/Resolver/SpecialTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,18 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
return clone $node;
}

if (str_contains($node->name, '::')) {
[$className, $member] = explode('::', $node->name, 2);
$lowerClass = strtolower($className);
if (\in_array($lowerClass, ['self', 'static', 'parent', '$this'], strict: true)) {
return clone $node;
}

$resolvedClass = self::resolveFqcnForFile($className, $file);

return $resolvedClass !== $className ? new IdentifierTypeNode($resolvedClass . '::' . $member) : clone $node;
}

$fqcn = self::resolveFqcnForFile($node->name, $file);

return $fqcn !== $node->name ? new IdentifierTypeNode($fqcn) : clone $node;
Expand Down Expand Up @@ -542,6 +554,25 @@ private static function resolveIdentifier(
}
}

if (str_contains($node->name, '::')) {
[$className, $member] = explode('::', $node->name, 2);
$lowerClass = strtolower($className);
if (($lowerClass === 'static' || $lowerClass === '$this') && $thisObj !== null) {
$resolvedClass = $thisObj::class;
} elseif ($lowerClass === 'static' && \is_string($context) && ($callingClass = strstr($context, '::', true)) !== false && self::symbolExists($callingClass)) {
$resolvedClass = $callingClass;
} elseif (($lowerClass === 'self' || $lowerClass === 'static' || $lowerClass === '$this') && $declaringClass !== null) {
$resolvedClass = $declaringClass;
} elseif ($lowerClass === 'parent' && $declaringClass !== null) {
$parentClass = get_parent_class($declaringClass);
$resolvedClass = $parentClass !== false ? $parentClass : $className;
} else {
$resolvedClass = self::resolveFqcn($className, $ref);
}

return $resolvedClass !== $className ? new IdentifierTypeNode($resolvedClass . '::' . $member) : $node;
}

$fqcn = self::resolveFqcn($node->name, $ref);

return $fqcn !== $node->name ? new IdentifierTypeNode($fqcn) : $node;
Expand All @@ -556,7 +587,7 @@ private static function resolveConstType(ConstTypeNode $node, ?string $declaring
$className = $node->constExpr->className;
$lowerClassName = strtolower($className);

if ($lowerClassName === 'self' && $declaringClass !== null) {
if (($lowerClassName === 'self' || $lowerClassName === 'static' || $lowerClassName === '$this') && $declaringClass !== null) {
$resolvedClass = $declaringClass;
} elseif ($lowerClassName === 'parent' && $declaringClass !== null) {
$parentClass = get_parent_class($declaringClass);
Expand Down Expand Up @@ -954,7 +985,7 @@ private static function resolveConstantOffsetValue(string $fqcn, string $constNa
return null;
}

private static function resolveConstantKeyValue(string $fqcn, string $constName): ConstExprStringNode|ConstExprIntegerNode|null
public static function resolveConstantKeyValue(string $fqcn, string $constName): ConstExprStringNode|ConstExprIntegerNode|null
{
$val = self::getClassConstant($fqcn, $constName);
if (\is_string($val)) {
Expand Down
4 changes: 2 additions & 2 deletions src/Internal/RuntimeTypeChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,12 @@ public static function setupScope(string $function, array $vars, object|string|n
);

if ($err !== null) {
TemplateManager::popCallFrame($effectiveFunction);

if (IgnoreManager::isCallerIgnored()) {
return null;
}

TemplateManager::popCallFrame($effectiveFunction);

$handled = ViolationCollector::handle($err, 'parameter', null, function: $effectiveFunction);
if ($handled instanceof ErrorMessage) {
return $handled;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\Fixtures\Commands\Actions;

use TypePHP\Tests\Fixtures\Commands\Core\HasLocationOptions;
use TypePHP\Tests\Fixtures\Commands\Model\Location;

/**
* @use HasLocationOptions<array{statuses: list<Location::STATUS_*>}>
*/
final class ClassLevelTraitLocationConsumer
{
use HasLocationOptions;
}
19 changes: 19 additions & 0 deletions tests/Fixtures/Commands/Actions/ExecuteLocationsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\Fixtures\Commands\Actions;

use TypePHP\Tests\Fixtures\Commands\Core\CommandInterface;
use TypePHP\Tests\Fixtures\Commands\Model\Location;

/**
* @implements CommandInterface<array{statuses: list<Location::STATUS_*>}>
*/
final class ExecuteLocationsCommand implements CommandInterface
{
public function execute(array $options): array
{
return $options;
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/Commands/Actions/GetLocationByStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\Fixtures\Commands\Actions;

use TypePHP\Tests\Fixtures\Commands\Core\Command;
use TypePHP\Tests\Fixtures\Commands\Model\Location;

/**
* @extends Command<array{status: Location::STATUS_ONLINE}>
*/
final class GetLocationByStatus extends Command
{
}
15 changes: 15 additions & 0 deletions tests/Fixtures/Commands/Actions/GetLocationDirectValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\Fixtures\Commands\Actions;

use TypePHP\Tests\Fixtures\Commands\Core\ValueCommand;
use TypePHP\Tests\Fixtures\Commands\Model\Location;

/**
* @extends ValueCommand<Location::STATUS_*>
*/
final class GetLocationDirectValue extends ValueCommand
{
}
15 changes: 15 additions & 0 deletions tests/Fixtures/Commands/Actions/GetLocationMapKeyValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\Fixtures\Commands\Actions;

use TypePHP\Tests\Fixtures\Commands\Core\ValueCommand;
use TypePHP\Tests\Fixtures\Commands\Model\Location;

/**
* @extends ValueCommand<key-of<Location::STATUS_MAP>>
*/
final class GetLocationMapKeyValue extends ValueCommand
{
}
15 changes: 15 additions & 0 deletions tests/Fixtures/Commands/Actions/GetLocationMaskValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\Fixtures\Commands\Actions;

use TypePHP\Tests\Fixtures\Commands\Core\ValueCommand;
use TypePHP\Tests\Fixtures\Commands\Model\Location;

/**
* @extends ValueCommand<int-mask-of<Location::STATUS_*>>
*/
final class GetLocationMaskValue extends ValueCommand
{
}
Loading
Loading