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 require-extends rules #2859

Merged
merged 26 commits into from Jan 10, 2024
Merged

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Jan 7, 2024

No description provided.

'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'PHPDoc tag @require-extends cannot contain generic type.',
Copy link
Contributor Author

@staabm staabm Jan 7, 2024

Choose a reason for hiding this comment

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

this error messge is a bit misleading. my idea was that in @phpstan-require-extends SomeClass we only name the class by its name, without generics params (generic classes can still be used at the using-class).

that way we don't need to validate the generics and therefore safe phpstan from validating them twice.

.. I think we just need a better error message.. wdyt?

@ondrejmirtes
Copy link
Member

Before I look at the source code, here's what I'd expect these rules to do:

A rule to check the definition of @phpstan-require-extends

  • That it's only above an interface or a trait (it's fine to hook onto InClassNode and check that, we don't need to care about cases where it's defined for example above a function).
  • That it's only an ObjectType, to reject @phpstan-require-extends int for example.
  • That it's an existing and non-final class

A rule to check the definition of @phpstan-require-implements

  • That it's only above a trait - other uses don't make sense IMHO.
  • That it's only an ObjectType
  • That it's an existing interface

A rule to check a class that directly implements an interface or directly uses a trait with @phpstan-require-extends

  • This class needs to directly or indirectly extend the class from the tag

A rule to check a class that directly uses a trait with @phpstan-require-implements

  • This class needs to directly or indirectly implement the interface from the tag

Please confirm the rules do all of these things. I think 4 rule classes as outlined above would be ideal.

@staabm staabm force-pushed the require-rules branch 4 times, most recently from 9309ac3 to 3a87cc0 Compare January 7, 2024 14:16
@staabm
Copy link
Contributor Author

staabm commented Jan 7, 2024

I have implemented 2 rules for require-extends, which I think fullfill your description. if you agree with them I would do the other 2 require-implements in a similar fashion.

one thing I noticed: since we use InClassNode for IncompatibleRequireExtendsTypeRule errors in phpdoc trait definitions will not show up. Should we work with ClassLike instead or maybe need a separate rules for checking the phpdoc on traits directly?


public function getNodeType(): string
{
return ClassLike::class;
Copy link
Member

Choose a reason for hiding this comment

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

Ask for InClassNode instead, it contains reflection already. You won't have to get the reflection from ReflectionProvider, and it works for anonymous classes too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but it doesn't work for trait defintions. it would not error on

/**
 * @phpstan-require-implements SomeTrait
 */
trait InvalidTrait1 {}

Copy link
Member

Choose a reason for hiding this comment

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

Two separate rules then please.

Copy link
Member

Choose a reason for hiding this comment

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

For traits there's InTraitNode.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

InTraitNode will only error for traits when a class exists which uses them. are you fine with that?


$errors = [];
foreach ($implementsTags as $implementsTag) {
$type = $implementsTag->getType();
Copy link
Member

Choose a reason for hiding this comment

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

This whole foreach is too complicated. Just check that the type is ObjectType.

src/Rules/PhpDoc/IncompatibleRequireImplementsTypeRule.php Outdated Show resolved Hide resolved
conf/config.level2.neon Outdated Show resolved Hide resolved
src/Rules/Classes/RequireExtendsRule.php Outdated Show resolved Hide resolved
src/Rules/Classes/RequireExtendsRule.php Outdated Show resolved Hide resolved
src/Rules/Classes/RequireExtendsRule.php Outdated Show resolved Hide resolved
src/Rules/Classes/RequireImplementsRule.php Outdated Show resolved Hide resolved
src/Rules/PhpDoc/IncompatibleRequireExtendsTypeRule.php Outdated Show resolved Hide resolved
src/Rules/PhpDoc/IncompatibleRequireExtendsTypeRule.php Outdated Show resolved Hide resolved
@staabm staabm marked this pull request as ready for review January 9, 2024 15:15
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.


public function getNodeType(): string
{
return InTraitNode::class;
Copy link
Member

Choose a reason for hiding this comment

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

You're right, this one is better served by Node\Stmt\Trait_ - even when the trait is used multiple times, we just want to report the declaration once.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Hey, I just pushed some cosmetic changes.

What still needs to be done here:

  1. Support @psalm- prefixes in PhpDocNodeResolver along with some tests: https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-require-extends
  2. There is a lot of literal duplication between RequireExtendsDefinitionTraitRule and RequireExtendsDefinitionClassRule. We usually solve this by introducing a "check" class that gets injected into rule classes to reuse the common code.

Also please note that after we merge the current two PRs and release them, I'd like you to work on properly introducing generics support for the new tags. But I want that to happen later and to not complicate the current PRs. Thanks.

@@ -56,6 +56,18 @@ public function testRule(): void
'Trait IncompatibleRequireImplements\InvalidTrait4 requires using class to implement IncompatibleRequireImplements\SomeClass<T>, but IncompatibleRequireImplements\InvalidTraitUse4 does not.',
137,
],
[
'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but AnonymousClass4fea61018e98554a605d5c87523a32cc does not.',
Copy link
Member

Choose a reason for hiding this comment

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

this message is wrong, you need to use getDisplayName on ClassReflection

@ondrejmirtes
Copy link
Member

Please use it consistently everywhere on ClassReflection instead of getName when building user-facing messages, thanks.

@staabm
Copy link
Contributor Author

staabm commented Jan 10, 2024

sorry missed one occurence

$classReflection = $node->getClassReflection();
$extendsTags = $classReflection->getRequireExtendsTags();

if (count($extendsTags) === 0) {
Copy link
Contributor Author

@staabm staabm Jan 10, 2024

Choose a reason for hiding this comment

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

I think we should error when a class defines 2 or more @*-require-extends

Copy link
Member

Choose a reason for hiding this comment

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

Not error, but support and check all of them.

Copy link
Member

Choose a reason for hiding this comment

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

Oh sorry, I misunderstood you. Please add a test and fix the rule.

@@ -30,6 +31,11 @@ public function __construct(
public function checkExtendsTags(Node $node, array $extendsTags): array
{
$errors = [];

if (count($extendsTags) > 1) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends can only be used once.'))->build();
Copy link
Member

Choose a reason for hiding this comment

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

Oh right, this makes sense because you can't have multiple parents :)

@ondrejmirtes ondrejmirtes merged commit 8900a43 into phpstan:1.10.x Jan 10, 2024
423 of 426 checks passed
@ondrejmirtes
Copy link
Member

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants