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

Squiz/EmbeddedPhp: add support for short open tag and various bug fixes #196

Merged
merged 25 commits into from Jan 19, 2024

Conversation

jrfnl
Copy link
Member

@jrfnl jrfnl commented Dec 27, 2023

Description

Sometimes I hate my brain.... I came to this sniff to fix #27 and figured I'd get the code coverage up to 100% while I was looking at the sniff anyway (related to #146)... I ended up doing a complete sniff review as wherever I looked, I saw issues in the sniff....

Squiz/EmbeddedPhp: remove some redundant code

The removed condition in the validateInlineEmbeddedPhp() method could never be true as this is already checked in the process() method before this method is ever called.

As that means we will always already have determined the valid close tag, we may as well pass it to both underlying methods instead of determining it again.

This should also yield a small performance improvement, especially when dealing with long files with only an open tag and no a closing tag, as it saves doing the token walking over the complete file for a second time.

Includes moving a comment to a more appropriate place.

Squiz/EmbeddedPhp: rename two confusingly named variables

Squiz/EmbeddedPhp: make test cases more descriptive

... to allow for easier debugging.

Squiz/EmbeddedPhp: add some extra tests with ignore annotations

... just as an extra safeguard.

Squiz/EmbeddedPhp: prevent tests being skipped

The sniff contains a condition which prevents checks from being run on the last tag set when it is a non-single line tag set.

Adding a final open tag will prevent the last test in the file accidentally being skipped, independently of what type of tag set this is, making the test case file more stable.

Squiz/EmbeddedPhp: empty tag set fixer should remove trailing spaces

As things were, the "empty embed" fixers would in certain circumstances leave trailing spaces behind.
For most codebases, this means the sniff would always need to be run in conjunction with a sniff which removes trailing spaces and that there is always a second fixer round needed: the first to fix the embedded PHP formatting, the second to allow the other sniff to remove the trailing spaces introduced by the fixes made by this sniff.

Aside from that, it also makes updating the test case file fiddly.

This commit fixes the "empty embed" fixers to remove trailing spaces, when applicable, and consolidates the code for this in a separate method.

Note: surrounding HTML whitespace, which is not trailing, will not be affected by this fix as the sniff should have no opinion on that.

Covered by pre-existing tests + some additional newly added tests.

Squiz/EmbeddedPhp: opener on own line fixer should remove trailing spaces

As things were, the "opener on own line / content after" fixer would always leave at least one trailing space behind.
For most codebases, this means the sniff would always need to be run in conjunction with a sniff which removes trailing spaces and that there is always a second fixer round needed: the first to fix the embedded PHP formatting, the second to allow the other sniff to remove the trailing spaces introduced by the fixes made by this sniff.

It also makes updating the test case file fiddly.

Secondly, if the space between the open tag and the first content was larger than exactly one space, the sniff would also need an extra fixer round to fix the indent of the "first content", which was moved to the next line and given indent, but with the extra space having being moved as well, the indent would now be too large.

This commit fixes the "opener on own line / content after" fixer to remove trailing spaces and prevent the potential extra fixer loop to fix the indent.

Covered by pre-existing tests + an additional newly added test.

Squiz/EmbeddedPhp: closer on own line fixers should remove trailing spaces

As things were, the "closer on own line" fixers would, in most cases, leave trailing whitespace behind.
For most codebases, this means the sniff would always need to be run in conjunction with a sniff which removes trailing spaces and that there is always a second fixer round needed: the first to fix the embedded PHP formatting, the second to allow the other sniff to remove the trailing spaces introduced by the fixes made by this sniff.

It also makes updating the test case file fiddly.

Additionally, when the next token is inline HTML, it could lead to weird/incorrect indents, which would not be fixable via a sniff (as determining intended indent for inline HTML is non-trivial).

This commit fixes the "closer on own line" fixers to remove trailing spaces and prevents them from affecting the indent of inline HTML unnecessarily.

Covered by pre-existing tests + additional newly added tests.

Squiz/EmbeddedPhp: bug fix - fixer adds stray new line

When the close tag of a previous multi-line embedded PHP snippet and the open tag of the next multi-line embedded PHP snippet are on the same line, the sniff would add a stray blank line with trailing whitespace between the two tokens due to the fixers both running in the same loop.

This commit adds a small tweak preventing this stray blank line from being added.

