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

Detect enum duplicated values #2371

Merged
merged 12 commits into from
May 23, 2023

Conversation

kubk
Copy link

@kubk kubk commented May 4, 2023

@phpstan-bot
Copy link
Collaborator

You've opened the pull request against the latest branch 1.11.x. If your code is relevant on 1.10.x and you want it to be released sooner, please rebase your pull request and change its target to 1.10.x.

@kubk kubk force-pushed the detect-duplicate-enum-values branch from 786b68e to 36d2b9c Compare May 4, 2023 09:37
@kubk kubk changed the base branch from 1.11.x to 1.10.x May 4, 2023 09:38

$enumCases = [];
foreach ($node->stmts as $stmt) {
if (!($stmt instanceof Node\Stmt\EnumCase && ($stmt->expr instanceof Node\Scalar\LNumber || $stmt->expr instanceof Node\Scalar\String_))) {
Copy link
Member

Choose a reason for hiding this comment

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

You could check more things here. Like:

  1. Wrong case value type against the type declared in $node->scalarType.
  2. Missing case value in backed enum, and extra case value in case of non-backed enum.

All three cases here should fail: https://phpstan.org/r/3ca06994-1a6e-4686-a57c-6083c6bdb34e

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the suggestions, I'll implement them.

Copy link
Author

@kubk kubk May 4, 2023

Choose a reason for hiding this comment

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

@ondrejmirtes Done. 3 tests are failing but they seem like unrelated to my changes.

->line($stmt->getLine())
->nonIgnorable()
->build();
}
Copy link
Author

@kubk kubk May 6, 2023

Choose a reason for hiding this comment

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

Here I had to use early return (if (!$isStringBackedWithoutStringCase) {) because linter forced me to do that.

Early return is a great rule to reduce code complexity, but in my opinion in this situation it made the code a bit harder to follow. Because instead of this code:

foreach ($enumCases as $enumCase) {
  if ($isIntBackedWithoutIntValue) {
   // add error
  }
  if ($isStringBackedWithoutStringValue) {
   // add error
  }
}

we got:

foreach ($enumCases as $enumCase) {
  if ($isIntBackedWithoutIntValue) {
   // add error
  }
  if (!$isStringBackedWithoutStringValue) {
    continue;
  }
  // add error
}

I saw that this rule was even suppressed once in phpstan repository:

https://github.com/phpstan/phpstan-deprecation-rules/blob/9d366debb01a8097106174e46c9aa138d3bf1c01/src/Rules/Deprecations/FetchingDeprecatedConstRule.php#L30

Copy link
Contributor

Choose a reason for hiding this comment

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

You might want to look into fine-tuning https://github.com/slevomat/coding-standard/blob/master/doc/control-structures.md#slevomatcodingstandardcontrolstructuresearlyexit-

I obviously can't speak for Ondřej, but the last time a similar thing like this was discussed it sounded like he is open to look into adapting it.

@ondrejmirtes ondrejmirtes force-pushed the detect-duplicate-enum-values branch from 1f1e5cc to 7edd4dd Compare May 9, 2023 15:06
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.

Hi, I just pushed some cosmetic changes that make the rule more consistent with others.

I still need a few changes from you:

  1. Missing case value in backed enum should have a special message - put if ($stmt->expr === null) below if ($node->scalarType === null) and handle it separately. This should make the test suite not crash again.
  2. Wrong scalar type like float or object is not yet reported AFAIK.

@kubk kubk force-pushed the detect-duplicate-enum-values branch from ad012f9 to 7edd4dd Compare May 12, 2023 05:16
@kubk
Copy link
Author

kubk commented May 13, 2023

Hi @ondrejmirtes

I've implemented what you asked. Let's summarize what is done:

1. Detecting duplicate values.
Covered code: https://github.com/phpstan/phpstan-src/pull/2371/files#diff-ff846fb46bb3972dcf0a281f7d686cd1fbc84f2d282966d6acdd0d7965e94a77R86

The rule checks that there are at least 2 duplicate case values.

2. Inconsistent case type and backed type
Covered code: https://github.com/phpstan/phpstan-src/pull/2371/files#diff-ff846fb46bb3972dcf0a281f7d686cd1fbc84f2d282966d6acdd0d7965e94a77R105

The rule checks that enum's scalar type and all the case types match.

3. Enum isn't backed, but has a backed case
Covered code: https://github.com/phpstan/phpstan-src/pull/2371/files#diff-ff846fb46bb3972dcf0a281f7d686cd1fbc84f2d282966d6acdd0d7965e94a77R113

The rule triggers when scalar type is empty, but a case isn't

4. Enum is backed, but one of the cases has value missed
Covered code: https://github.com/phpstan/phpstan-src/pull/2371/files#diff-ff846fb46bb3972dcf0a281f7d686cd1fbc84f2d282966d6acdd0d7965e94a77R106

UPD: I have added one more example because I mistakenly hardcoded the error message: https://github.com/phpstan/phpstan-src/pull/2371/files#diff-ff846fb46bb3972dcf0a281f7d686cd1fbc84f2d282966d6acdd0d7965e94a77R110

The rule triggers when scalar type isn't empty, but a case is.

@kubk
Copy link
Author

kubk commented May 13, 2023

@ondrejmirtes

Wrong scalar type like float or object is not yet reported AFAIK.

It has been already implemented:

enum BackedEnumWithFloatType: float

'Backed enum EnumSanity\BackedEnumWithFloatType can have only "int" or "string" type.',

@kubk
Copy link
Author

kubk commented May 13, 2023

@ondrejmirtes I have a question regarding your fix: 7edd4dd

foreach ($enumCases as $caseValue => $caseNames) {
if (count($caseNames) <= 1) {
continue;
}
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s has duplicate value %s for %s %s.',
$node->namespacedName->toString(),
$caseValue,
count($caseNames) === 1 ? 'case' : 'cases',

You've replaced 'cases' with count($caseNames) === 1 ? 'case' : 'cases'.

This line has triggered PHPStan, because due to line 192 it will always evaluate to false (it's great that PHPStan noticed it!). But as I understand the code should be reverted. $caseNames is a map, the example structure is ['a' => ['case1', 'case2'], 'b' => 'case3'] for the following enum:

enum Test: string
{
  case case1 = 'a';
  case case2 = 'a';
  case case3 = 'b';
}

So when we see that a value has at least 2 corresponding cases - we report duplication. So I brought it back to make PHPStan check pass. Let me know if it needs to be improved somehow.

@ondrejmirtes ondrejmirtes merged commit 961a73f into phpstan:1.10.x May 23, 2023
@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
Development

Successfully merging this pull request may close these issues.

Backed Enum Duplicate Value
4 participants