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

Added new type-safe API that allows validating a given value against a given class-string<Enum> #143

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

Ocramius
Copy link
Contributor

@Ocramius Ocramius commented Mar 11, 2021

This patch introduces two new methods:

  • Enum::isValidEnumValue()
  • Enum::assertValidEnumValue()

The Enum::isValidEnumValue() is still wonky due to vimeo/psalm#5372, but
overall, this allows for matching against other Enum sub-types, by validating that a given $value
is effectively a subtype of T within an Enum<T>.

The previous approach did not work, because static method types are not coupled with the class they
are put on (they are effectively namespaced functions, and not much else).

Detailed explanation of what was done is available commit-by-commit, but the idea is that it is impossible to validate T for a given Enum<T> when a static method is used to do so, because static methods, by definition, are not coupled to the class they live on.

Therefore, I had to be a bit more creative: Enum::assertValidEnumValue(ConcreteEnumClass::class, $value) is to be used instead.

TODOs:

/cc @michaelpetri @guidobonuzzi @DavideBicego

These added tests verify that:

 * `Enum::assert*` methods can be used in a pure form
 * `Enum::from*` methods can be used in a pure form
 * `Enum#__construct()` properly binds the instance type of the enumerable with the input value
 * `Enum::assert*` methods currently **CANNOT** bind the input to an enumerable potential value (BUG)
 * `Enum::from*` methods currently **CANNOT** bind the input to an enumerable potential value (BUG)

In addition to that, `psalm.xml` has been made a bit stricter:

 * `resolveFromConfigFile` was removed (we were using the default value)
 * `findUnusedPsalmSuppress` was added, allowing us to find whether intentional suppressions are now
    invalid. This allows us to verify negative scenarios, in which we **expect** a type error to appear
 * `restrictReturnTypes` better refines templated types, requiring inputs/outputs to match for those too
…ound to any template

These functions had a `T` template parameter in them, but this `T` is completely decoupled
from the one defined at class-level (and therefore at `Enum` instance level, rather than
statically):

 * `Enum::from()`
 * `Enum::isValid()`
 * `Enum::assertValidValue()`
 * `Enum::assertValidValueReturningKey()` (internal detail)

In practice, this means that myclabs#135 (Added new named constructor to create enum from mixed)
was not a valid approach to infer types for enumerable values, when performed statically.

A new approach will be attempted, but the current one will likely need to be scrapped for
now.

In practice, `@psalm-assert`, `@psalm-assert-if-true` and `@psalm-return static<T>` had no
effect on these methods, due to a design-level issue that wasn't spotted (by myself either)
at review.
…a given `class-string<Enum>`

This patch introduces two new methods:

 * `Enum::isValidEnumValue()`
 * `Enum::assertValidEnumValue()`

The `Enum::isValidEnumValue()` is still wonky due to vimeo/psalm#5372, but
overall, this allows for matching against other `Enum` sub-types, by validating that a given `$value`
is effectively a subtype of `T` within an `Enum<T>`.

The previous approach did **not** work, because static method types are not coupled with the class they
are put on (they are effectively namespaced functions, and not much else).

In here, we also:
 * completed test coverage for `Enum::from()`
 * pinned `vimeo/psalm:4.6.2`, mostly for build stability
 * added coverage mapping for `phpunit.xml`
@@ -28,6 +28,6 @@
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.6.2"
"vimeo/psalm": "4.6.2"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pinned: changing this dependency is too fragile for builds to be kept stable.

My suggestion is to instead use dependabot to regularly keep it updated.

}

/** @psalm-pure */
function canCallFromWithKnownValue(): InstantiatedEnum
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Notice how we CANNOT infer InstantiatedEnum<'C'> from this signature: this is a known problem with Enum::from() (static method), as I've described in the patch overview.

@@ -11,7 +11,7 @@
* @method static PureEnum C()
*
* @psalm-immutable
* @psalm-template T of 'A'|'B'
* @psalm-template T of 'A'|'C'
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 was a bug with this file - otherwise all good.

* @psalm-return 'A'|'C'
*
* @psalm-suppress MixedReturnStatement
* @psalm-suppress MixedInferredReturnType at the time of this writing, we did not yet find
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 comment is kinda oudated - perhaps I can drop the static analysis check overall, now that I designed Enum::assertValidEnumValue() 🤔

* @psalm-return 'A'|'C'
*
* @psalm-suppress MixedReturnStatement
* @psalm-suppress MixedInferredReturnType at the time of this writing, we did not yet find
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 comment is kinda oudated - perhaps I can drop the static analysis check overall, now that I designed Enum::isValidEnumValue() 🤔

* @psalm-return 'A'|'C'|1
*
* @psalm-suppress InvalidReturnType https://github.com/vimeo/psalm/issues/5372
* @psalm-suppress InvalidReturnStatement https://github.com/vimeo/psalm/issues/5372
Copy link
Contributor Author

Choose a reason for hiding this comment

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

vimeo/psalm#5372 is still kinda blocking this

* @return bool
*
* deprecated use {@see Enum::isValidEnumValue()} instead
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 not yet @deprecated - should I mark it as such, or should I drop the comment and leave it be?

* @psalm-pure
* @param mixed $value
*
* deprecated use {@see Enum::assertValidEnumValue()} instead
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 not yet @deprecated - should I mark it as such, or should I drop the comment and leave it be?

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
restrictReturnTypes="true"
findUnusedPsalmSuppress="true"
totallyTyped="true"
Copy link
Contributor

Choose a reason for hiding this comment

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

Psalm docs say this is deprecated, might be dropped as level is max already

https://psalm.dev/docs/running_psalm/configuration/#totallytyped

Copy link
Contributor

Choose a reason for hiding this comment

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

WithtotallyTyped="true" the default level is 1, but without totallyTyped it would be 2: https://github.com/vimeo/psalm/pull/5340/files
So I think we should replace totallyTyped="true" with errorLevel="1".

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, it's a bit out of scope but has been touched already so 🤷🏾

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I generally remove errorLevel wherever I go: it's just confusing.

@mnapoli
Copy link
Member

mnapoli commented Mar 13, 2021

Hey! Just to be sure I understand, these 2 new methods are only useful for static analysis right?

If we can avoid that that would be great though (I'm not sold on deprecating current methods which are simpler to use).

How about:

public static function isValid($value, ?string $enumType = null)

The new parameter would be optional, and only those using static analysis would go through the trouble of providing the class name. Would that work?

@Ocramius
Copy link
Contributor Author

Heads up: spent a lot of time debugging issues around this today, and will certainly work more on the patch as soon as I can, but I think what I'll do for now is removing <T> from these existing methods, so we fix the immediate issue (will open a new PR for that).

@Ocramius
Copy link
Contributor Author

(I'm not sold on deprecating current methods which are simpler to use).

Agree, probably better to just leave them be.

The new parameter would be optional, and only those using static analysis would go through the trouble of providing the class name. Would that work?

That is an excellent question, and I do indeed need to try it out! 👍

@boesing
Copy link

boesing commented Jul 14, 2021

I recently stumbled upon this as well. If there is something I can help with @Ocramius, let me know.

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.

6 participants