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

Add Universal.CodeAnalysis.MixedBooleanOperator #271

Closed
wants to merge 36 commits into from
Closed

Add Universal.CodeAnalysis.MixedBooleanOperator #271

wants to merge 36 commits into from

Conversation

TimWolla
Copy link

As previously discussed on Fediverse. I've copied over the logic and tests as-is, just making the necessary adjustments to fit the code style of PHPCSExtra, to make it easy to include the Sniff in CodeSniffer itself in the future.


This is a clone of the proposed sniff in squizlabs/PHP_CodeSniffer#3205. Adding it to PHPCSExtra to make it generally available quicker than by including it in core CodeSniffer.

This is a clone of the proposed sniff in squizlabs/PHP_CodeSniffer#3205. Adding
it to PHPCSExtra to make it generally available quicker than by including it in
core CodeSniffer.
Copy link
Member

@jrfnl jrfnl left a comment

Choose a reason for hiding this comment

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

Hi @TimWolla, oh well, figured I may as well review it straight away. Thanks for getting this PR transferred over!

Naming of the sniff

Question: should the sniff name be MixedBooleanOperator or MixedBooleanOperators (note the plural) ?
Personally, I'm leaning towards the plural form as if there is only one operator, the issue doesn't exist and there will never be a mix.

Note: we need to settle on a good name now as renaming the sniff later would be a breaking change.

Premise of the sniff

As things are, the sniff only concerns itself with the boolean and/or operators.
Questions:

  • Should it be documented (in the class docblock) why the boolean not ! operator is not included in this sniff ?
  • Should the sniff also apply to the logical and/or/xor operators ?
    If not, maybe we should open an issue to create a sister-sniff for the logical operators ?

Review notes:

Docs: checked against the typical issues (line length too long, non-space indentation, missing <em> tags etc) and everything is 💯 okay.

Sniff & tests: see my inline notes.

Metrics are not relevant for this sniff. ✔️
Auto-fixing could be possible for this sniff, but would involve an assumption about the correctness of the code as written, so I'm fine with this sniff not having a fixer and letting a human decide where the parentheses should be. ✔️

I'm also trying to think if there is a situation in which the sniff should stop scanning or behave differently when an operator with a higher precedence is encountered ?
Note: the $phpcsFile->findStartOfStatement($stackPtr) call does not take this into account, so the sniff would have to.

Other

The build failures related to the code coverage checks are a known issue for PRs from forks and I'm working with the Coveralls team on a solution. You don't need to worry about this (though checking code coverage locally for the sniff would be good and will show that the sniff as-is is not completely covered by tests).

image

Hope this review helps. Please let me know if you have any questions or would like me to clarify any of my remarks.

@TimWolla
Copy link
Author

Handled the obvious stuff already. Will look into the stuff that needs some additional thinking later. Thank you for the review so far!

@TimWolla TimWolla requested a review from jrfnl September 18, 2023 14:52

