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
2 changes: 1 addition & 1 deletion .github/workflows/code_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
-
name: 'Active Classes'
run: |
vendor/bin/class-leak check bin config src rules utils --skip-suffix "Rector" --skip-type="Rector\\Utils\\Compiler\\Unprefixer" --skip-type="Rector\\NodeCollector\\BinaryOpConditionsCollector" --skip-type="Rector\\Set\\Contract\\SetListInterface" --skip-type="Rector\\DependencyInjection\\PHPStan\\RichParserFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\DuplicateRegistrationFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\SingleRegistrationFactory" --skip-type="Rector\\ChangesReporting\\Contract\\Output\\OutputFormatterInterface" --skip-type="Rector\\NodeTypeResolver\\Contract\\NodeTypeResolverInterface" --skip-type="Rector\\CodingStyle\\Contract\\ClassNameImport\\ClassNameImportSkipVoterInterface" --skip-type="Rector\\Php80\\Contract\\ConverterAttributeDecoratorInterface" --skip-type="Rector\\NodeNameResolver\\Contract\\NodeNameResolverInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpDocParser\\PhpDocTypeMapperInterface" --skip-type="Rector\\PhpAttribute\\Contract\\AnnotationToAttributeMapperInterface" --skip-type="Rector\\PHPStanStaticTypeMapper\\Contract\\TypeMapperInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpParser\\PhpParserNodeMapperInterface"
vendor/bin/class-leak check bin config src rules utils --skip-suffix "Rector" --skip-type="Rector\\Utils\\Compiler\\Unprefixer" --skip-type="Rector\\NodeCollector\\BinaryOpConditionsCollector" --skip-type="Rector\\Set\\Contract\\SetListInterface" --skip-type="Rector\\DependencyInjection\\PHPStan\\RichParserFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\DuplicateRegistrationFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\SingleServiceRegistrationRule\\Source\\SingleRegistrationFactory" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\PreferGenericTemplateOnSameUnionTypeRule\\Source\\SameUnionParamAndReturn" --skip-type="Rector\\Utils\\PHPStan\\Tests\\Rule\\PreferGenericTemplateOnSameUnionTypeRule\\Source\\SkipGenericTemplate" --skip-type="Rector\\ChangesReporting\\Contract\\Output\\OutputFormatterInterface" --skip-type="Rector\\NodeTypeResolver\\Contract\\NodeTypeResolverInterface" --skip-type="Rector\\CodingStyle\\Contract\\ClassNameImport\\ClassNameImportSkipVoterInterface" --skip-type="Rector\\Php80\\Contract\\ConverterAttributeDecoratorInterface" --skip-type="Rector\\NodeNameResolver\\Contract\\NodeNameResolverInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpDocParser\\PhpDocTypeMapperInterface" --skip-type="Rector\\PhpAttribute\\Contract\\AnnotationToAttributeMapperInterface" --skip-type="Rector\\PHPStanStaticTypeMapper\\Contract\\TypeMapperInterface" --skip-type="Rector\\StaticTypeMapper\\Contract\\PhpParser\\PhpParserNodeMapperInterface"

-
name: 'Compatible PHPStan versions'
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ includes:
rules:
- Rector\Utils\PHPStan\Rule\RegisterRelatedPolyfillRectorRule
- Rector\Utils\PHPStan\Rule\NoScopeFetchBeforeCheapGuardRule
- Rector\Utils\PHPStan\Rule\PreferGenericTemplateOnSameUnionTypeRule

parameters:
level: 8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ public function provideMinPhpVersion(): int
return PhpVersionFeature::VARIADIC_PARAM;
}

/**
* @template TFunctionLike of ClassMethod|Function_|Closure
* @param TFunctionLike $node
* @return TFunctionLike|null
*/
private function applyVariadicParams(
ClassMethod|Function_|Closure $node,
string $variableName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ private function getValidEnumExpr(Node $node): null|ClassConstFetch|Expr
};
}

/**
* @template TExpr of PropertyFetch|Variable
* @param TExpr $expr
* @return TExpr|null
*/
private function getPropertyFetchOrVariable(PropertyFetch|Variable $expr): null|PropertyFetch|Variable
{
if (! $this->isObjectType($expr, new ObjectType('MyCLabs\Enum\Enum'))) {
Expand Down
5 changes: 5 additions & 0 deletions rules/Privatization/NodeManipulator/VisibilityManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ public function removeReadonly(Class_|Property|Param $node): void
}
}

/**
* @template TNode of ClassConst|ClassMethod|Property
* @param TNode $node
* @return TNode|null
*/
public function publicize(ClassConst|ClassMethod|Property $node): ClassConst|ClassMethod|Property|null
{
// already non-public
Expand Down
5 changes: 5 additions & 0 deletions rules/Renaming/Rector/MethodCall/RenameMethodRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ private function shouldKeepForParentInterface(
);
}

/**
* @template TClassLike of Class_|Trait_|Interface_
* @param TClassLike $classOrInterface
* @return TClassLike|null
*/
private function refactorClass(
Class_|Trait_|Interface_ $classOrInterface,
Scope $scope
Expand Down
5 changes: 5 additions & 0 deletions rules/Renaming/Rector/PropertyFetch/RenamePropertyRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ private function isUsingTrait(ClassLike $classLike, string $traitName): bool
return $classReflection->hasTraitUse($traitName);
}

/**
* @template TPropertyFetch of PropertyFetch|StaticPropertyFetch
* @param TPropertyFetch $propertyFetch
* @return TPropertyFetch|null
*/
private function refactorPropertyFetch(
PropertyFetch|StaticPropertyFetch $propertyFetch
): null|PropertyFetch|StaticPropertyFetch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public function __construct(
/**
* @return ClassMethod|Function_|null the passed node when an array return type was added, null otherwise
*/
/**
* @template TFunctionLike of ClassMethod|Function_
* @param TFunctionLike $node
* @return TFunctionLike|null
*/
public function resolve(ClassMethod|Function_ $node): ClassMethod|Function_|null
{
// 1. is variable instantiated with array
Expand Down Expand Up @@ -90,6 +95,11 @@ public function resolve(ClassMethod|Function_ $node): ClassMethod|Function_|null
return $this->processAddArrayReturnType($node, $returnType);
}

/**
* @template TFunctionLike of ClassMethod|Function_
* @param TFunctionLike $node
* @return TFunctionLike|null
*/
private function processAddArrayReturnType(
ClassMethod|Function_ $node,
Type $returnType
Expand Down
5 changes: 5 additions & 0 deletions rules/TypeDeclaration/NodeManipulator/AddNeverReturnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public function __construct(
) {
}

/**
* @template TFunctionLike of ClassMethod|Function_|Closure
* @param TFunctionLike $node
* @return TFunctionLike|null
*/
public function add(ClassMethod|Function_|Closure $node, Scope $scope): ClassMethod|Function_|Closure|null
{
if ($this->shouldSkip($node, $scope)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public function __construct(
) {
}

/**
* @template TFunctionLike of ClassMethod|Function_
* @param TFunctionLike $functionLike
* @return TFunctionLike|null
*/
public function add(ClassMethod|Function_ $functionLike, Scope $scope): ClassMethod|Function_|null
{
if ($functionLike->returnType instanceof Node) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public function __construct(
) {
}

/**
* @template TFunctionLike of ClassMethod|Function_
* @param TFunctionLike $functionLike
* @return TFunctionLike|null
*/
public function add(ClassMethod|Function_ $functionLike, Scope $scope): ClassMethod|Function_|null
{
if ($functionLike->stmts === null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ private function isUnionPossibleReturnsVoid(ClassMethod|Function_|Closure $node)
return false;
}

/**
* @template TFunctionLike of ClassMethod|Function_|Closure
* @param TFunctionLike $node
* @return TFunctionLike
*/
private function processSingleUnionType(
ClassMethod|Function_|Closure $node,
UnionType $unionType,
Expand Down Expand Up @@ -200,6 +205,11 @@ private function shouldSkip(ClassMethod|Function_|Closure $node, Scope $scope):
return $this->isUnionPossibleReturnsVoid($node);
}

/**
* @template TFunctionLike of ClassMethod|Function_|Closure
* @param TFunctionLike $functionLike
* @return TFunctionLike
*/
private function refactorSingleReturnType(
Return_ $return,
Identifier|Name|NullableType|ComplexType $returnedStrictTypeNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ private function createInstanceof(Expr $expr, ObjectType $objectType): Instanceo
return new Instanceof_($expr, $fullyQualified);
}

/**
* @template TLoop of While_|Do_
* @param TLoop $while
* @return TLoop|null
*/
private function refactorNotIdentical(While_|Do_ $while, NotIdentical $notIdentical): While_|Do_|null
{
if (! $this->valueResolver->isNull($notIdentical->right)) {
Expand Down
121 changes: 121 additions & 0 deletions utils/phpstan/src/Rule/PreferGenericTemplateOnSameUnionTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Rule;

use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\ComplexType;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\UnionType;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* Flags a method whose param and return share the same class-type union, e.g. (A|B $node): A|B|null.
* Such a method usually returns the passed node, so a @template generic narrows the return to the passed type.
*
* @implements Rule<ClassMethod>
* @see \Rector\Utils\PHPStan\Tests\Rule\PreferGenericTemplateOnSameUnionTypeRule\PreferGenericTemplateOnSameUnionTypeRuleTest
*/
final class PreferGenericTemplateOnSameUnionTypeRule implements Rule
{
private const string ERROR_MESSAGE = 'Param and return share the same class-type union "%s". Add a @template generic to narrow the return type to the passed one.';

public function getNodeType(): string
{
return ClassMethod::class;
}

/**
* @param ClassMethod $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
$docComment = $node->getDocComment();
if ($docComment instanceof Doc && str_contains($docComment->getText(), '@template')) {
return [];
}

$returnNames = $this->resolveClassUnionNames($node->returnType);
if ($returnNames === null) {
return [];
}

foreach ($node->params as $param) {
$paramNames = $this->resolveClassUnionNames($param->type);
if ($paramNames === null) {
continue;
}

if ($paramNames !== $returnNames) {
continue;
}

$shortNames = array_map(
static function (string $name): string {
$parts = explode('\\', $name);
return end($parts);
},
$returnNames
);

return [
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, implode('|', $shortNames)))
->identifier('rector.genericTemplateOverSameUnionType')
->line($node->getStartLine())
->build(),
];
}

return [];
}

/**
* Returns sorted class-type member names (null stripped), or null when the type is not a
* union of 2+ class types - scalars, array and intersections disqualify it.
*
* @return list<string>|null
*/
private function resolveClassUnionNames(null|Identifier|Name|ComplexType $type): ?array
{
if (! $type instanceof UnionType) {
return null;
}

$names = [];
foreach ($type->types as $memberType) {
if ($memberType instanceof Identifier) {
if ($memberType->toLowerString() === 'null') {
continue;
}

// built-in scalar/array type, not a class
return null;
}

if ($memberType instanceof Name) {
$names[] = $memberType->toString();
continue;
}

// IntersectionType or anything else
return null;
}

if (count($names) < 2) {
return null;
}

$names = array_unique($names);
sort($names);

return $names;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\PreferGenericTemplateOnSameUnionTypeRule;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use Rector\Utils\PHPStan\Rule\PreferGenericTemplateOnSameUnionTypeRule;

/**
* @extends RuleTestCase<PreferGenericTemplateOnSameUnionTypeRule>
*/
final class PreferGenericTemplateOnSameUnionTypeRuleTest extends RuleTestCase
{
public function testSameUnionParamAndReturn(): void
{
$expectedErrorMessage = 'Param and return share the same class-type union "ClassMethod|Function_". Add a @template generic to narrow the return type to the passed one.';

$this->analyse([__DIR__ . '/Source/SameUnionParamAndReturn.php'], [[$expectedErrorMessage, 12]]);
}

public function testSkip(): void
{
$this->analyse([__DIR__ . '/Source/SkipGenericTemplate.php'], []);
}

protected function getRule(): Rule
{
return new PreferGenericTemplateOnSameUnionTypeRule();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\PreferGenericTemplateOnSameUnionTypeRule\Source;

use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;

final class SameUnionParamAndReturn
{
public function process(ClassMethod|Function_ $node): ClassMethod|Function_|null
{
return $node;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStan\Tests\Rule\PreferGenericTemplateOnSameUnionTypeRule\Source;

use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;

final class SkipGenericTemplate
{
/**
* @template TNode of ClassMethod|Function_
* @param TNode $node
* @return TNode|null
*/
public function alreadyGeneric(ClassMethod|Function_ $node): ClassMethod|Function_|null
{
return $node;
}

public function scalarUnion(int|float $value): int|float
{
return $value + 1;
}

public function mismatchedUnion(ClassMethod|Function_ $node): ClassMethod|Interface_|null
{
return null;
}
}
Loading