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

Improve type annotations #290

Merged
merged 7 commits into from
Mar 28, 2023
Merged

Improve type annotations #290

merged 7 commits into from
Mar 28, 2023

Conversation

xificurk
Copy link
Contributor

@xificurk xificurk commented Feb 7, 2023

  • new feature
  • BC break? no
  • doc PR: not needed

Add more specific type annotations to improve static analysis support.

src/Utils/Image.php Outdated Show resolved Hide resolved
@xificurk xificurk force-pushed the phpstan-types branch 2 times, most recently from 21c3a34 to d82f3ca Compare February 7, 2023 20:45
@@ -15,6 +15,8 @@
/**
* Provides the base class for a generic list (items can be accessed by index).
* @template T
* @implements \IteratorAggregate<int, T>
Copy link
Member

Choose a reason for hiding this comment

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

Isn't that duplicate information?

Copy link
Contributor

Choose a reason for hiding this comment

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

It is, but just for getIterator(). Implementing IteratorAggregate also passes generic type to Traversable. Without it Traversable type is mixed. Not sure if this specific case is problem, but generally speaking, generic interface should be always implemented.

Generic type from getIterator() should be removed, as it is already defined by IteratorAggregate

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation. Can I ask @xificurk to remove the annotation from getIterator()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a bit more complicated...

Latest PHPStan can infer key/value types even without the @implements annotation. Actually, the correct annotations on methods like getIterator() are the key to get this working.

The @implements annotations on class come into play only when checking compatibility between e.g. ArrayList<Foo> and Traversable<string>.

Example with method annotations only:
https://phpstan.org/r/0ecafc0e-1f6a-422b-8121-601992d34b25

Example with class annotation only:
https://phpstan.org/r/ec2e29f0-53a0-45c8-9030-dcfb5cb3ab57

@@ -225,6 +225,7 @@ public static function flatten(array $array, bool $preserveKeys = false): array

/**
* Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list.
* @return ($value is list ? true : false)
Copy link
Member

Choose a reason for hiding this comment

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

What's it good for?

Copy link
Contributor

Choose a reason for hiding this comment

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

If variable is of type list it will always return true. Informs user that check is effectively useless.

But it should probably be @return ($value is list ? true : bool)

Copy link
Member

Choose a reason for hiding this comment

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

I understand what it does, I ask what good it does :-) I just need an example.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here it is. If condition is always true.
https://phpstan.org/r/37d792a0-1fe7-488a-8818-15048810f6c0

Copy link
Contributor

Choose a reason for hiding this comment

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

Sadly it is only half of the solution and it seems phpstan does not support assert for list. Even after isList() is given value array
https://phpstan.org/r/c26d6864-a46a-4d81-9fad-54dbc75d7e83

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mabar @enumag Why do you think the return type should be @return ($value is list ? true : **bool**)? Can you give me example of value type which is not list, but the method will return true?

This change was actually the reason, why the examples you've posted seemingly did not work. The correct conditional return annotation is enough for phpstan to correctly detect always true/false conditions & perform type narrowing.

https://phpstan.org/r/c3f4cdc0-3417-42d8-9e5f-24f1c722b01c

Copy link
Contributor

@enumag enumag Mar 20, 2023

Choose a reason for hiding this comment

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

@xificurk Because $value can be a list without PHPStan being aware of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@enumag Can you create example in phpstan playground?

btw, isn't it exactly the case of the last if-else block in my example? PHPStan can't know if the value is list or not, but if it is, the isList() method will always return true, otherwise false.

Copy link
Contributor

Choose a reason for hiding this comment

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

I tried a few things and yeah you're right, false is enough there. PHPStan does treat unknown array as a possible list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For completeness, here is a good example for it: https://phpstan.org/r/8f789bd9-8fbe-477c-87e1-e7c2cad3d6f2

* @property-read int $width
* @property-read int $height
* @property-read positive-int $width
* @property-read positive-int $height
Copy link
Member

Choose a reason for hiding this comment

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

What's it good for?

Copy link
Contributor

Choose a reason for hiding this comment

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

Mostly for compatibility with other classes that already expect width and height to be >= 0. e.g. for database entity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As any type (narrowing) hint, it's good to make code more readable and eliminate useless checks.

If you rely on static analysis and narrow down the types enough, checks like this become useless and can be safely eliminated.

Dimensions are often used in various calculations. Knowing that those are actually positive integers, implies e.g.:

  • their ratio is well defined and also positive number
  • the area (number of pixels in image) is positive integer

@@ -267,6 +271,8 @@ public static function detectTypeFromString(string $s, &$width = null, &$height

/**
* Returns the file extension for the given `Image::XXX` constant.
* @param self::JPEG|self::PNG|self::GIF|self::WEBP|self::AVIF|self::BMP $type
* @return value-of<self::Formats>
Copy link
Member

Choose a reason for hiding this comment

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

What's @return annotation good for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For me the most important thing out of this is specifying that the output extension is non-empty string, which is useful when you want to use the extension further. I guess @return non-empty-string could cover most of the use-cases, but why shouldn't we make the return type as narrow as possible?

Copy link
Member

Choose a reason for hiding this comment

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

Now, as a return annotation, this should probably be ImageType::*, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because it returns one of the string values contained in self::Formats, not ImageType::* value.

src/Utils/Image.php Outdated Show resolved Hide resolved
@xificurk xificurk marked this pull request as draft February 8, 2023 07:43
@dg dg force-pushed the master branch 2 times, most recently from 55488d5 to 3678b20 Compare February 8, 2023 10:09
@dg
Copy link
Member

dg commented Mar 19, 2023

I would like to release version 4.0.1 soon, we can finish this somehow @xificurk ?

@xificurk
Copy link
Contributor Author

@dg I'll take a look today/tomorrow.

@xificurk xificurk marked this pull request as ready for review March 20, 2023 19:29
@xificurk
Copy link
Contributor Author

@dg rebased & squashed, I think it's ready from my point of view.

@dg
Copy link
Member

dg commented Mar 28, 2023

Thanks!

@dg dg merged commit 6ab8350 into nette:master Mar 28, 2023
dg pushed a commit that referenced this pull request Mar 28, 2023
@xificurk xificurk deleted the phpstan-types branch March 28, 2023 20:41
dg pushed a commit that referenced this pull request Jul 30, 2023
dg pushed a commit that referenced this pull request Jul 30, 2023
dg pushed a commit that referenced this pull request Jul 30, 2023
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.

None yet

4 participants