if (
$previous === false
|| \in_array($tokens[$previous]['code'], [\T_INLINE_THEN, \T_INLINE_ELSE], true)
Copy link
Author

Choose a reason for hiding this comment

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

Should these be implicitly handled when setting local to true for findPrevious?

Copy link
Member

Choose a reason for hiding this comment

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

PHPCS does not take these into account. Whether it should or not, is a different debate and I'd rather not pollute this PR with that discussion as we can't solve it in the PHPCSExtra repo anyway.

Copy link
Member

@jrfnl jrfnl left a comment

Choose a reason for hiding this comment

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

I haven't done a full re-review yet, but thought I'd give you the below feedback already.

@TimWolla
Copy link
Author

Naming of the sniff

Question: should the sniff name be MixedBooleanOperator or MixedBooleanOperators (note the plural) ? Personally, I'm leaning towards the plural form as if there is only one operator, the issue doesn't exist and there will never be a mix.

This makes sense. Should this also include the “Binary” part within the name? i.e. MixedBinaryBooleanOperators?

Premise of the sniff

As things are, the sniff only concerns itself with the boolean and/or operators. Questions:

* Should it be documented (in the class docblock) why the boolean not `!` operator is not included in this sniff ?

With including the Binary in the name this should be clear. But I can add a short blurb (or you add some suggestion using GitHub's feature.

* Should the sniff also apply to the logical `and`/`or`/`xor` operators ?
  If not, maybe we should open an issue to create a sister-sniff for the logical operators ?

Included now.

Review notes:

Auto-fixing could be possible for this sniff, but would involve an assumption about the correctness of the code as written, so I'm fine with this sniff not having a fixer and letting a human decide where the parentheses should be. ✔️

Yes, this Sniff is intentionally written for detecting possibly incorrect code and we found several instances in our code base with the initial version I proposed. That's why autofixing must not happen.

I'm also trying to think if there is a situation in which the sniff should stop scanning or behave differently when an operator with a higher precedence is encountered ? Note: the $phpcsFile->findStartOfStatement($stackPtr) call does not take this into account, so the sniff would have to.

The Sniff tripping over such a situation would likely indicate that the human would also be confused. I am fine with some false-positives should there be such a situation, you can't have enough parentheses to make precedence clear. And if you mix and and && or others, you are really asking for trouble if you don't use parentheses.

Other

The build failures related to the code coverage checks are a known issue for PRs from forks and I'm working with the Coveralls team on a solution. You don't need to worry about this (though checking code coverage locally for the sniff would be good and will show that the sniff as-is is not completely covered by tests).

I wasn't able to get coverage running locally, due to the ancient PHPUnit requirement that is not trivially compatible with PHP 8.2. But with the updated code there isn't really much logic left in the sniff itself. If you would check that it's up to 100% coverage now, I'd appreciate it.

@TimWolla
Copy link
Author

The commit history is a little messy due to the number of changes required due to my lack of knowledge of CodeSniffer. I can squash it for you once you are happy with the diff.

@jrfnl
Copy link
Member

jrfnl commented Sep 19, 2023

The commit history is a little messy due to the number of changes required due to my lack of knowledge of CodeSniffer. I can squash it for you once you are happy with the diff.

Yes please. Would be great if you could do that once the PR is ready for merge.

@TimWolla
Copy link
Author

TimWolla commented Sep 21, 2023

You already agreed, so all is fine, but I wanted to note this nonetheless:

Except there isn't really room for interpretation due to the $payloadFields = and = having higher precedence than any of the operators this sniff is looking for.

I actually needed to print the AST using https://github.com/nikic/PHP-Parser to be sure how it was interpreted.

In fact the fact that

$a && $d = null === $b || true === $c ? true : false;

is

$a && ($d = ((null === $b || true === $c) ? true : false));

but

$a && null === $b || true === $c ? true : false;

is

(($a && null === $b) || true === $c) ? true : false;

is surprising to me.

@jrfnl
Copy link
Member

jrfnl commented Sep 21, 2023

You already agreed, so all is fine, but I wanted to note this nonetheless:

Except there isn't really room for interpretation due to the $payloadFields = and = having higher precedence than any of the operators this sniff is looking for.

I actually needed to print the AST using https://github.com/nikic/PHP-Parser to be sure how it was interpreted.

In fact the fact that

$a && $d = null === $b || true === $c ? true : false;

is

$a && ($d = ((null === $b || true === $c) ? true : false));

but

$a && null === $b || true === $c ? true : false;

is

(($a && null === $b) || true === $c) ? true : false;

is surprising to me.

Guess, you never had the chance to see my "The big "Why equal doesn't equal" Quiz" ? 😂

But yes, operator precedence can be a bitch and I see way too many issues created because people don't understand/know it well enough (which is all the more reason why I'm happy to accept this sniff in this package).

You already saw PR #277 and yes, that was fun to write what with the operator precedence issues which could be caused when auto-fixing ;-)

@TimWolla
Copy link
Author

Sorry, my bad. This is the correct code sample for the false positive (I copied a bit too much when I added the comments):

Simplified:

<?php

match (true) {
	$a || ($b && $c) => true,
};

This is somehow caused by the match() behaving differently than e.g. an array:

<?php

[
	$a || ($b && $c) => true,
];

Specifically findStartOfStatement returns the $a for both operators in the match() and $a for the || and $b for the && in the array.

While I don't want to blame the upstream library, I must say that this is very surprising behavior for constructs that are so similar. Is this a bug in CodeSniffer itself or is this intentional? If this is intentional, do you have some specific advice / code snippet on how to fix this in this Sniff, ideally without replicating the entire logic?

@TimWolla TimWolla requested a review from jrfnl October 4, 2023 17:34
Copy link
Member

@jrfnl jrfnl left a comment

Choose a reason for hiding this comment

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

I've given the sniff another look over and left some comments, but it looks like a number of my remarks from my previous comment haven't been addressed yet:

  • Deciding on a final name for the sniff.
  • Deciding whether to include the "binary" terminology in the messaging. I'm unsure whether that would clarify things or cause more confusion.
  • "Boolean operators" vs "logical operators" terminology.
  • Adding some tests which involve arrow functions.

While I don't want to blame the upstream library, I must say that this is very surprising behavior for constructs that are so similar. Is this a bug in CodeSniffer itself or is this intentional? If this is intentional, do you have some specific advice / code snippet on how to fix this in this Sniff, ideally without replicating the entire logic?

Looks like this was done by design in PHPCS upstream, though I'm not sure that was the correct choice. Having said that, I don't think comparing match conditions with array keys is an even remotely fair comparion. Array keys are always singular (one key per item), while match branches can have multiple comma-separated conditions, which, what with the comma also being the end of the "return" statement makes parsing match expressions based on the tokens a lot harder.

If needs be, I'd be okay with adding the simplified match code to the tests and marking this as a known false positive + opening an issue to follow up on this later (and adding a link to the issue as a comment to the test).

@TimWolla
Copy link
Author

TimWolla commented Oct 5, 2023

I've given the sniff another look over and left some comments, but it looks like a number of my remarks from my previous comment

Yes, I plan to do the renaming and related changes once the functionality is clear and correct to avoid unnecessary churn. I was waiting for advice regarding the match statement, before proceeding. All the stuff that is not yet collapsed still is on my list.

@TimWolla
Copy link
Author

TimWolla commented Oct 6, 2023

Okay, I managed to fix the handling for the match case and added arrow function tests.

I believe this should now be everything with regard to the implementation. I'll think about the naming adjustments once you confirm the implementation is correct and complete.

Do I still need “syntax error” tests or did that become irrelevant with the simplified implementation?

@jrfnl
Copy link
Member

jrfnl commented Oct 11, 2023

Okay, I managed to fix the handling for the match case and added arrow function tests.

@TimWolla Thank you for making those changes and updates. I've tried to come up with cases which could break the current implementation, but my imagination struck out, so I think we're good. Well done! 🎉

Might be worth adding the following extra tests still to safeguard things further, but these are already handled correctly:

// Not ok.
match (true) {
    $a || ($b && $c) && $d => true,
    $b && $c['a'] || $d => true,
    $b && ${$var} || $d => true,
};

I believe this should now be everything with regard to the implementation. I'll think about the naming adjustments once you confirm the implementation is correct and complete.

I'm happy with the implementation as it is now. Let's get the name sorted & get this merged!

Do I still need “syntax error” tests or did that become irrelevant with the simplified implementation?

With the current implementation, I don't see a case where a parse error could cause the sniff to throw a PHP notice, so as far as I'm concerned it's not needed for the sniff as-it-is now.

@TimWolla
Copy link
Author

Okay, I:

  • Added the additional test case.
  • Explained why ! is not handled.
  • Adjusted the copyright line.
  • Added the requested references to other sniffs.
  • Renamed the Sniff to RequireExplicitBooleanOperatorPrecedence. I've opted for “Boolean” instead of “Logical”, because:
    • The internal token array is called $booleanOperators.
    • It's mainly about && and || (because no one should use and, or, xor in the first place) and the internal tokens are T_BOOLEAN_AND, T_BOOLEAN_OR vs T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR for the others.
    • PSR-12 uses “boolean operator”.
    • It matches the name of PSR12.ControlStructures.BooleanOperatorPlacementSniff, which is reasonably related, because operator placement and precedence go hand in hand.

@TimWolla
Copy link
Author

Will squash the entire commit history once you approve that everything is good.

@TimWolla TimWolla requested a review from jrfnl October 16, 2023 12:16
TimWolla added a commit to TimWolla/PHP_CodeSniffer that referenced this pull request Oct 16, 2023
@jrfnl jrfnl removed this from the 1.2.0 milestone Dec 1, 2023
@jrfnl
Copy link
Member

jrfnl commented Dec 1, 2023

FYI: I've moved this PR out of the 1.2.0 milestone as, what with the PHPCS repo move, the sniff can go into the main PHPCS repo as soon as its been pulled there (as it's been extensively reviewed here already), which is what the Op indicated originally as the preferred option. I've discussed this with @TimWolla off-GH.

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

Successfully merging this pull request may close these issues.

None yet

2 participants