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 .