PHP CodeSniffer and CodeIgniter

I recently found out that Thomas Ernest have created a CodeIgniter standard for CodeSniffer. He mentioned on the CI formus that it’s not perfect and does spit out a few warnings and errors for things that are really OK in the CI style guide. False positives.

This is the slightly modified welcome.php controller file I wanted to scan. According to my understanding of the CodeIgniter style guide, it should pass with flying colors:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
* File: welcome.php
* 
* PHP version 5
*
* @category Frontend
* @package  Citest
* @author   Erik Torsner <erik@torgesta.com>
* @license  torgesta.com http://www.torgesta.com/  
* @link     http://www.torgesta.com/  
*/

/**
 * Class Welcome
 *
 * @category Frontend
 * @package  Citest
 * @author   Erik Torsner <erik@torgesta.com>
 * @license  torgesta.com http://www.torgesta.com/
 * @link     http://www.torgesta.com/  
 **/
class Welcome extends CI_Controller {
	/**
	 * Index Page for this controller.
	 *
	 * Maps to the following URL
	 * 		http://example.com/index.php/welcome
	 *	- or -  
	 * 		http://example.com/index.php/welcome/index
	 *	- or -
	 * Since this controller is set as the default controller in 
	 * config/routes.php, it's displayed at http://example.com/
	 *
	 * So any other public methods not prefixed with an underscore will
	 * map to /index.php/welcome/<method_name>
	 *
	 * @see http://codeigniter.com/user_guide/general/urls.html
	 * @return void
	 */
	public function index()
	{
		$this->load->view('welcome_message');
	}
}

/* End of file welcome.php */
/* Location: ./controllers/welcome.php */

See? File doc comment, class doc comment, function doc comment, no PHP end tag but the two standard comments instead. Beautiful. But scanning with CodeSniffer, I’d get something like this:

$ phpcs --standard=CodeIgniter welcome.php 

FILE: /home/foo/src/citest/application/controllers/welcome.php
--------------------------------------------------------------------------------
FOUND 3 ERROR(S) AFFECTING 3 LINE(S)
--------------------------------------------------------------------------------
  1 | ERROR | Missing file doc comment
 47 | ERROR | Multi lines comments are not allowed; use "// Comment" DocBlock
    |       | comments instead
--------------------------------------------------------------------------------

Time: 0 seconds, Memory: 3.25Mb

First problem, CodeSniffer doesn’t see the file doc comment at all. Turns out the CodeSniffer looks for the first non white space token after the PHP open tag. If it’s a valid file doc comment, all is good. In CodeIgniter, the standard behavior is to add a check for “BASEPATH” right at the top and it gets in the way of finding the existing file doc comment.

Second problem. CodeIgniter style guide first say that all code comments should be one line comments. A few examples:

<?php

// One line comment using '//'. Fine with CodeIgniter 
$foo = "bar";

/*One line comment using '/*', not OK */
$fuu = "bar";

// Multi line comments, you should still use '//'
// This is the next comment line
$fuubar = NULL:

/* Multi line comments, this way is not OK
*This is the next comment line */
$fuubar = 42:

// But wait! at the end of the file. CI expects this:
// Two single line comments using '/* ... */. 
/* End of file welcome.php */
/* Location: ./controllers/welcome.php */

This is simply not consistent. To get clean scans using CodeSniffer, you have two options. Rewrite all end-of-file comments to use ‘//’ rather than ‘/*’ or fix CodeSniffer. Even if consistency is a good thing, this is a very well established style in CodeIgniter so I was inclined to fix CodeSniffer rather than fight.

The CodeSniffer standard is installed (on Ubuntu) in

/usr/share/php/PHP/CodeSniffer/Standards/CodeIgniter

 

To fix the comments, I made the following change to

/usr/share/php/PHP/CodeSniffer/Standards/CodeIgniter/Sniffs/Commenting/InlineCommentSniff.php

 

    private function _checkCommentStyle(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        $cmt = $tokens[$stackPtr]['content'];
        if($this->beginsWith($cmt, '/* End of file')) return TRUE;
        if($this->beginsWith($cmt, '/* Location:')) return TRUE;
        ...
        ...

That it, to explicitly allow the two CodeIgniter specific comments. The second change was bigger. The CodeIgniter standard for CodeSniffer reuses PEAR’s check for file doc comments. Rather than modifying what is a built in CodeSniffer standard, I copied the correct file to the CodeIgniter standard and made modifications there. So I created the file

/usr/share/php/PHP/CodeSniffer/Standards/CodeIgniter/Sniffs/Commenting/FileCommentsSniff.php

 

as a copy of the corresponding file from the PEAR standard. Function process was modified:

    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
    {
        $this->currentFile = $phpcsFile;

        // We are only interested if this is the first open tag.
        if ($stackPtr !== 0) {
            if ($phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)) !== false) {
                return;
            }
        }

        $tokens = $phpcsFile->getTokens();

        // Allow CodeIgniter std "if ( ! defined('BASEPATH')).... on the first line
        // keep iterating until we find a token on line 2...
        $startLine = $tokens[$stackPtr]['line'];
        $lastToken = $stackPtr;
        while($tokens[$lastToken+1]['line'] == $startLine) {
          $lastToken++;
        }

So, a simple loop that simply ignores whatever tokens that  is on the same line as the PHP open tag. Simple and it works. The last change I made was in

/usr/share/php/PHP/CodeSniffer/Standards/CodeIgniter/ruleset.xml

 

where the rule that refers to the FileCommentSniff file is modified so that it looks at the local copy rather than the one from the PEAR standard. Just change the line:

<rule ref="PEAR.Commenting.FileComment">

Into

<rule ref="CodeIgniter.Commenting.FileComment">

That’s it. A new test scan reveals that the perfectly decent looking welcome.php doesn’t generate any further errors:

$ phpcs --standard=CodeIgniter welcome.php 
Time: 0 seconds, Memory: 3.25Mb

Now, go to github and get the repo.