Includes unit tests proving the issue and safeguarding the fix.

Squiz/EmbeddedPhp: rename test case file

... to allow for additional test case files to be added.

Squiz/EmbeddedPhp: add tests safeguarding that single line embedded PHP blocks are never ignored

The exception conditions for the first/last tag set only apply to multi-line PHP blocks and PHP blocks without a closing tag.
Single-line embedded PHP with a complete set of tags should not be affected.

This is already handled correctly. This commit just adds tests to safeguard this.

Squiz/EmbeddedPhp: sniff for short open tags

As things were, the Squiz.PHP.EmbeddedPhp sniff would only check the formatting for code embedded using long PHP open tags, while short PHP open tags are pretty common for embedded PHP.

This commit adds support for examining short PHP open tags to the sniff.

Notes:

  • The error codes used are the same for both type of open tags, with one exception: for single line embedded statements, it is checked that there is a semi-colon at the end of the statement.
    The "statement" in an embedded snippet using short open tags may just be a single variable and in some lines of thinking it is acceptable to leave out the semi-colon in that case.
    To accommodate this, the error code for the missing semi-colon differentiates between whether the statement uses long open tags NoSemicolon (same as before) or short open tags ShortOpenNoSemicolon ShortOpenEchoNoSemicolon. This will allows for selectively excluding the ShortOpenNoSemicolon ShortOpenEchoNoSemicolon error code.
  • I've considered enforcing that short open tags always are always used as a single-line statement, or adding an option to enforce this, but have decided against this at this time.
    After all, this sniff is about consistent formatting, single line stays single line and multi-line is made consistently/properly multi-line.
    It could be considered future scope to add a separate sniff to enforce short open echo tag sets to always be single line when they only contain a single statement.

Fixes #27

Squiz/EmbeddedPhp: add dedicated tests related to ignoring of first open tag

Squiz/EmbeddedPhp: efficiency fix - bow out early if there is nothing to do

No need to go through the rest of the logic for the multi-line PHP block handling if there is nothing to do for the sniff.

Squiz/EmbeddedPhp: document behaviour for unclosed tag block with content at end of file

Squiz/EmbeddedPhp: document the behaviour for closed tag block at end of file [1]

As things are, the last tag block in a file, at the end of a file, will always be ignored if it contains a PHP close tag, even when it would otherwise trigger errors.

These tests document this behaviour.

Squiz/EmbeddedPhp: document the behaviour for closed tag block at end of file [2]

The last tag block in a file will not be ignored if it is followed by non-empty inline HTML.

These tests document this behaviour.

Squiz/EmbeddedPhp: bug fix - closer on own line indent is not calculated correctly

When the PHP close tag is on the same line as the first PHP content in a multi-line embedded PHP block and the indent of the first PHP content in the block is incorrect, the indent for the close tag when it is moved to its own line will be based on the (incorrect) indent of the first PHP content line and not on the corrected indent for the first PHP content, which the indent fixer uses.

This commit fixes this.

Covered by pre-existing tests and adjusting the "fixed" expectations for those.
Additionally, an extra test has been added for the case when $indent is not pre-calculated (= when the first content in the block is a scope closer).

Squiz/EmbeddedPhp: add test safeguarding handling of blank lines and scope closers

Squiz/EmbeddedPhp: bug fix - content intended indent calculation

The intended content indent calculation contained a pretty nasty bug, though it is unlikely to have been hit much in real code bases.

What was happening:

  • The sniff would try to find the first whitespace token on the line containing the stack pointer.
    If found, the size of the whitespace was taken as the required indent. This part was working correctly.
  • If no whitespace token was found, the sniff would try and find the first inline HTML token.
    If an inline HTML token was found, the leading whitespace in the inline HTML token was taken as the indent. This, again, is correct.
  • However, now it gets iffy... what if no whitespace or inline HTML token was found on the line containing the open tag ? I.e. the situation where the open tag is the first token on the line.
    This situation was not accounted for by the sniff.
    The File::findFirstOnLine() method would return false in that case, which means that the indent calculation would end up like this:
    $indent = (strlen($tokens[false]['content']) - strlen(ltrim($tokens[false]['content'])));
    Now, false is not a valid array key, so this would end up being juggled to 0, meaning the length of the leading whitespace for the first token in the file was being used.

Fixed now.

Includes various additional unit tests safeguarding the fix and one (in a separate file) demonstrating the situation where the bug would have had a real life impact.

Squiz/EmbeddedPhp: bug fix - open tag on own line intended indent calculation

Basically, this is the same bug as fixed in a previous commit for the intended content indent calculation.
The indentation of the first token in the file would be used to calculate the intended indentation, instead of the indentation for the first token on the line containing the PHP open tag.

Fixed now, includes test.

Note: The only reason the test doesn't actually show the bug is because of the earlier "fixer adds stray new line" bugfix in this PR, which means that the fixer for the previous close tag will prevent the buggy fixer from running.
Without that fix, this bug would be visible when running with -vvv (indent would get set to 32 and then corrected to 4 on the next fixer round via the OpenTagIndent fixer).

Squiz/EmbeddedPhp: bug fix - close tag intended indent calculation

Yet another indentation calculation bug.

If the close tag is not on its own line, but is on a line containing a subsequent line of a star-slash style comment, the indentation would not be calculated correctly as the comment token will include the indentation.

Fixed now, includes test.

Squiz/EmbeddedPhp: bug fix - open tag intended indent calculation

Basically, this is the same bug as fixed in a previous commit for the intended close tag indent calculation.
If the first token on the line before the open tag would be a subsequent line for a star-slash comment, the indentation would not be calculated correctly, but would always be set to 0, even when there was indentation.

Fixed now, includes test.

Squiz/EmbeddedPhp: add some extra defensive coding

$firstContentAfterBlock should not be able to be false at this point as it is already checked and handled at the top of the function, but better to be safe than sorry.

Squiz/EmbeddedPhp: document the behaviour for closed tag block at end of file [3]

While the last tag block in a file will not be ignored if it is followed by non-empty inline HTML, the "blank lines before the close tag" check will be ignored if this is the last embedded PHP block.

This test documents this behaviour.

Additional observations

The tag sets for the first PHP open tag and the last PHP close tag, providing the set is multi-line, are explicitly exempt from this sniff to prevent conflicts with sniffs specifically about the start/end of file.

There are two problems with this:

  • Whether something is the first/last tag in the file is determined in a different way than it is typically determined in the start/end of file sniffs.
  • For the tag set which has the last closing tag, other checks, like whether the open tag is on its own line are skipped as well. In practice, the whole tag set is ignored, while I believe the intention was to only ignore the check which involve the closing tag.

For now, I've left that part of the sniff as-is, but this is something which may need to be re-evaluated in the future.

Another issue I see in this sniff is that in multiple places, the sniff looks for the first non-whitespace token after a closing tag. However, unless there is a PHP open tag straight after the close tag, the next token will always be T_INLINE_HTML, which may or may not contain any content.

Suggested changelog entry

Added

  • Squiz/EmbeddedPhp sniff will now also examine the formatting of embedded PHP statements using short open echo tags.
    • Includes a new ShortOpenEchoNoSemicolon errorcode to allow for selectively ignoring missing semicolons in single line embedded PHP snippets within short open echo tags.
    • The other error codes are the same and do not distinguish between what type of open tag was used.

Changed

  • The following sniffs have had performance improvements:
    • Squiz.PHP.EmbeddedPhp

Fixed

  • Squiz/EmbeddedPhp fixer will no longer leave behind trailing whitespace when moving code to another line.
  • Squiz/EmbeddedPhp will now determine the needed indent for code being moved to a new line with higher precision.
  • Squiz/EmbeddedPhp fixer will no longer insert a stray new line when the closer of a multi-line embedded PHP block and the opener of the next multi-line embedded PHP block would be on the same line.

Related issues/external references

Fixes #27
Related to #146

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
    • This change is only breaking for integrators, not for external standards or end-users.
  • Documentation improvement

@jrfnl jrfnl force-pushed the feature/27-squiz-embeddedphp-support-short-open-tag branch 2 times, most recently from 37011f6 to f9a0bde Compare January 8, 2024 20:34
The removed condition in the `validateInlineEmbeddedPhp()` method could never be `true` as this is already checked in the `process()` method before this method is ever called.

As that means we will always already have determined the valid close tag, we may as well pass it to both underlying methods instead of determining it again.

This should also yield a small performance improvement, especially when dealing with long files with only an open tag and no a closing tag, as it saves doing the token walking over the complete file for a second time.

Includes moving a comment to a more appropriate place.
... to allow for easier debugging.
The sniff contains a condition which prevents checks from being run on the last tag set when it is a non-single line tag set.

Adding a final open tag will prevent the _last_ test in the file accidentally being skipped, independently of what type of tag set this is, making the test case file more stable.
As things were, the "empty embed" fixers would in certain circumstances leave trailing spaces behind.
For most codebases, this means the sniff would always need to be run in conjunction with a sniff which removes trailing spaces and that there is always a second fixer round needed: the first to fix the embedded PHP formatting, the second to allow the other sniff to remove the trailing spaces introduced by the fixes made by this sniff.

Aside from that, it also makes updating the test case file fiddly.

This commit fixes the "empty embed" fixers to remove trailing spaces, when applicable, and consolidates the code for this in a separate method.

Note: _surrounding_ HTML whitespace, which is not trailing, will not be affected by this fix as the sniff should have no opinion on that.

Covered by pre-existing tests + some additional newly added tests.
…aces

As things were, the "opener on own line / content after" fixer would always leave at least one trailing space behind.
For most codebases, this means the sniff would always need to be run in conjunction with a sniff which removes trailing spaces and that there is always a second fixer round needed: the first to fix the embedded PHP formatting, the second to allow the other sniff to remove the trailing spaces introduced by the fixes made by this sniff.

It also makes updating the test case file fiddly.

Secondly, if the space between the open tag and the first content was larger than exactly one space, the sniff would also need an extra fixer round to fix the indent of the "first content", which was moved to the next line and given indent, but with the extra space having being moved as well, the indent would now be too large.

This commit fixes the "opener on own line / content after" fixer to remove trailing spaces and prevent the potential extra fixer loop to fix the indent.

Covered by pre-existing tests + an additional newly added test.
…paces

As things were, the "closer on own line" fixers would, in most cases, leave trailing whitespace behind.
For most codebases, this means the sniff would always need to be run in conjunction with a sniff which removes trailing spaces and that there is always a second fixer round needed: the first to fix the embedded PHP formatting, the second to allow the other sniff to remove the trailing spaces introduced by the fixes made by this sniff.

It also makes updating the test case file fiddly.

Additionally, when the next token is inline HTML, it could lead to weird/incorrect indents, which would not be fixable via a sniff (as determining intended indent for inline HTML is non-trivial).

This commit fixes the "closer on own line" fixers to remove trailing spaces and prevents them from affecting the indent of inline HTML unnecessarily.

Covered by pre-existing tests + additional newly added tests.
When the close tag of a previous multi-line embedded PHP snippet and the open tag of the next multi-line embedded PHP snippet are on the same line, the sniff would add a stray blank line with trailing whitespace between the two tokens due to the fixers both running in the same loop.

This commit adds a small tweak preventing this stray blank line from being added.

Includes unit tests proving the issue and safeguarding the fix.
... to allow for additional test case files to be added.
…HP blocks are never ignored

The exception conditions for the first/last tag set only apply to multi-line PHP blocks and PHP blocks without a closing tag.
Single-line embedded PHP with a complete set of tags should not be affected.

This is already handled correctly. This commit just adds tests to safeguard this.
As things were, the `Squiz.PHP.EmbeddedPhp` sniff would only check the formatting for code embedded using long PHP open tags, while short PHP open tags are pretty common for embedded PHP.

This commit adds support for examining short PHP open tags to the sniff.

Notes:
* The error codes used are the same for both type of open tags, with one exception: for single line embedded statements, it is checked that there is a semi-colon at the end of the statement.
    The "statement" in an embedded snippet using short open tags may just be a single variable and in _some_ lines of thinking it is acceptable to leave out the semi-colon in that case.
    To accommodate this, the error code for the missing semi-colon differentiates between whether the statement uses long open tags `NoSemicolon` (same as before) or short open tags `ShortOpenEchoNoSemicolon`. This will allows for selectively excluding the `ShortOpenEchoNoSemicolon` error code.
* I've considered enforcing that short open tags always are always used as a single-line statement, or adding an option to enforce this, but have decided against this at this time.
    After all, this sniff is about _consistent_ formatting, single line stays single line and multi-line is made consistently/properly multi-line.
    It could be considered future scope to add a separate sniff to enforce short open echo tag sets to always be single line when they only contain a single statement.

