Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement offset/index access type #1318

Merged
merged 3 commits into from
May 16, 2022
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
16 changes: 16 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
Expand Down Expand Up @@ -71,6 +72,7 @@
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\OffsetAccessType;
use PHPStan\Type\ResourceType;
use PHPStan\Type\StaticType;
use PHPStan\Type\StringType;
Expand Down Expand Up @@ -157,6 +159,8 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
return $this->resolveArrayShapeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof ConstTypeNode) {
return $this->resolveConstTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof OffsetAccessTypeNode) {
return $this->resolveOffsetAccessNode($typeNode, $nameScope);
}

return new ErrorType();
Expand Down Expand Up @@ -904,6 +908,18 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
return new ErrorType();
}

private function resolveOffsetAccessNode(OffsetAccessTypeNode $typeNode, NameScope $nameScope): Type
{
$type = $this->resolve($typeNode->type, $nameScope);
$offset = $this->resolve($typeNode->offset, $nameScope);

if ($type->isOffsetAccessible()->no() || $type->hasOffsetValueType($offset)->no()) {
return new ErrorType();
}

return new OffsetAccessType($type, $offset);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeNodeResolver could detect when the type doesn't make sense. For example when $type isn't offset-accessible or when $type–>hasOffsetValueType($offset) is no(). It could return new ErrorType() in that case and the appropriate rules will pick this ErrorType and complain about an "unresolvable type" automatically.

}

private function expandIntMaskToType(Type $type): ?Type
{
$ints = array_map(static fn (ConstantIntegerType $type) => $type->getValue(), TypeUtils::getConstantIntegers($type));
Expand Down
2 changes: 2 additions & 0 deletions src/Rules/Generics/TemplateTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\KeyOfType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
Expand Down Expand Up @@ -107,6 +108,7 @@ public function check(
&& $boundTypeClass !== ObjectWithoutClassType::class
&& $boundTypeClass !== ObjectType::class
&& $boundTypeClass !== GenericObjectType::class
&& $boundTypeClass !== KeyOfType::class
&& !$boundType instanceof UnionType
&& !$boundType instanceof IntersectionType
&& !$boundType instanceof TemplateType
Expand Down
54 changes: 54 additions & 0 deletions src/Type/Generic/TemplateKeyOfType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Generic;

use PHPStan\Type\KeyOfType;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;

/** @api */
final class TemplateKeyOfType extends KeyOfType implements TemplateType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird to add support for key-of in generics but not for value-of at the same time :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have it implemented, but I wasn't able to think of a good use-case for it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't either but it looks like an ommission not to have it :)

{

/** @use TemplateTypeTrait<KeyOfType> */
use TemplateTypeTrait;
use UndecidedComparisonCompoundTypeTrait;

public function __construct(
TemplateTypeScope $scope,
TemplateTypeStrategy $templateTypeStrategy,
TemplateTypeVariance $templateTypeVariance,
string $name,
KeyOfType $bound,
)
{
parent::__construct($bound->getType());
$this->scope = $scope;
$this->strategy = $templateTypeStrategy;
$this->variance = $templateTypeVariance;
$this->name = $name;
$this->bound = $bound;
}

public function traverse(callable $cb): Type
{
$newBound = $cb($this->getBound());
if ($this->getBound() !== $newBound && $newBound instanceof KeyOfType) {
return new self(
$this->scope,
$this->strategy,
$this->variance,
$this->name,
$newBound,
);
}

return $this;
}

protected function shouldGeneralizeInferredType(): bool
{
return false;
}

}
5 changes: 5 additions & 0 deletions src/Type/Generic/TemplateTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\KeyOfType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
Expand Down Expand Up @@ -84,6 +85,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound);
}

if ($bound instanceof KeyOfType && ($boundClass === KeyOfType::class || $bound instanceof TemplateType)) {
return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound);
}

return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true));
}

Expand Down
3 changes: 2 additions & 1 deletion src/Type/Generic/TemplateTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Type\SubtractableType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
Expand Down Expand Up @@ -246,7 +247,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
}

