Skip to content

Support @phan- prefixes on recognized phpdoc tags #3000

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

Merged
merged 4 commits into from
Apr 5, 2024
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
31 changes: 17 additions & 14 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar
{
$resolved = [];
$resolvedByTag = [];
foreach (['@var', '@psalm-var', '@phpstan-var'] as $tagName) {
foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) {
$tagResolved = [];
foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) {
$type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope);
Expand Down Expand Up @@ -165,7 +165,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope):
$resolved = [];
$originalNameScope = $nameScope;

foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) {
foreach (['@method', '@phan-method', '@psalm-method', '@phpstan-method'] as $tagName) {
foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) {
$nameScope = $originalNameScope;
$templateTags = [];
Expand Down Expand Up @@ -232,7 +232,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope)
{
$resolved = [];

foreach (['@extends', '@template-extends', '@phpstan-extends'] as $tagName) {
foreach (['@extends', '@phan-extends', '@phan-inherits', '@template-extends', '@phpstan-extends'] as $tagName) {
foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) {
$resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag(
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
Expand Down Expand Up @@ -289,8 +289,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope

$prefixPriority = [
'' => 0,
'psalm' => 1,
'phpstan' => 2,
'phan' => 1,
'psalm' => 2,
'phpstan' => 3,
];

foreach ($phpDocNode->getTags() as $phpDocTagNode) {
Expand All @@ -300,7 +301,7 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope
}

$tagName = $phpDocTagNode->name;
if (in_array($tagName, ['@template', '@psalm-template', '@phpstan-template'], true)) {
if (in_array($tagName, ['@template', '@phan-template', '@psalm-template', '@phpstan-template'], true)) {
$variance = TemplateTypeVariance::createInvariant();
} elseif (in_array($tagName, ['@template-covariant', '@psalm-template-covariant', '@phpstan-template-covariant'], true)) {
$variance = TemplateTypeVariance::createCovariant();
Expand All @@ -310,7 +311,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope
continue;
}

if (str_starts_with($tagName, '@psalm-')) {
if (str_starts_with($tagName, '@phan-')) {
$prefix = 'phan';
} elseif (str_starts_with($tagName, '@psalm-')) {
$prefix = 'psalm';
} elseif (str_starts_with($tagName, '@phpstan-')) {
$prefix = 'phpstan';
Expand Down Expand Up @@ -368,7 +371,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope):

$unusedClosureThisTypes = $closureThisTypes;

foreach (['@param', '@psalm-param', '@phpstan-param'] as $tagName) {
foreach (['@param', '@phan-param', '@psalm-param', '@phpstan-param'] as $tagName) {
foreach ($phpDocNode->getParamTagValues($tagName) as $tagValue) {
$parameterName = substr($tagValue->parameterName, 1);
$parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope);
Expand Down Expand Up @@ -458,7 +461,7 @@ public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope):
{
$resolved = null;

foreach (['@return', '@psalm-return', '@phpstan-return'] as $tagName) {
foreach (['@return', '@phan-return', '@phan-real-return', '@psalm-return', '@phpstan-return'] as $tagName) {
foreach ($phpDocNode->getReturnTagValues($tagName) as $tagValue) {
$type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope);
if ($this->shouldSkipType($tagName, $type)) {
Expand Down Expand Up @@ -546,7 +549,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop
{
$resolved = [];

foreach (['@psalm-type', '@phpstan-type'] as $tagName) {
foreach (['@phan-type', '@psalm-type', '@phpstan-type'] as $tagName) {
foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) {
$alias = $typeAliasTagValue->alias;
$typeNode = $typeAliasTagValue->type;
Expand Down Expand Up @@ -581,7 +584,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na
*/
public function resolveAssertTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
foreach (['@phpstan', '@psalm'] as $prefix) {
foreach (['@phpstan', '@psalm', '@phan'] as $prefix) {
$resolved = array_merge(
$this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert', AssertTag::NULL),
$this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-true', AssertTag::IF_TRUE),
Expand Down Expand Up @@ -682,7 +685,7 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool
public function resolveIsPure(PhpDocNode $phpDocNode): bool
{
foreach ($phpDocNode->getTags() as $phpDocTagNode) {
if (in_array($phpDocTagNode->name, ['@pure', '@psalm-pure', '@phpstan-pure'], true)) {
if (in_array($phpDocTagNode->name, ['@pure', '@phan-pure', '@phan-side-effect-free', '@psalm-pure', '@phpstan-pure'], true)) {
return true;
}
}
Expand All @@ -703,7 +706,7 @@ public function resolveIsImpure(PhpDocNode $phpDocNode): bool

public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool
{
foreach (['@readonly', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) {
foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) {
$tags = $phpDocNode->getTagsByName($tagName);

if (count($tags) > 0) {
Expand All @@ -716,7 +719,7 @@ public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool

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

if (count($tags) > 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function processNode(Node $node, Scope $scope): array

$errors = [];
foreach ($phpDocNode->getTags() as $phpDocTag) {
if (str_starts_with($phpDocTag->name, '@psalm-')) {
if (str_starts_with($phpDocTag->name, '@phan-') || str_starts_with($phpDocTag->name, '@psalm-')) {
continue;
}

Expand Down
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,23 @@ public function testTypeFromFunctionPhpDocsPhpstanPrefix(
);
}

/**
* @dataProvider dataTypeFromFunctionPhpDocs
* @dataProvider dataTypeFromFunctionPrefixedPhpDocs
*/
public function testTypeFromFunctionPhpDocsPhanPrefix(
string $description,
string $expression,
): void
{
require_once __DIR__ . '/data/functionPhpDocs-phanPrefix.php';
$this->assertTypes(
__DIR__ . '/data/functionPhpDocs-phanPrefix.php',
$description,
$expression,
);
}

public function dataTypeFromMethodPhpDocs(): array
{
return [
Expand Down Expand Up @@ -3829,6 +3846,31 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix(
);
}

/**
* @dataProvider dataTypeFromFunctionPhpDocs
* @dataProvider dataTypeFromMethodPhpDocs
*/
public function testTypeFromMethodPhpDocsPhanPrefix(
string $description,
string $expression,
bool $replaceClass = true,
): void
{
$description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhanPrefix)', $description);

if ($replaceClass && $expression !== '$this->doFoo()') {
$description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhanPrefix)', $description);
if ($description === 'MethodPhpDocsNamespace\Foo') {
$description = 'MethodPhpDocsNamespace\FooPhanPrefix';
}
}
$this->assertTypes(
__DIR__ . '/data/methodPhpDocs-phanPrefix.php',
$description,
$expression,
);
}

/**
* @dataProvider dataTypeFromFunctionPhpDocs
* @dataProvider dataTypeFromMethodPhpDocs
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/data/classPhpDocs.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* @method array arrayOfStrings()
* @psalm-method array<string> arrayOfStrings()
* @phpstan-method array<string, int> arrayOfInts()
* @phan-method array<string> arrayOfStrings()
* @method array arrayOfInts()
* @method mixed overrodeMethod()
* @method static mixed overrodeStaticMethod()
Expand Down
79 changes: 79 additions & 0 deletions tests/PHPStan/Analyser/data/functionPhpDocs-phanPrefix.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace MethodPhpDocsNamespace;

use SomeNamespace\Amet as Dolor;
use SomeNamespace\Consecteur;

/**
* @phan-param Foo|Bar $unionTypeParameter
* @phan-param int $anotherMixedParameter
* @phan-param int $anotherMixedParameter
* @phan-paran int $yetAnotherMixedProperty
* @phan-param int $integerParameter
* @phan-param integer $anotherIntegerParameter
* @phan-param aRray $arrayParameterOne
* @phan-param mixed[] $arrayParameterOther
* @phan-param Lorem $objectRelative
* @phan-param \SomeOtherNamespace\Ipsum $objectFullyQualified
* @phan-param Dolor $objectUsed
* @phan-param null|int $nullableInteger
* @phan-param Dolor|null $nullableObject
* @phan-param Dolor $anotherNullableObject
* @phan-param Null $nullType
* @phan-param Bar $barObject
* @phan-param Foo $conflictedObject
* @phan-param Baz $moreSpecifiedObject
* @phan-param resource $resource
* @phan-param array[array] $yetAnotherAnotherMixedParameter
* @phan-param \\Test\Bar $yetAnotherAnotherAnotherMixedParameter
* @phan-param New $yetAnotherAnotherAnotherAnotherMixedParameter
* @phan-param void $voidParameter
* @phan-param Consecteur $useWithoutAlias
* @phan-param true $true
* @phan-param false $false
* @phan-param true $boolTrue
* @phan-param false $boolFalse
* @phan-param bool $trueBoolean
* @phan-param bool $parameterWithDefaultValueFalse
* @phan-return Foo
*/
function doFooPhanPrefix(
$mixedParameter,
$unionTypeParameter,
$anotherMixedParameter,
$yetAnotherMixedParameter,
$integerParameter,
$anotherIntegerParameter,
$arrayParameterOne,
$arrayParameterOther,
$objectRelative,
$objectFullyQualified,
$objectUsed,
$nullableInteger,
$nullableObject,
$nullType,
$barObject,
Bar $conflictedObject,
Bar $moreSpecifiedObject,
$resource,
$yetAnotherAnotherMixedParameter,
$yetAnotherAnotherAnotherMixedParameter,
$yetAnotherAnotherAnotherAnotherMixedParameter,
$voidParameter,
$useWithoutAlias,
$true,
$false,
bool $boolTrue,
bool $boolFalse,
bool $trueBoolean,
$parameterWithDefaultValueFalse = false,
$anotherNullableObject = null
)
{
$fooFunctionResult = doFoo();

foreach ($moreSpecifiedObject->doFluentUnionIterable() as $fluentUnionIterableBaz) {
die;
}
}
17 changes: 16 additions & 1 deletion tests/PHPStan/Analyser/data/generics.php
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,7 @@ class PrefixedTemplateWins2
* @template T of Foo
* @phpstan-template T of Bar
* @psalm-template T of Baz
* @phan-template T of Quux
*/
class PrefixedTemplateWins3
{
Expand Down Expand Up @@ -1209,19 +1210,33 @@ class PrefixedTemplateWins5

}

/**
* @phan-template T of Foo
* @phpstan-template T of Bar
*/
class PrefixedTemplateWins6
{

/** @var T */
public $name;

}

function testPrefixed(
PrefixedTemplateWins $a,
PrefixedTemplateWins2 $b,
PrefixedTemplateWins3 $c,
PrefixedTemplateWins4 $d,
PrefixedTemplateWins5 $e
PrefixedTemplateWins5 $e,
PrefixedTemplateWins6 $f

) {
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $a->name);
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $b->name);
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $c->name);
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $d->name);
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $e->name);
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $f->name);
};

/**
Expand Down
Loading
Loading