Did you ever get a little annoyed at phpcs
? Maybe you’re like me: I like the idea of PHP Code Sniffer (phpcs), but I think the user experience has room for improvement:
Especially when trying to get larger amounts of PHP code to comply with the coding standards, it feels like this tool just scolds you and all you can do is to diligently work away on each and every violation that it discovers.
And you might have gotten yourself into such a situation because you realized well into the project that you forgot an important ruleset, or when you’re merging codebases.
Sniffs are Opinions
One important thing that we tend to forget: often we are in a position where we can actually make the decisions which rules to follow and which not. A ruleset prepared by someone else might contain things we disagree with. Thus phpcs contains the ability to exclude sniffs via its phpcs.xml configuration file, and I think it’s ok to make use of that. But also, I think that phpcs could be more helpful to you in doing so.
When you encounter such an overly eager ruleset, you might complain that the ruleset itself should be updated. But maybe you are not in a position to fight that battle, so you can just find a solution for yourself.
phpcbf can help fix things
The PHP Code Sniffer project comes with two main executables: phpcs
, and phpcbf
. While phpcs
complains, phpcbf
tries to help you. Sniffs can provide auto-fixers for violations they encounter and phpcbf
applies them for you. But oftentimes it seems like there should be an auto-fixer but in the end, you have to fix it yourself.
A side-note here: maintaining the phpcs project is certainly not a piece of cake since it’s a project heavily exposed to opinion. Many things have to be considered, and it looks like there has been agreement that an auto-fixer can only be added if there is a single and clear solution to the problem.
Thus, there are a number of sniffs where the resolution of a violation needs a user decision. This decision could be complex, like you need to rearrange code, or add something that you forgot (like a type for a parameter). But I believe there are also decisions to make where it’s a matter of choosing between several options.
User Decision Required
One example of this is Squiz.Commenting.InlineComment.InvalidEndChar
: “Inline comments must end in full-stops, exclamation marks, or question marks.” Let’s not argue about its validity but about the reason it cannot be auto-fixed: There are multiple valid end chars and an auto-fixer cannot know which one is the right one here.
So, what happens if we make phpcbf
interactive, so that it can ask the user to select one of the potential fixes, and then just goes ahead and makes them?
Or, if you realize that you disagree with this sniff: what if phpcbf
will just quickly add an <exclude ref="Sniff.Name"/>
to your phpcs.xml
? Or it could help you just disable the rule for the file? Or just ignore this line?
Many IDEs actually already have some of these ways of dealing with violations, either natively or through an extension. Bringing them to phpcbf itself would make this available to a wider audience and solutions could be baked in to sniffs.
Interactive Mode
All of these thoughts culminate in this idea to make phpcbf
interactive. I didn’t know until recently that phpcs has an interactive mode, too. But all it does is to process a file and pauses to allow the user to edit the file, and then to process it again. It assists you in checking, but not in fixing.
So, here is how this could work. Let’s assume this test.php
:
<?php
//helo
class test
{
echo 'hello';
}
This file has some problems as phpcs
tells you (the selection of Sniffs here is a bit arbitrary but to show the variety of options we’ll face):
\e[1mFILE: test.php\e[0m
----------------------------------------------------------------------------------------------
\e[1mFOUND 6 ERRORS AFFECTING 4 LINES\e[0m
----------------------------------------------------------------------------------------------
1 | \e[31mERROR\e[0m | [ ] You must use "/**" style comments for a file comment
3 | \e[31mERROR\e[0m | [x] No space found before comment text; expected "// helo" but found "//helo"
3 | \e[31mERROR\e[0m | [ ] Inline comments must start with a capital letter
3 | \e[31mERROR\e[0m | [ ] Inline comments must end in full-stops, exclamation marks, or question marks
4 | \e[31mERROR\e[0m | [ ] Test name must begin with a capital letter
6 | \e[31mERROR\e[0m | [x] Line indented incorrectly; expected at least 4 spaces, found 0
----------------------------------------------------------------------------------------------
\e[1mPHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY\e[0m
----------------------------------------------------------------------------------------------
As you can see, some violations can be fixed, most cannot. The reason for those is that there is no auto-fixer implemented for them, and often the reason is that there is no one-and-only-way to fix this.
So let’s see how interactive mode would work here. In this first case I’d like to show a situation where there is no auto-fixer available. These would be the options:
[1mPHPCBF INTERACTIVE MODE - test.php[0m
[33mERRORS[0m at line 1, column 6:
You must use "/**" style comments for a file comment
Sniff: PEAR.Commenting.FileComment.WrongStyle
[31m(Not auto-fixable)[0m
[33m 1> <?php[0m
[90m 2 [0m
[90m 3 //helo[0m
Choose an action:
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: edit):
Now let’s look at a case where an auto-fixer is available. In non-interactive mode, phpcbf
would just fix it, but here you have the option to skip it, too. It would be nice if it even could preview the auto-fix:
[33mERRORS[0m at line 6, column 1:
Line indented incorrectly; expected at least 4 spaces, found 0
Sniff: Generic.WhiteSpace.ScopeIndent.Incorrect
[32m(Auto-fixable)[0m
[90m 4 class test[0m
[90m 5 {[0m
[31m 6- echo 'hello';[0m
[32m 6+ echo 'hello';[0m
[90m 7 }[0m
[90m 8 [0m
Choose an action:
[f] Fix automatically
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: auto-fix):
Now let’s look come back to our example from above where it seems like an auto-fixer could exist but it does not because there are multiple ways to fix it. For this one, we’d have an interactive fixer that gives the user the choice how to fix it:
[33mERRORS[0m at line 3, column 1:
Inline comments must end in full-stops, exclamation marks, or question marks
Sniff: Squiz.Commenting.InlineComment.InvalidEndChar
[36m(Interactive fixes available)[0m
[1mInteractive Fix Options:[0m
[1] Append a .
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[32m 3+ // Helo.[0m
[90m 4 class test[0m
[90m 5 {[0m
[2] Append a !
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[32m 3+ // Helo![0m
[90m 4 class test[0m
[90m 5 {[0m
[3] Append a ?
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[32m 3+ // Helo?[0m
[90m 4 class test[0m
[90m 5 {[0m
[4] Remove comment
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[90m 4 class test[0m
[90m 5 {[0m
Choose an action:
[1] Apply: Append a .
[2] Apply: Append a !
[3] Apply: Append a ?
[4] Apply: Remove comment
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: 1 "Append a ."):
One could argue that you can do all of this today but I think with such an interactive mode we can relieve much of the pain it causes to get a code base to comply by streamlining what you need to do already today anyway: go through each violation and try to fix it.
Some of the choices I haven’t described yet:
i
would just append a// phpcs:ignore Sniff.Name
to the line.d
would add a// phpcs:disable Sniff.Name
at the beginning of the file.p
would add an<exclude ref="Sniff.Name"/>
to yourphpcs.xml
.e
would open an editor at the line of the code
In summary, this is a proposal to improve the user experience. I think this could transform an experience where phpcs
is the tool that scolds you, to the tool that assists you in unifying a codebase to certain standards.
Finally, expand this to see the whole run-down of an interactive phpcbf -a
session.
❯ bat test.php
[38;5;246m───────┬────────────────────────────────────────────────────────────────────────[0m
[38;5;246m│ [0mFile: [1mtest.php[0m
[38;5;246m───────┼────────────────────────────────────────────────────────────────────────[0m
[38;5;246m 1[0m [38;5;246m│[0m [38;5;231m<?php[0m
[38;5;246m 2[0m [38;5;246m│[0m
[38;5;246m 3[0m [38;5;246m│[0m [38;5;242m//[0m[38;5;242mhelo[0m
[38;5;246m 4[0m [38;5;246m│[0m [38;5;149mclass[0m[38;5;231m [0m[4;38;5;81mtest[0m
[38;5;246m 5[0m [38;5;246m│[0m [38;5;231m{[0m
[38;5;246m 6[0m [38;5;246m│[0m [38;5;167mecho[0m[38;5;231m [0m[38;5;186m'[0m[38;5;186mhello[0m[38;5;186m'[0m[38;5;231m;[0m
[38;5;246m 7[0m [38;5;246m│[0m [38;5;231m}[0m
[38;5;246m───────┴────────────────────────────────────────────────────────────────────────[0m
❯ ./bin/phpcs test.php
[1mFILE: test.php[0m
----------------------------------------------------------------------------------------------
[1mFOUND 6 ERRORS AFFECTING 4 LINES[0m
----------------------------------------------------------------------------------------------
1 | [31mERROR[0m | [ ] You must use "/**" style comments for a file comment
3 | [31mERROR[0m | [x] No space found before comment text; expected "// helo" but found "//helo"
3 | [31mERROR[0m | [ ] Inline comments must start with a capital letter
3 | [31mERROR[0m | [ ] Inline comments must end in full-stops, exclamation marks, or question marks
4 | [31mERROR[0m | [ ] Test name must begin with a capital letter
6 | [31mERROR[0m | [x] Line indented incorrectly; expected at least 4 spaces, found 0
----------------------------------------------------------------------------------------------
[1mPHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY[0m
----------------------------------------------------------------------------------------------
❯ ./bin/phpcbf -a test.php
[1mPHPCBF INTERACTIVE MODE - test.php[0m
[33mERRORS[0m at line 1, column 6:
You must use "/**" style comments for a file comment
Sniff: PEAR.Commenting.FileComment.WrongStyle
[31m(Not auto-fixable)[0m
[33m 1> <?php[0m
[90m 2 [0m
[90m 3 //helo[0m
Choose an action:
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: edit): i
Added phpcs:ignore comment to line 1
--------------------------------------------------------------------------------
[33mERRORS[0m at line 6, column 1:
Line indented incorrectly; expected at least 4 spaces, found 0
Sniff: Generic.WhiteSpace.ScopeIndent.Incorrect
[32m(Auto-fixable)[0m
[90m 4 class test[0m
[90m 5 {[0m
[31m 6- echo 'hello';[0m
[32m 6+ echo 'hello';[0m
[90m 7 }[0m
[90m 8 [0m
Choose an action:
[f] Fix automatically
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: auto-fix):
Auto-fixing...
[32mFixed![0m
--------------------------------------------------------------------------------
[33mERRORS[0m at line 3, column 1:
No space found before comment text; expected "// helo" but found "//helo"
Sniff: Squiz.Commenting.InlineComment.NoSpaceBefore
[32m(Auto-fixable)[0m
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- //helo[0m
[32m 3+ // helo[0m
[90m 4 class test[0m
[90m 5 {[0m
Choose an action:
[f] Fix automatically
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: auto-fix):
Auto-fixing...
[32mFixed![0m
--------------------------------------------------------------------------------
[33mERRORS[0m at line 3, column 1:
Inline comments must start with a capital letter
Sniff: Squiz.Commenting.InlineComment.NotCapital
[36m(Interactive fixes available)[0m
[1mInteractive Fix Options:[0m
[1] Suggestion
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // helo[0m
[32m 3+ // Helo[0m
[90m 4 class test[0m
[90m 5 {[0m
Choose an action:
[1] Apply: Suggestion
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: 1 "Capitalize first letter"):
Applying option 1...
[32mFixed![0m
--------------------------------------------------------------------------------
[33mERRORS[0m at line 3, column 1:
Inline comments must end in full-stops, exclamation marks, or question marks
Sniff: Squiz.Commenting.InlineComment.InvalidEndChar
[36m(Interactive fixes available)[0m
[1mInteractive Fix Options:[0m
[1] Append a .
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[32m 3+ // Helo.[0m
[90m 4 class test[0m
[90m 5 {[0m
[2] Append a !
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[32m 3+ // Helo![0m
[90m 4 class test[0m
[90m 5 {[0m
[3] Append a ?
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[32m 3+ // Helo?[0m
[90m 4 class test[0m
[90m 5 {[0m
[4] Remove comment
[90m 1 <?php[0m
[90m 2 [0m
[31m 3- // Helo[0m
[90m 4 class test[0m
[90m 5 {[0m
Choose an action:
[1] Apply: Append a .
[2] Apply: Append a !
[3] Apply: Append a ?
[4] Apply: Remove comment
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: 1 "Append a ."): 2
Fixing with option 2...
[32mFixed![0m
--------------------------------------------------------------------------------
[33mERRORS[0m at line 4, column 7:
Test name must begin with a capital letter
Sniff: PEAR.NamingConventions.ValidClassName.StartWithCapital
[36m(Interactive fixes available)[0m
[1mInteractive Fix Options:[0m
[1] Suggestion
[90m 2 [0m
[90m 3 // Helo![0m
[31m 4- class test[0m
[32m 4+ class Test[0m
[90m 5 {[0m
[90m 6 echo 'hello';[0m
Choose an action:
[1] Apply: Suggestion
[i] Add a phpcs:ignore to this line (phpcs:ignore)
[d] Disable this sniff for this file (phpcs:disable)
[p] Project-wide: exclude this sniff in phpcs.xml
[e] Edit the file manually
[s] Skip this violation
[q] Quit
Action (default: 1 "Capitalize first letter"):
Applying option 1...
[32mFixed![0m
--------------------------------------------------------------------------------
[1mPHPCBF RESULT SUMMARY[0m
----------------------------------------------------------------------
[1mFILE FIXED REMAINING[0m
----------------------------------------------------------------------
test.php 5 0
----------------------------------------------------------------------
[1mA TOTAL OF 5 ERRORS WERE FIXED IN 1 FILE[0m
----------------------------------------------------------------------
❯ bat test.php
[38;5;246m───────┬────────────────────────────────────────────────────────────────────────[0m
[38;5;246m│ [0mFile: [1mtest.php[0m
[38;5;246m───────┼────────────────────────────────────────────────────────────────────────[0m
[38;5;246m 1[0m [38;5;246m│[0m [38;5;231m<?php[0m[38;5;231m [0m[38;5;242m//[0m[38;5;242m phpcs:ignore PEAR.Commenting.FileComment.WrongStyle[0m
[38;5;246m 2[0m [38;5;246m│[0m
[38;5;246m 3[0m [38;5;246m│[0m [38;5;242m//[0m[38;5;242m Helo![0m
[38;5;246m 4[0m [38;5;246m│[0m [38;5;149mclass[0m[38;5;231m [0m[4;38;5;81mTest[0m
[38;5;246m 5[0m [38;5;246m│[0m [38;5;231m{[0m
[38;5;246m 6[0m [38;5;246m│[0m [38;5;231m [0m[38;5;167mecho[0m[38;5;231m [0m[38;5;186m'[0m[38;5;186mhello[0m[38;5;186m'[0m[38;5;231m;[0m
[38;5;246m 7[0m [38;5;246m│[0m [38;5;231m}[0m
[38;5;246m───────┴────────────────────────────────────────────────────────────────────────[0m
You can find the issue I created about this in the PHP CodeSniffer repo, and my implementation on Github. This is not yet a PR because they prefer to have a discussion it first but I worked on an implementation already so that you can experience how it feels to use it and don’t just have to imagine it. Find the instructions to do so on the bottom of the issue.
PS: to make the terminal output look well in this post, I have made this syntaxhighlighter-ansi plugin for the SyntaxHighlighter Evolved plugin for WordPress that will interpret the ANSI escape sequences from the terminal and convert it to colorful output like you can see above.