$map = $this->getBound()->inferTemplateTypes($receivedType);
$resolvedBound = TemplateTypeHelper::resolveTemplateTypes($this->getBound(), $map);
$resolvedBound = TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes($this->getBound(), $map));
if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) {
return (new TemplateTypeMap([
$this->name => $this->shouldGeneralizeInferredType() ? $receivedType->generalize(GeneralizePrecision::templateArgument()) : $receivedType,
Expand Down
7 changes: 6 additions & 1 deletion src/Type/KeyOfType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use function sprintf;

/** @api */
final class KeyOfType implements CompoundType, LateResolvableType
class KeyOfType implements CompoundType, LateResolvableType
{

use LateResolvableTypeTrait;
Expand All @@ -18,6 +18,11 @@ public function __construct(private Type $type)
{
}

public function getType(): Type
{
return $this->type;
}

public function getReferencedClasses(): array
{
return $this->type->getReferencedClasses();
Expand Down
94 changes: 94 additions & 0 deletions src/Type/OffsetAccessType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Traits\LateResolvableTypeTrait;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use function array_merge;
use function sprintf;

/** @api */
final class OffsetAccessType implements CompoundType, LateResolvableType
{

use LateResolvableTypeTrait;
use NonGeneralizableTypeTrait;

public function __construct(
private Type $type,
private Type $offset,
)
{
}

public function getReferencedClasses(): array
{
return array_merge(
$this->type->getReferencedClasses(),
$this->offset->getReferencedClasses(),
);
}

public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
{
return array_merge(
$this->type->getReferencedTemplateTypes($positionVariance),
$this->offset->getReferencedTemplateTypes($positionVariance),
);
}

public function equals(Type $type): bool
{
return $type instanceof self
&& $this->type->equals($type->type)
&& $this->offset->equals($type->offset);
}

public function describe(VerbosityLevel $level): string
{
return sprintf(
'%s[%s]',
$this->type->describe($level),
$this->offset->describe($level),
);
}

public function isResolvable(): bool
{
return !TypeUtils::containsTemplateType($this->type)
&& !TypeUtils::containsTemplateType($this->offset);
}

protected function getResult(): Type
{
return $this->type->getOffsetValueType($this->offset);
}

/**
* @param callable(Type): Type $cb
*/
public function traverse(callable $cb): Type
{
$type = $cb($this->type);
$offset = $cb($this->offset);

if ($this->type === $type && $this->offset === $offset) {
return $this;
}

return new OffsetAccessType($type, $offset);
}

/**
* @param mixed[] $properties
*/
public static function __set_state(array $properties): Type
{
return new self(
$properties['type'],
$properties['offset'],
);
}

}
9 changes: 7 additions & 2 deletions src/Type/Traits/LateResolvableTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ private function isSuperTypeOfDefault(Type $type): TrinaryLogic
return TrinaryLogic::createYes();
}

return $this->getResult()->isSuperTypeOf($type)
->and(TrinaryLogic::createMaybe());
$isSuperType = $this->getResult()->isSuperTypeOf($type);

if (!$this->isResolvable()) {
$isSuperType = $isSuperType->and(TrinaryLogic::createMaybe());
}

return $isSuperType;
}

public function canAccessProperties(): TrinaryLogic
Expand Down
28 changes: 28 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,34 @@ public function testBug7215(): void
$this->assertNoErrors($errors);
}

public function testBug7094(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-7094.php');
$this->assertCount(6, $errors);

$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[0]->getMessage());
$this->assertSame(75, $errors[0]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects 5|6|7, 3 given.', $errors[1]->getMessage());
$this->assertSame(76, $errors[1]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[2]->getMessage());
$this->assertSame(78, $errors[2]->getLine());
$this->assertSame('Return type of call to method Bug7094\Foo::getAttribute() contains unresolvable type.', $errors[3]->getMessage());
$this->assertSame(79, $errors[3]->getLine());

$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array<string, 5|6|7|bool|string> given.', $errors[4]->getMessage());
$this->assertSame(29, $errors[4]->getLine());
$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, array<\'bar\'|\'baz\'|\'foo\', 5|6|7|bool|string> given.', $errors[5]->getMessage());
$this->assertSame(49, $errors[5]->getLine());
}

public function testOffsetAccess(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/offset-access.php');
$this->assertCount(1, $errors);
$this->assertSame('PHPDoc tag @return contains unresolvable type.', $errors[0]->getMessage());
$this->assertSame(42, $errors[0]->getLine());
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1819,7 +1819,7 @@ public function dataProperties(): array
'$this->resource',
],
[
'*ERROR*',
'mixed',
'$this->yetAnotherAnotherMixedParameter',
],
[
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,8 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7144-composer-integration.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4371.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/initializer-expr-type-resolver.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/offset-access.php');
}

/**
Expand Down
Loading