Skip to content

Check redeclared readonly properties#3041

Closed
schlndh wants to merge 5 commits intophpstan:1.10.xfrom
schlndh:feature-checkRedeclaredReadonlyProperties
Closed

Check redeclared readonly properties#3041
schlndh wants to merge 5 commits intophpstan:1.10.xfrom
schlndh:feature-checkRedeclaredReadonlyProperties

Conversation

@schlndh
Copy link
Contributor

@schlndh schlndh commented May 3, 2024

Fixes phpstan/phpstan#8101

Readonly properties in PHP work as follows:

  • Readonly property can only be assigned once and only from the scope where it was declared. It is not necessary to assign it in the constructor, it can be done later.
  • Readonly properties can be redeclared by child classes, but they still must be assigned at most once (either by the child or the parent).

PHPStan enforces that readonly properties are assigned in the constructor right from level 0 (MissingReadOnlyPropertyAssignRule). Therefore, I propose that we prohibit redeclaring a readonly property and simultaneously calling a constructor of the parent which also declares it, or any of its children (they may call their parent's constructor and so on).

It could technically be allowed to both redeclare a readonly property and call the parent constructor, as long as the property is not assigned anywhere in the class. However, I cannot think of a reason why anyone would need to redeclare a readonly property, other than to assign it in the child class, instead of in the parent. Therefore, I didn't add an exception for this case. There is a use-case: adding attributes to the property (phpstan/phpstan#9864).

The rule will still reject some valid edge-cases, but IMO it is on par with MissingReadOnlyPropertyAssignRule. So I also added it as part of level 0 (which strictly speaking breaks the BC promise, but so did MissingReadOnlyPropertyAssignRule - it might need to have an exception for rules which detect code which is highly likely to lead to a runtime error).


Additionally, I would like to ask you whether you'd be willing to prohibit redeclaring readonly properties completely (i.e. #1788) as part of strict-rules. My reasoning is as follows:

  • PHPStan itself already requires readonly properties to be assigned in the constructor (i.e. they must both be assigned and it must be done in the constructor).
  • Strict rules require that you call the parent constructor.
  • If you follow these rules, while also redeclaring a readonly property you'll get a runtime error. Therefore, strict rules should prohibit you from redeclaring readonly properties.

This would give additional safety for those of us who do not need to redeclare readonly properties, because the general rule that I implemented does not cover everything (e.g. the constructor call could be hidden somehow).

@schlndh schlndh force-pushed the feature-checkRedeclaredReadonlyProperties branch from 5a3d08c to 5d6a482 Compare May 3, 2024 17:15
@ondrejmirtes
Copy link
Member

Some ideas (I haven't seen the code yet):

  • Considering this example: https://phpstan.org/r/cf781e9e-cfdd-4aae-855d-ead64bf16afa - if (a subclass redeclares readonly property in promoted fashion OR assigns the redeclared readonly property) AND calls parent constructor AND parent constructor declares the readonly property in promoted fashion, then we should always report an error. This can be achieved with local analysis only, without performance impact. PHPStan should see that as duplicate assignment with existing rules in place. So MissingReadOnlyPropertyAssignRule should report "is already assigned" just by modifying the data that is fed into it.
  • If it's more complicated than that and we cannot tell from the outside when the parent class assigns the readonly property (it's not promoted), we also have a way of tackling that, with collectors (https://phpstan.org/developing-extensions/collectors). One collector can collect parent::__construct() for constructors with non-promoted parameters. Another collector can collect assignments of readonly properties in non-final class constructors. The CollectedDataNode rule in the end can compare these datasets and decide what should be reported as "already assigned". Recently I used collectors for this use-case: 281a87d. It's very effective.

I'd accept a PR that does the first bullet point. I think this PR should be that. The second bullet point can be a subject of a future PR. What's your opinion?

@schlndh
Copy link
Contributor Author

schlndh commented May 4, 2024

Ok, first bullet point seems good. It should already catch many issues. I'll try to implement that.

Second bullet-point: I'm not convinced that it's necessary to be so permissive. PHPStan requires the parent to assign the property in the constructor. So IMO it makes sense to assume that it is assigned.

As for the possibility of detecting the property assigns in the parent class: I guess it could be useful for use-cases like this: https://phpstan.org/r/56181865-dda9-4e35-bf25-571ef2deb066 (ReadonlyParentWithIsset, though that's currently not supported by PHPStan either). However, I wonder if it wouldn't be better for the parent to declare that it permits descendants to redeclare and assign the property (e.g. something like @phpstan-consistent-constructor either for the whole class, or for individual properties). Then we wouldn't need a collector, just a separate rule that would enforce the correct behavior in the parent.

@ondrejmirtes
Copy link
Member

PHPStan requires the parent to assign the property in the constructor

You can't assume that. If someone ignores that error then the property is not assigned in the constructor. I feel like opinionated rules like this shouldn't intervene with each other, it makes the tool less usable.

I feel like https://phpstan.org/r/56181865-dda9-4e35-bf25-571ef2deb066 is a useful error, if we omit the possibility to extend the class which can initialize the property before calling parent::__construct(). So I think this situation should be reported only in final classes.

I wonder if it wouldn't be better for the parent to declare that it permits descendants to redeclare and assign the property

I feel that if we can solve something with collectors, we shouldn't bother the user with new PHPDoc tags.

@schlndh schlndh closed this May 6, 2024
@schlndh schlndh deleted the feature-checkRedeclaredReadonlyProperties branch May 6, 2024 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants