Analyze your code with PHP_CodeSniffer

7 min read
265

You can ask me why we should use coding standards? Shortly, to make more strict rules for your code and control the code quality in your team. A lot of similar things you can do differently, using various syntax. I described the basic knowledge about PHP coding standards, WPCS, with simple examples of my own coding standard earlier.

This article will describe how to create coding standards as a separate package for the Composer with your own Sniffs and tests. Come to a new level for teamwork.

Coding Standards’ Structure

For creating a new coding standards package, let’s start with the composer.json:

{
	"name": "wppunk/phpcs-template",
	"description": "Template for PHPCS.",
	"type": "phpcodesniffer-standard",
	"license": "MIT",
	"authors": [
		{
			"name": "WPPunk",
			"email": "i@wp-punk.com"
		}
	],
	"require": {
		"squizlabs/php_codesniffer": "^3.5"
	},
	"require-dev": {
		"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
	}
}

Special attention to the type line, using the phpcodesniffer-standard value we make available this package for the squizlabs/php_codesniffer package.

The next point is install all needed dependencies:

composer install

Let’s to prepare a special structure:

wppunk-phpcs/
├── WPPunk/                         # → CS root directory
│   ├── Sniffs/                     # → Sniffs directory
│   ├── Tests/                      # → Tests directory
│   └── ruleset.xml                 # → Ruleset for CS.
└── composer.json

Into ruleset.xml, you could include other coding standards, disable some rules, configure default Rules. Interesting rules you can find in a previous article about CS. Let’s fill the rulset.xml file:

<?xml version="1.0"?>
<ruleset name="WPPunk">
	<description>The code standard by WPPunk.</description>
</ruleset>

Register your coding standards for PHPCS CLI

That’s easy, just use the next commands:

vendor/bin/phpcs --config-set installed_paths WPPunk

To make sure that your CS was added, you can run the next command and see all registered and available to usage coding standards:

vendor/bin/phpcs -i

When your coding standard was registered, you can check the file or folder with your CS with the next command:

vendor/bin/phpcs --standard=WPPunk path/to/folder/or/file

Create Custom PHP Sniff

I’m going to write a sniff that will check the empty line before the return statement.

How to create a custom sniff? Create a PHP file in the Sniffs directory that has the suffix Sniff. Let’s create Sniffs/Formatting/EmptyLineBeforeReturnSniff.php:

<?php

namespace WPPunk\Sniffs\Formatting;

use PHP_CodeSniffer\Sniffs\Sniff;

class EmptyLineBeforeReturnSniff implements Sniff {
	public function register() {	
		// ...
	}
	public function process( File $phpcsFile, $stackPtr ) {
		// ...
	}
}

So, we need to implement only two methods.

The register method should return an array of PHP elements that should check the current sniff. That most critical part for the performance of your custom sniff. Take a very responsible approach to this simple method.

Here the important term is token. A token is one PHP element that has its own content, type, position, etc. The token example could be a return statement or opened/closed brackets, class or function name, access modifiers, in other words, any element from PHP syntax. Where can we find the names of PHP elements? Into …/vendor/squizlabs/php_codesniffer/src/Util/Tokens.php. If it’s your first sniff that you should dive into this class. This a helper that describes all available PHP elements and has many typical groups of elements.

The process method makes magic and describes all logic for your checks. More detail I’ll describe a bit later:

<?php

namespace WPPunk\Sniffs\Formatting;

use WPPunk\Sniffs\BaseSniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHP_CodeSniffer\Sniffs\Sniff;

class EmptyLineBeforeReturnSniff implements Sniff {

	public function register() {
		return [
			T_RETURN,
		];
	}

	public function process( File $phpcsFile, $stackPtr ) {
		$tokens   = $phpcsFile->getTokens();
		$previous = $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true );

		if ( $tokens[ $stackPtr ]['line'] - $tokens[ $previous ]['line'] < 2 ) {
			$phpcsFile->addError(
				sprintf(
					'Add empty line before return statement in %d line.',
					$tokens[ $stackPtr ]['line'] - 1
				),
				$stackPtr,
				'AddEmptyLineBeforeReturnStatement'
			);
		}
	}
}

In the process method, we have the PHPCS file object and cursor to the position of an element. The cursor equals position for the element that type equal to the one from the register method list.

Also, we can get all tokens in this file with the $phpcsFile->getTokens() method. Example of the token:

Array
(
    [code] => 345
    [type] => T_RETURN
    [content] => return
    [line] => 11
    [column] => 2
    [length] => 6
    [level] => 1
    [conditions] => Array
        (
            [31] => 342
        )

)

Also, we have beneficial methods for navigation between tokens. With the findNext method possible to find the next token and with findPrevious — previous. The first argument of these methods is any token or list of tokens.

The $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); code means that I’m going to find previous not empty (spaces, blanks, etc.) token.

When we have all the needed tokens, we can check the number of lines between them and show an error if an empty line doesn’t exist.

And the last but not least, the ability to add errors and warning to the file with $phpcsFile->addError or $phpcsFile->addWarning methods.

Fixable warnings/errors

The PHP_CodeSniff library also has the phpcbf command that allows to automatically fix errors if it is possible. But for this great feature, your sniffs should be ready.

Let’s upgrade our sniff to be ready to automatically fixes:

<?php
// ...
class EmptyLineBeforeReturnSniff implements Sniff {
	// ...
	public function process( File $phpcsFile, $stackPtr ) {
		// ...
		if ( $tokens[ $stackPtr ]['line'] - $tokens[ $previous ]['line'] < 2 ) {
			$fix = $phpcsFile->addFixableError(
				sprintf(
					'Add empty line before return statement in %d line.',
					$tokens[ $stackPtr ]['line'] - 1
				),
				$stackPtr,
				'AddEmptyLineBeforeReturnStatement'
			);

			if ( $fix === true ) {
				$phpcsFile->fixer->addNewline( $previous );
			}
		}
	}
}

What was changed? The addFixableError or addFixableWarning instead of addError or addWarning methods. The result of fixable methods will return true when sniffs were run via phpcbf.

The $phpcsFile->fixer is the PHP_CodeSniffer\Fixer object that could modify a validated file. In our example, we just added a new line using it.

Sniffs’ Unit Tests

A specialty of tests for PHP sniffs is a test file that we need to check inside a test class. We’ll add code to the test file that covers as much as possible all test cases to which the tested sniff reacts or does not respond.

Tests Environment

If is your first tests then better to start with the The practice of WordPress unit testing article. Because I’ll describe shortly how it works.

First of all, let’s create a PHPUnit config in the /Tests/phpunit.xml file:

<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 bootstrap="./bootstrap.php"
		 colors="true"
		xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
>
	<testsuites>
		<testsuite name="WPPunk-tests">
			<directory suffix=".php">./Tests/</directory>
		</testsuite>
	</testsuites>
</phpunit>

Then, let’s add the Sniffs to autoload and add the script to simplify the tests run:

{
	// ...
	"autoload-dev": {
		"psr-4": {
			"WPPunk\\": "WPPunk/"
		}
	},
	"scripts": {
		"unit": "vendor/bin/phpunit --configuration ./WPPunk/Tests/phpunit.xml"
	}
}

The next step is creating the /Tests/bootstrap.php file:

<?php

define( 'WPPUNK_SNIFFS_PATH', realpath( __DIR__ . '/../Sniffs' ) . '/' );
define( 'WPPUNK_TESTED_FILES_PATH', __DIR__ . '/TestedFiles/' );

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../vendor/squizlabs/php_codesniffer/tests/bootstrap.php';

I’ve defined two constants: the sniffs directory and the directory for tested files. Then included vendor autoload and bootstrap file from the PHP_CodeSniffer library that describes a lot of important constants.

Create TestCase class

To simplify tests, I suggest to create your own TestCase in the /Tests/TestCase.php file:

<?php

namespace WPPunk\Tests;

use Exception;
use ReflectionClass;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\LocalFile;

class TestCase extends \PHPUnit\Framework\TestCase {
	protected function process( Sniff $className ) {
		$class     = new ReflectionClass( $className );
		$localFile = WPPUNK_TESTED_FILES_PATH . str_replace( 'Sniff.php', '.php', str_replace( WPPUNK_SNIFFS_PATH, '', $class->getFileName() ) );
		if ( ! file_exists( $localFile ) ) {
			throw new Exception(
				sprintf(
					'The %s file doesn\'t exist',
					$localFile
				)
			);
		}

		$config  = new Config();
		$ruleset = new Ruleset( $config );
		$ruleset->registerSniffs( [ $class->getFileName() ], [], [] );
		$ruleset->populateTokenListeners();
		$phpcsFile = new LocalFile(
			$localFile,
			$ruleset,
			$config
		);
		$phpcsFile->process();

		return $phpcsFile;
	}

	protected function assertFileHasErrors( LocalFile $phpcsFile, string $name, array $lines ) {
		$errors = [];
		foreach ( $phpcsFile->getErrors() as $line => $groupErrors ) {
			foreach ( $groupErrors as $sniffErrors ) {
				foreach ( $sniffErrors as $error ) {
					if ( preg_match( '/\.' . $name . '$/', $error['source'] ) ) {
						$errors[] = $line;
					}
				}
			}
		}
		$this->assertEquals( $errors, $lines );
	}
}

The process method is a magic, that register sniff passed throw method’s argument, processes the test file and return the PHP_CodeSniffer\Files\LocalFile object.

The assertFileHasErrors method is assert that compare found errors into LocalFiles and passed list of lines. Yep, I know that it looks complicated but this help to us create simple a short tests.

Tested file

The next step is to create the tested file with some code in the .../WPPunk/Tests/TestedFiles/Formatting/EmptyLineBeforeReturn.php file. You can push to this file any code that you want. Just imagine that is your projects and you want to describe as many as possible cases related to your sniff:

<?php

function valid_function() {

	return true;
}

function valid_function_2() {

	$var = true;

	return $var;
}

function valid_function_3() {

	if ( true ) {
		$krya = true;

		return true;
	}

	while ( true ) {
		echo 'null';

		return true;
	}

	return true;
}

class Valid {

	public function valid_function_4() {

		return [
			'amount'   => $amount,
			'currency' => strtolower( get_option( 'currency', 'USD' ) ),
		];
	}
}

$p = array_map(
	static function( $slug ) {

		return "{$slug}/{$slug}.php";
	},
	wp_list_pluck( $addons_data, 'slug' )
);

function invalid_function() {
	return true;
}

function invalid_function_2() {

	$var = true;
	return $var;
}

function invalid_function_3() {

	if ( true ) {
		$krya = true;
		return true;
	}

	while ( true ) {
		echo 'null';
		return true;
	}
	return true;
}

The testing

And the last but not least, our test case in the .../WPPunk/Tests/Tests/Formatting/EmptyLineBeforeReturnSniffTest.php file:

<?php

namespace WPPunk\Tests\Formatting;

use WPPunk\Tests\TestCase;
use WPPunk\Sniffs\Formatting\EmptyLineBeforeReturnSniff;

class EmptyLineBeforeReturnSniffTest extends TestCase {
	public function test_process() {
		$phpcsFile = $this->process( new EmptyLineBeforeReturnSniff() );
		$this->assertFileHasErrors( $phpcsFile, 'AddEmptyLineBeforeReturnStatement', [ 52, 58, 65, 70, 72 ] );
	}
}

We’ve asserted that file has AddEmptyLineBeforeReturnStatement errors in the 52, 58, 65, 70, and 72 lines. Of course, all these lines need to check manually to be sure that your sniff works as well.

I hope you’re still here because it’s all with code, and we a ready to run them:

composer unit
Create your own coding standards with PHP_CodeSniffer to control your project's code quality and make daily work easier. - WP Punk

Overall

In general, It was an interesting experience that reminder to me of my student time when you need to write a simple algorithm with a lot of magic unknowns.

In product development, the crucial part is how easy to read your code. As a perfectionist, I like the strict structure of the document, that easy to read. Sometimes really hard to convince yourself and always be strict about some coding standards. In teamwork, it’s impossible without special tools.

In the example above, I showed an elementary example, but what about the big problems you could prevent using CS. If we had a special sniff that checks public properties, it was a really solid fundament for future refactorings.

I really like quality tools that allow keeping the mind clear, because every day the business logic becomes more complicated and you should be focus on the most critical part during the daily work. But don’t forget the balance between real value for business and “I want to try a new feature for your money”. Have a great day.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe to news. I promise not to spam :)
Follow me, don't be shy