Skip to content
Closed
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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ parameters:
callUserFunc: true
finalByPhpDoc: true
magicConstantOutOfContext: true
consistentTemplates: true
3 changes: 3 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ parameters:
callUserFunc: false
finalByPhpDoc: false
magicConstantOutOfContext: false
consistentTemplates: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -905,6 +906,8 @@ services:

-
class: PHPStan\Rules\Generics\GenericObjectTypeCheck
arguments:
reportConsistentTemplates: %featureToggles.consistentTemplates%

-
class: PHPStan\Rules\Generics\TemplateTypeCheck
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ parametersSchema:
callUserFunc: bool()
finalByPhpDoc: bool()
magicConstantOutOfContext: bool()
consistentTemplates: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
13 changes: 13 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,19 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool
return count($finalTags) > 0;
}

public function resolveHasConsistentTemplates(PhpDocNode $phpDocNode): bool
{
foreach (['@consistent-templates', '@phpstan-consistent-templates', '@psalm-consistent-templates'] as $tagName) {
$tags = $phpDocNode->getTagsByName($tagName);

if (count($tags) > 0) {
return true;
}
}

return false;
}

public function resolveIsPure(PhpDocNode $phpDocNode): bool
{
foreach ($phpDocNode->getTags() as $phpDocTagNode) {
Expand Down
14 changes: 14 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class ResolvedPhpDocBlock

private ?bool $acceptsNamedArguments = null;

private ?bool $hasConsistentTemplates = null;

private function __construct()
{
}
Expand Down Expand Up @@ -196,6 +198,7 @@ public static function createEmpty(): self
$self->isAllowedPrivateMutation = false;
$self->hasConsistentConstructor = false;
$self->acceptsNamedArguments = true;
$self->hasConsistentTemplates = false;

return $self;
}
Expand Down Expand Up @@ -256,6 +259,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self
$result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation();
$result->hasConsistentConstructor = $this->hasConsistentConstructor();
$result->acceptsNamedArguments = $acceptsNamedArguments;
$result->hasConsistentTemplates = $this->hasConsistentTemplates();

return $result;
}
Expand Down Expand Up @@ -671,6 +675,16 @@ public function acceptsNamedArguments(): bool
return $this->acceptsNamedArguments;
}

public function hasConsistentTemplates(): bool
{
if ($this->hasConsistentTemplates === null) {
$this->hasConsistentTemplates = $this->phpDocNodeResolver->resolveHasConsistentTemplates(
$this->phpDocNode,
);
}
return $this->hasConsistentTemplates;
}

public function getTemplateTypeMap(): TemplateTypeMap
{
return $this->templateTypeMap;
Expand Down
17 changes: 17 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Helper\GetTemplateTypeType;
use PHPStan\Type\Generic\GenericStaticType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
Expand Down Expand Up @@ -742,6 +743,22 @@ static function (string $variance): TemplateTypeVariance {
}

return new ErrorType();
} elseif ($mainTypeName === 'static') {
if (count($genericTypes) > 0) {
$className = $nameScope->getClassName();

if ($className === null) {
return new ErrorType();
}

if ($this->getReflectionProvider()->hasClass($className)) {
$classReflection = $this->getReflectionProvider()->getClass($className);

if ($classReflection->hasConsistentTemplates()) {
return new GenericStaticType($classReflection, $genericTypes);
}
}
}
}

$mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope);
Expand Down
12 changes: 12 additions & 0 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class ClassReflection

private ?bool $hasConsistentConstructor = null;

private ?bool $hasConsistentTemplates = null;

private ?TemplateTypeMap $templateTypeMap = null;

private ?TemplateTypeMap $activeTemplateTypeMap = null;
Expand Down Expand Up @@ -1160,6 +1162,16 @@ public function hasConsistentConstructor(): bool
return $this->hasConsistentConstructor;
}

public function hasConsistentTemplates(): bool
{
if ($this->hasConsistentTemplates === null) {
$resolvedPhpDoc = $this->getResolvedPhpDoc();
$this->hasConsistentTemplates = $resolvedPhpDoc !== null && $resolvedPhpDoc->hasConsistentTemplates();
}

return $this->hasConsistentTemplates;
}

