Search code examples
phpeclipsephpcodesnifferphpmd

PHPMD (PHP Mess Detector) integration in Eclipse Mars


Before the release of Mars it was possible to install PHPMD support in Eclipse, albeit with some caveats and difficulties.

Now support from PTI seems to have been removed completely, even if development of PHPMD hasn't stopped and PHPMD does offer some features that other tools do not: for example, detect unused variables.

For this last feature I've found a not too recent CodeSniffer plugin that does the trick. There are also some sniffs that should do the work but they don't seem to work for me, or not in all cases at the very least: I have a project in need of refactoring for which I have 11 warnings from CodeSniffer and 2524 from PHPMD.

I think I have a simple and inelegant way of shoehorning PHPMD back in, but before doing that, I wondered whether anyone has this specific same problem/need, and whether s/he managed to solve it somehow.


Solution

  • Okay, so here goes.

    I do have a PHPMD binary that works from the command line. What I planned to do is to inject its output into that of the CodeSniffer plugin, to "enrich" the latter with PHPMD messages.

    To this effect, I mangled phpcs.php which comes in my plugins/org.ppsrc.eclipse.pti.tools.codesniffer_.../php/tools directory.

    (Since another problem I have with CodeSniffer is that it will often re-scan files it ought to know about, I decided to give CodeSniffer a memory.

    First thing, I extract the last argument to the invocation, which is the file being analyzed (lines marked by +++ are my additions/changes):

    // Optionally use PHP_Timer to print time/memory stats for the run.
    // Note that the reports are the ones who actually print the data
    // as they decide if it is ok to print this data to screen.
    @include_once 'PHP/Timer.php';
    if (class_exists('PHP_Timer', false) === true) {
        PHP_Timer::start();
    }
    
    if (is_file(dirname(__FILE__).'/../CodeSniffer/CLI.php') === true) {
        include_once dirname(__FILE__).'/../CodeSniffer/CLI.php';
    } else {
        include_once 'PHP/CodeSniffer/CLI.php';
    }
    
    +++ $lastArgument   = array_pop($_SERVER['argv']);
    

    Then I add some flags that CS doesn't seem to pass, and that I need, such as ignoring some directories:

    +++ $_SERVER['argv'][]  = '--ignore=tests,vendor,cache';
    +++ $_SERVER['argv'][]  = $lastArgument;
    

    Then CS invocation proceeds, but now I save its results to a buffer instead of sending them straight to Eclipse.

    $phpcs = new PHP_CodeSniffer_CLI();
    $phpcs->checkRequirements();
    +++ ob_start();
    $numErrors = $phpcs->process();
    
    +++ $dom    = new DOMDocument();
    +++ $dom->loadXML(ob_get_clean());
    
    +++ $cs     = $dom->getElementsByTagName('phpcs')->item(0);
    +++ $xpath  = new DOMXPath($dom);
    

    Now I have PHPCS output ready as XML.

    All that remains is to invoke PHPMD using its own syntax.

    // Add PHPMD.
    $mdCmd  = "C:/PHP/composer/vendor/phpmd/phpmd/src/bin/phpmd \"{$lastArgument}\" xml \"C:/Program Files/eclipse/plugins/org.phpsrc.eclipse.pti.library.pear_1.2.2.R20120127000000/php/library/PEAR/data/PHP_PMD/resources/rulesets/codesize.xml,C:/Program Files/eclipse/plugins/org.phpsrc.eclipse.pti.library.pear_1.2.2.R20120127000000/php/library/PEAR/data/PHP_PMD/resources/rulesets/naming.xml,C:/Program Files/eclipse/plugins/org.phpsrc.eclipse.pti.library.pear_1.2.2.R20120127000000/php/library/PEAR/data/PHP_PMD/resources/rulesets/unusedcode.xml\"";
    

    ...and load it into another XML:

        fprintf(STDERR, $mdCmd . "\n");
        $dompmd = new DOMDocument();
        $dompmd->loadXML($mdxml = shell_exec($mdCmd));
    

    Now I get all errors out of the PMD object, and add it to the CS one:

        $files  = $dompmd->getElementsByTagName('file');
        foreach ($files as $file) {
            $name   = $file->getAttribute('name');
            $list   = $xpath->query("//file[@name=\"{$name}\"]");
            if (null === $list) {
                continue;
            }
            $csFile = $list->item(0);
            if (null === $csFile) {
                // No errors from CS.
                $csFile = $dom->createElement('file');
                $csFile->setAttribute('name', $name);
                $csFile->setAttribute('errors', 0);
                $csFile->setAttribute('warnings', 0);
                $csFile->setAttribute('fixable', 0);
                $cs->appendChild($csFile);
            }
            $errs   = 0;
            foreach ($file->childNodes as $violation) {
                if ($violation instanceof \DOMText) {
                    continue;
                }
                $error  = $dom->createElement('warning', trim($violation->nodeValue));
    
    
                $error->setAttribute('line', $violation->getAttribute('beginline'));
                $error->setAttribute('column', 1);
                $error->setAttribute('source', 'PHPMD.' . $violation->getAttribute('ruleset'));
                $error->setAttribute('severity', $violation->getAttribute('priority'));
                $error->setAttribute('fixable', 0);
    
                $csFile->appendChild($error);
    
                $errs++;
            }
            $csFile->getAttributeNode('errors')->value += $errs;
        }
    

    Finally, send the data back to Eclipse:

    print $dom->saveXML();
    exit($numErrors ? 1: 0);
    

    Caching CodeSniffer (and PHPMD too)

    Since another problem I have with CodeSniffer is that it will often re-scan files it ought to know about, I decided to give CodeSniffer a memory. It's pretty easy: I can store a temporary file with the saved XML and a name built up of the MD5 of the original file name and its contents:

    /tmp/68ce1959ef67bcc94e05084e2e20462a.76e55e72f32156a20a183de82fe0b3b6.xml
    

    So when PHPCS is asked to analyze /path/to/file/so-and-so.php, it will:

    • create the MD5 of the filename.
    • create the MD5 of its contents.
    • if /tmp/md5file.md5content.xml does not exist:
      • delete any /tmp/md5file.*.xml there are: they're obsolete.
      • run code as above.
      • save results into /tmp/md5file.md5content.xml
    • output the content of /tmp/md5file.md5content.xml .