Fixes 27
… to do

No need to go through the rest of the logic for the multi-line PHP block handling if there is nothing to do for the sniff.
… of file [1]

As things are, the last tag block in a file, at the end of a file, will always be ignored if it contains a PHP close tag, even when it would otherwise trigger errors.

These tests document this behaviour.
… of file [2]

The last tag block in a file will **not** be ignored if it is followed by non-empty inline HTML.

These tests document this behaviour.
…ted correctly

When the PHP close tag is on the same line as the first PHP content in a multi-line embedded PHP block and the indent of the first PHP content in the block is incorrect, the indent for the close tag when it is moved to its own line will be based on the (incorrect) indent of the first PHP content line and not on the corrected indent for the first PHP content, which the indent fixer uses.

This commit fixes this.

Covered by pre-existing tests and adjusting the "fixed" expectations for those.
Additionally, an extra test has been added for the case when `$indent` is not pre-calculated (= when the first content in the block is a scope closer).
The intended content indent calculation contained a pretty nasty bug, though it is unlikely to have been hit much in real code bases.

What was happening:
* The sniff would try to find the first whitespace token on the line containing the stack pointer.
    If found, the size of the whitespace was taken as the required indent. This part was working correctly.
* If no whitespace token was found, the sniff would try and find the first inline HTML token.
    If an inline HTML token was found, the leading whitespace in the inline HTML token was taken as the indent. This, again, is correct.
* However, now it gets iffy... what if no whitespace or inline HTML token was found on the line containing the open tag ? I.e. the situation where the open tag _is_ the first token on the line.
    This situation was not accounted for by the sniff.
    The `File::findFirstOnLine()` method would return `false` in that case, which means that the indent calculation would end up like this:
    ```php
    $indent = (strlen($tokens[false]['content']) - strlen(ltrim($tokens[false]['content'])));
    ```
    Now, `false` is not a valid array key, so this would end up being juggled to `0`, meaning the length of the leading whitespace for the first token in the **file** was being used.

Fixed now.

Includes various additional unit tests safeguarding the fix and one (in a separate file) demonstrating the situation where the bug would have had a real life impact.
…culation

Basically, this is the same bug as fixed in a previous commit for the intended content indent calculation.
The indentation of the first token in the file would be used to calculate the intended indentation, instead of the indentation for the first token on the line containing the PHP open tag.

Fixed now, includes test.

Note: The only reason the test doesn't actually show the bug is because of the earlier "fixer adds stray new line" bugfix in this PR, which means that the fixer for the previous close tag will prevent the buggy fixer from running.
Without _that_ fix, this bug would be visible when running with `-vvv` (indent would get set to 32 and then corrected to 4 on the next fixer round via the `OpenTagIndent` fixer).
Yet another indentation calculation bug.

If the close tag is not on its own line, but is on a line containing a subsequent line of a star-slash style comment, the indentation would not be calculated correctly as the comment token will include the indentation.

Fixed now, includes test.
Basically, this is the same bug as fixed in a previous commit for the intended close tag indent calculation.
If the first token on the line before the open tag would be a subsequent line for a star-slash comment, the indentation would not be calculated correctly, but would always be set to `0`, even when there was indentation.

Fixed now, includes test.
`$firstContentAfterBlock` should not be able to be `false` at this point as it is already checked and handled at the top of the function, but better to be safe than sorry.
… of file [3]

While the last tag block in a file will **not** be ignored if it is followed by non-empty inline HTML, the "blank lines before the close tag" check _will_ be ignored if this is the last embedded PHP block.

This test documents this behaviour.
@jrfnl jrfnl force-pushed the feature/27-squiz-embeddedphp-support-short-open-tag branch from f9a0bde to 50ae189 Compare January 19, 2024 04:25
@jrfnl
Copy link
Member Author

jrfnl commented Jan 19, 2024

Rebased without changes before merge.

@jrfnl jrfnl enabled auto-merge January 19, 2024 04:26
@jrfnl jrfnl merged commit 73a6cae into master Jan 19, 2024
44 checks passed
@jrfnl jrfnl deleted the feature/27-squiz-embeddedphp-support-short-open-tag branch January 19, 2024 04:31
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.

Squiz/EmbeddedPhp doesn't examine code using short open echo tag
2 participants