public function isFinalByKeyword(): bool
{
if ($this->isAnonymous()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
use PHPStan\Reflection\Php\DummyParameterWithPhpDocs;
use PHPStan\Reflection\ResolvedMethodReflection;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\GenericStaticType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
Expand Down Expand Up @@ -83,7 +86,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
array_map(
fn (ParameterReflectionWithPhpDocs $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs(
$parameter->getName(),
$this->transformStaticType($parameter->getType()),
$this->transformStaticType($parameter->getType(), $declaringClass),
$parameter->isOptional(),
$parameter->passedByReference(),
$parameter->isVariadic(),
Expand All @@ -95,7 +98,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
$acceptor->getParameters(),
),
$acceptor->isVariadic(),
$this->transformStaticType($acceptor->getReturnType()),
$this->transformStaticType($acceptor->getReturnType(), $declaringClass),
$this->transformStaticType($acceptor->getPhpDocReturnType()),
$this->transformStaticType($acceptor->getNativeReturnType()),
$acceptor->getCallSiteVarianceMap(),
Expand All @@ -109,9 +112,25 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
return new ChangedTypeMethodReflection($declaringClass, $method, $variants, $namedArgumentsVariants);
}

private function transformStaticType(Type $type): Type
private function transformStaticType(Type $type, ClassReflection $declaringClass): Type
{
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($declaringClass): Type {
if ($type instanceof GenericStaticType) {
if (! $declaringClass->hasConsistentTemplates()) {
return $traverse($type);
}

if ($this->calledOnType instanceof GenericObjectType) {
return new GenericObjectType($this->calledOnType->getClassName(), $type->getTypes());
}

if ($this->calledOnType instanceof ObjectType) {
return new GenericObjectType($this->calledOnType->getClassName(), $type->getTypes());
}

return $this->calledOnType;
}

if ($type instanceof StaticType) {
return $this->calledOnType;
}
Expand Down
82 changes: 80 additions & 2 deletions src/Rules/Generics/ClassAncestorsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@
use PHPStan\Node\InClassNode;
use PHPStan\PhpDoc\Tag\ExtendsTag;
use PHPStan\PhpDoc\Tag\ImplementsTag;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Type;
use function array_map;
use function array_merge;
use function count;
use function sprintf;

/**
Expand Down Expand Up @@ -45,9 +51,12 @@ public function processNode(Node $node, Scope $scope): array
$className = $classReflection->getName();
$escapedClassName = SprintfHelper::escapeFormatString($className);

$extendsTagTypes = array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags());
$implementsTagTypes = array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags());

$extendsErrors = $this->genericAncestorsCheck->check(
$originalNode->extends !== null ? [$originalNode->extends] : [],
array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()),
$extendsTagTypes,
sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName),
sprintf('Class %s has @extends tag, but does not extend any class.', $escapedClassName),
sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $escapedClassName),
Expand All @@ -63,7 +72,7 @@ public function processNode(Node $node, Scope $scope): array

$implementsErrors = $this->genericAncestorsCheck->check(
$originalNode->implements,
array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()),
$implementsTagTypes,
sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName),
sprintf('Class %s has @implements tag, but does not implement any interface.', $escapedClassName),
sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $escapedClassName),
Expand All @@ -81,7 +90,76 @@ public function processNode(Node $node, Scope $scope): array
$implementsErrors[] = $error;
}

foreach ($this->checkConsistentTemplateViolations($classReflection, $extendsTagTypes, 'extends') as $error) {
$extendsErrors[] = $error;
}

foreach ($this->checkConsistentTemplateViolations($classReflection, $implementsTagTypes, 'implements') as $error) {
$implementsErrors[] = $error;
}

return array_merge($extendsErrors, $implementsErrors);
}

/**
* @param Type[] $types
*
* @return RuleError[]
*/
private function checkConsistentTemplateViolations(ClassReflection $classReflection, array $types, string $tagName): array
{
/** @var RuleError[] $errors */
$errors = [];

foreach ($types as $type) {
if (! $type instanceof GenericObjectType) {
continue;
}

$ancestorClassReflection = $type->getClassReflection();

if ($ancestorClassReflection === null) {
continue;
}

if (! $ancestorClassReflection->hasConsistentTemplates()) {
continue;
}

$className = $classReflection->getName();
$escapedClassName = SprintfHelper::escapeFormatString($className);

$ancestorTemplateCount = count($ancestorClassReflection->getTemplateTags());

if (count($classReflection->getTemplateTags()) !== $ancestorTemplateCount) {
$errors[] = RuleErrorBuilder::message(
sprintf(
'Class %s should have same amount of template tags as %s. %d expected but %d found.',
$escapedClassName,
SprintfHelper::escapeFormatString($ancestorClassReflection->getName()),
$ancestorTemplateCount,
count($classReflection->getTemplateTags()),
),
)->build();
}

foreach ($type->getTypes() as $extendType) {
if ($extendType instanceof TemplateType) {
continue;
}

$errors[] = RuleErrorBuilder::message(
sprintf(
'Class %s has non-template types in @%s tag but %s has consistent templates.',
$escapedClassName,
$tagName,
SprintfHelper::escapeFormatString($ancestorClassReflection->getName()),
),
)->build();
}
}

return $errors;
}

}
25 changes: 22 additions & 3 deletions src/Rules/Generics/GenericObjectTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\GenericStaticType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeVariance;
Expand All @@ -23,6 +24,10 @@
class GenericObjectTypeCheck
{

public function __construct(private bool $reportConsistentTemplates = false)
{
}

/**
* @return RuleError[]
*/
Expand All @@ -44,6 +49,10 @@ public function check(
continue;
}

if ($genericType instanceof GenericStaticType && ! $classReflection->hasConsistentTemplates() && ! $this->reportConsistentTemplates) {
continue;
}

$classLikeDescription = 'class';
if ($classReflection->isInterface()) {
$classLikeDescription = 'interface';
Expand Down Expand Up @@ -153,21 +162,31 @@ public function check(
$classReflection->getDisplayName(false),
))->build();
}

if (!($genericType instanceof GenericStaticType) || $classReflection->hasConsistentTemplates() || !$this->reportConsistentTemplates) {
continue;
}

$messages[] = RuleErrorBuilder::message(sprintf(
'Unsafe usage of %s type in PHPDoc tag. Consider adding \'@phpstan-consistent-templates\' to the class.',
$genericType->describe(VerbosityLevel::typeOnly()),
))->build();
}

return $messages;
}

/**
* @return GenericObjectType[]
* @return (GenericObjectType|GenericStaticType)[]
*/
private function getGenericTypes(Type $phpDocType): array
{
$genericObjectTypes = [];
TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
if ($type instanceof GenericObjectType) {
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
$resolvedType = TemplateTypeHelper::resolveToBounds($type);
if (!$resolvedType instanceof GenericObjectType) {

if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) {
throw new ShouldNotHappenException();
}
$genericObjectTypes[] = $resolvedType;
Expand Down
1 change: 1 addition & 0 deletions src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ public function processNode(Node $node, Scope $scope): array

$errors[] = $errorBuilder->build();
}

}
}

Expand Down
Loading