601 lines
18 KiB
PHP
601 lines
18 KiB
PHP
<?php
|
|
/*
|
|
* $Id: PhpCodeSnifferTask.php 905 2010-10-05 16:28:03Z mrook $
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
* This software consists of voluntary contributions made by many individuals
|
|
* and is licensed under the LGPL. For more information please see
|
|
* <http://phing.info>.
|
|
*/
|
|
|
|
require_once 'phing/Task.php';
|
|
|
|
/**
|
|
* A PHP code sniffer task. Checking the style of one or more PHP source files.
|
|
*
|
|
* @author Dirk Thomas <dirk.thomas@4wdmedia.de>
|
|
* @version $Id: PhpCodeSnifferTask.php 905 2010-10-05 16:28:03Z mrook $
|
|
* @package phing.tasks.ext
|
|
*/
|
|
class PhpCodeSnifferTask extends Task {
|
|
|
|
protected $file; // the source file (from xml attribute)
|
|
protected $filesets = array(); // all fileset objects assigned to this task
|
|
|
|
// parameters for php code sniffer
|
|
protected $standard = 'Generic';
|
|
protected $sniffs = array();
|
|
protected $showWarnings = true;
|
|
protected $showSources = false;
|
|
protected $reportWidth = 80;
|
|
protected $verbosity = 0;
|
|
protected $tabWidth = 0;
|
|
protected $allowedFileExtensions = array('php');
|
|
protected $ignorePatterns = false;
|
|
protected $noSubdirectories = false;
|
|
protected $configData = array();
|
|
|
|
// parameters to customize output
|
|
protected $showSniffs = false;
|
|
protected $format = 'default';
|
|
protected $formatters = array();
|
|
|
|
/**
|
|
* Holds the type of the doc generator
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $docGenerator = '';
|
|
|
|
/**
|
|
* Holds the outfile for the documentation
|
|
*
|
|
* @var PhingFile
|
|
*/
|
|
protected $docFile = null;
|
|
|
|
private $haltonerror = false;
|
|
private $haltonwarning = false;
|
|
|
|
/**
|
|
* Load the necessary environment for running PHP_CodeSniffer.
|
|
*
|
|
* @throws BuildException
|
|
* @return void
|
|
*/
|
|
public function init()
|
|
{
|
|
/**
|
|
* Determine PHP_CodeSniffer version number
|
|
*/
|
|
preg_match('/\d\.\d\.\d/', shell_exec('phpcs --version'), $version);
|
|
|
|
if (version_compare($version[0], '1.2.2') < 0) {
|
|
throw new BuildException(
|
|
'PhpCodeSnifferTask requires PHP_CodeSniffer version >= 1.2.2',
|
|
$this->getLocation()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* File to be performed syntax check on
|
|
* @param PhingFile $file
|
|
*/
|
|
public function setFile(PhingFile $file) {
|
|
$this->file = $file;
|
|
}
|
|
|
|
/**
|
|
* Nested creator, creates a FileSet for this task
|
|
*
|
|
* @return FileSet The created fileset object
|
|
*/
|
|
function createFileSet() {
|
|
$num = array_push($this->filesets, new FileSet());
|
|
return $this->filesets[$num-1];
|
|
}
|
|
|
|
/**
|
|
* Sets the coding standard to test for
|
|
*
|
|
* @param string $standard The coding standard
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setStandard($standard)
|
|
{
|
|
if (!class_exists('PHP_CodeSniffer')) {
|
|
include_once 'PHP/CodeSniffer.php';
|
|
}
|
|
|
|
if (PHP_CodeSniffer::isInstalledStandard($standard) === false) {
|
|
// They didn't select a valid coding standard, so help them
|
|
// out by letting them know which standards are installed.
|
|
$installedStandards = PHP_CodeSniffer::getInstalledStandards();
|
|
$numStandards = count($installedStandards);
|
|
$errMsg = '';
|
|
|
|
if ($numStandards === 0) {
|
|
$errMsg = 'No coding standards are installed.';
|
|
} else {
|
|
$lastStandard = array_pop($installedStandards);
|
|
|
|
if ($numStandards === 1) {
|
|
$errMsg = 'The only coding standard installed is ' . $lastStandard;
|
|
} else {
|
|
$standardList = implode(', ', $installedStandards);
|
|
$standardList .= ' and ' . $lastStandard;
|
|
$errMsg = 'The installed coding standards are ' . $standardList;
|
|
}
|
|
}
|
|
|
|
throw new BuildException(
|
|
'ERROR: the "' . $standard . '" coding standard is not installed. ' . $errMsg,
|
|
$this->getLocation()
|
|
);
|
|
}
|
|
|
|
$this->standard = $standard;
|
|
}
|
|
|
|
/**
|
|
* Sets the sniffs which the standard should be restricted to
|
|
* @param string $sniffs
|
|
*/
|
|
public function setSniffs($sniffs)
|
|
{
|
|
$token = ' ,;';
|
|
$sniff = strtok($sniffs, $token);
|
|
while ($sniff !== false) {
|
|
$this->sniffs[] = $sniff;
|
|
$sniff = strtok($token);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the type of the doc generator
|
|
*
|
|
* @param string $generator HTML or Text
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setDocGenerator($generator)
|
|
{
|
|
$this->docGenerator = $generator;
|
|
}
|
|
|
|
/**
|
|
* Sets the outfile for the documentation
|
|
*
|
|
* @param PhingFile $file The outfile for the doc
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setDocFile(PhingFile $file)
|
|
{
|
|
$this->docFile = $file;
|
|
}
|
|
|
|
/**
|
|
* Sets the flag if warnings should be shown
|
|
* @param boolean $show
|
|
*/
|
|
public function setShowWarnings($show)
|
|
{
|
|
$this->showWarnings = StringHelper::booleanValue($show);
|
|
}
|
|
|
|
/**
|
|
* Sets the flag if sources should be shown
|
|
*
|
|
* @param boolean $show Whether to show sources or not
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setShowSources($show)
|
|
{
|
|
$this->showSources = StringHelper::booleanValue($show);
|
|
}
|
|
|
|
/**
|
|
* Sets the width of the report
|
|
*
|
|
* @param int $width How wide the screen reports should be.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function setReportWidth($width)
|
|
{
|
|
$this->reportWidth = (int) $width;
|
|
}
|
|
|
|
/**
|
|
* Sets the verbosity level
|
|
* @param int $level
|
|
*/
|
|
public function setVerbosity($level)
|
|
{
|
|
$this->verbosity = (int)$level;
|
|
}
|
|
|
|
/**
|
|
* Sets the tab width to replace tabs with spaces
|
|
* @param int $width
|
|
*/
|
|
public function setTabWidth($width)
|
|
{
|
|
$this->tabWidth = (int)$width;
|
|
}
|
|
|
|
/**
|
|
* Sets the allowed file extensions when using directories instead of specific files
|
|
* @param array $extensions
|
|
*/
|
|
public function setAllowedFileExtensions($extensions)
|
|
{
|
|
$this->allowedFileExtensions = array();
|
|
$token = ' ,;';
|
|
$ext = strtok($extensions, $token);
|
|
while ($ext !== false) {
|
|
$this->allowedFileExtensions[] = $ext;
|
|
$ext = strtok($token);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the ignore patterns to skip files when using directories instead of specific files
|
|
* @param array $extensions
|
|
*/
|
|
public function setIgnorePatterns($patterns)
|
|
{
|
|
$this->ignorePatterns = array();
|
|
$token = ' ,;';
|
|
$pattern = strtok($patterns, $token);
|
|
while ($pattern !== false) {
|
|
$this->ignorePatterns[] = $pattern;
|
|
$pattern = strtok($token);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the flag if subdirectories should be skipped
|
|
* @param boolean $subdirectories
|
|
*/
|
|
public function setNoSubdirectories($subdirectories)
|
|
{
|
|
$this->noSubdirectories = StringHelper::booleanValue($subdirectories);
|
|
}
|
|
|
|
/**
|
|
* Creates a config parameter for this task
|
|
*
|
|
* @return Parameter The created parameter
|
|
*/
|
|
public function createConfig() {
|
|
$num = array_push($this->configData, new Parameter());
|
|
return $this->configData[$num-1];
|
|
}
|
|
|
|
/**
|
|
* Sets the flag if the used sniffs should be listed
|
|
* @param boolean $show
|
|
*/
|
|
public function setShowSniffs($show)
|
|
{
|
|
$this->showSniffs = StringHelper::booleanValue($show);
|
|
}
|
|
|
|
/**
|
|
* Sets the output format
|
|
* @param string $format
|
|
*/
|
|
public function setFormat($format)
|
|
{
|
|
$this->format = $format;
|
|
}
|
|
|
|
/**
|
|
* Create object for nested formatter element.
|
|
* @return CodeSniffer_FormatterElement
|
|
*/
|
|
public function createFormatter () {
|
|
$num = array_push($this->formatters,
|
|
new PhpCodeSnifferTask_FormatterElement());
|
|
return $this->formatters[$num-1];
|
|
}
|
|
|
|
/**
|
|
* Sets the haltonerror flag
|
|
* @param boolean $value
|
|
*/
|
|
function setHaltonerror($value)
|
|
{
|
|
$this->haltonerror = $value;
|
|
}
|
|
|
|
/**
|
|
* Sets the haltonwarning flag
|
|
* @param boolean $value
|
|
*/
|
|
function setHaltonwarning($value)
|
|
{
|
|
$this->haltonwarning = $value;
|
|
}
|
|
|
|
/**
|
|
* Executes PHP code sniffer against PhingFile or a FileSet
|
|
*/
|
|
public function main() {
|
|
if (!class_exists('PHP_CodeSniffer')) {
|
|
include_once 'PHP/CodeSniffer.php';
|
|
}
|
|
|
|
if(!isset($this->file) and count($this->filesets) == 0) {
|
|
throw new BuildException("Missing either a nested fileset or attribute 'file' set");
|
|
}
|
|
|
|
if (count($this->formatters) == 0) {
|
|
// turn legacy format attribute into formatter
|
|
$fmt = new PhpCodeSnifferTask_FormatterElement();
|
|
$fmt->setType($this->format);
|
|
$fmt->setUseFile(false);
|
|
$this->formatters[] = $fmt;
|
|
}
|
|
|
|
if (!isset($this->file))
|
|
{
|
|
$fileList = array();
|
|
$project = $this->getProject();
|
|
foreach ($this->filesets as $fs) {
|
|
$ds = $fs->getDirectoryScanner($project);
|
|
$files = $ds->getIncludedFiles();
|
|
$dir = $fs->getDir($this->project)->getAbsolutePath();
|
|
foreach ($files as $file) {
|
|
$fileList[] = $dir.DIRECTORY_SEPARATOR.$file;
|
|
}
|
|
}
|
|
}
|
|
|
|
$codeSniffer = new PHP_CodeSniffer($this->verbosity, $this->tabWidth);
|
|
$codeSniffer->setAllowedFileExtensions($this->allowedFileExtensions);
|
|
if (is_array($this->ignorePatterns)) $codeSniffer->setIgnorePatterns($this->ignorePatterns);
|
|
foreach ($this->configData as $configData) {
|
|
$codeSniffer->setConfigData($configData->getName(), $configData->getValue(), true);
|
|
}
|
|
|
|
if ($this->file instanceof PhingFile) {
|
|
$codeSniffer->process($this->file->getPath(), $this->standard, $this->sniffs, $this->noSubdirectories);
|
|
|
|
} else {
|
|
$codeSniffer->process($fileList, $this->standard, $this->sniffs, $this->noSubdirectories);
|
|
}
|
|
|
|
$report = $this->printErrorReport($codeSniffer);
|
|
|
|
// generate the documentation
|
|
if ($this->docGenerator !== '' && $this->docFile !== null) {
|
|
ob_start();
|
|
|
|
$codeSniffer->generateDocs($this->standard, $this->sniffs, $this->docGenerator);
|
|
|
|
$output = ob_get_contents();
|
|
ob_end_clean();
|
|
|
|
// write to file
|
|
$outputFile = $this->docFile->getPath();
|
|
$check = file_put_contents($outputFile, $output);
|
|
|
|
if (is_bool($check) && !$check) {
|
|
throw new BuildException('Error writing doc to ' . $outputFile);
|
|
}
|
|
} elseif ($this->docGenerator !== '' && $this->docFile === null) {
|
|
$codeSniffer->generateDocs($this->standard, $this->sniffs, $this->docGenerator);
|
|
}
|
|
|
|
if ($this->haltonerror && $report['totals']['errors'] > 0)
|
|
{
|
|
throw new BuildException('phpcodesniffer detected ' . $report['totals']['errors']. ' error' . ($report['totals']['errors'] > 1 ? 's' : ''));
|
|
}
|
|
|
|
if ($this->haltonwarning && $report['totals']['warnings'] > 0)
|
|
{
|
|
throw new BuildException('phpcodesniffer detected ' . $report['totals']['warnings'] . ' warning' . ($report['totals']['warnings'] > 1 ? 's' : ''));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prints the error report.
|
|
*
|
|
* @param PHP_CodeSniffer $phpcs The PHP_CodeSniffer object containing
|
|
* the errors.
|
|
*
|
|
* @return int The number of error and warning messages shown.
|
|
*/
|
|
protected function printErrorReport($phpcs)
|
|
{
|
|
if ($this->showSniffs) {
|
|
$sniffs = $phpcs->getSniffs();
|
|
$sniffStr = '';
|
|
foreach ($sniffs as $sniff) {
|
|
$sniffStr .= '- ' . $sniff.PHP_EOL;
|
|
}
|
|
$this->log('The list of used sniffs (#' . count($sniffs) . '): ' . PHP_EOL . $sniffStr, Project::MSG_INFO);
|
|
}
|
|
|
|
$filesViolations = $phpcs->getFilesErrors();
|
|
$reporting = new PHP_CodeSniffer_Reporting();
|
|
$report = $reporting->prepare($filesViolations, $this->showWarnings);
|
|
|
|
// process output
|
|
foreach ($this->formatters as $fe) {
|
|
switch ($fe->getType()) {
|
|
case 'default':
|
|
// default format goes to logs, no buffering
|
|
$this->outputCustomFormat($report);
|
|
$fe->setUseFile(false);
|
|
break;
|
|
|
|
default:
|
|
$reportFile = '';
|
|
|
|
if ($fe->getUseFile()) {
|
|
$reportFile = $fe->getOutfile()->getPath();
|
|
ob_start();
|
|
}
|
|
|
|
$reporting->printReport(
|
|
$fe->getType(),
|
|
$filesViolations,
|
|
$this->showWarnings,
|
|
$this->showSources,
|
|
$reportFile,
|
|
$this->reportWidth
|
|
);
|
|
|
|
// reporting class uses ob_end_flush(), but we don't want
|
|
// an output if we use a file
|
|
if ($fe->getUseFile()) {
|
|
ob_end_clean();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $report;
|
|
}
|
|
|
|
/**
|
|
* Outputs the results with a custom format
|
|
*
|
|
* @param array $report Packaged list of all errors in each file
|
|
*/
|
|
protected function outputCustomFormat($report) {
|
|
$files = $report['files'];
|
|
foreach ($files as $file => $attributes) {
|
|
$errors = $attributes['errors'];
|
|
$warnings = $attributes['warnings'];
|
|
$messages = $attributes['messages'];
|
|
if ($errors > 0) {
|
|
$this->log($file . ': ' . $errors . ' error' . ($errors > 1 ? 's' : '') . ' detected', Project::MSG_ERR);
|
|
$this->outputCustomFormatMessages($messages, 'ERROR');
|
|
} else {
|
|
$this->log($file . ': No syntax errors detected', Project::MSG_VERBOSE);
|
|
}
|
|
if ($warnings > 0) {
|
|
$this->log($file . ': ' . $warnings . ' warning' . ($warnings > 1 ? 's' : '') . ' detected', Project::MSG_WARN);
|
|
$this->outputCustomFormatMessages($messages, 'WARNING');
|
|
}
|
|
}
|
|
|
|
$totalErrors = $report['totals']['errors'];
|
|
$totalWarnings = $report['totals']['warnings'];
|
|
$this->log(count($files) . ' files where checked', Project::MSG_INFO);
|
|
if ($totalErrors > 0) {
|
|
$this->log($totalErrors . ' error' . ($totalErrors > 1 ? 's' : '') . ' detected', Project::MSG_ERR);
|
|
} else {
|
|
$this->log('No syntax errors detected', Project::MSG_INFO);
|
|
}
|
|
if ($totalWarnings > 0) {
|
|
$this->log($totalWarnings . ' warning' . ($totalWarnings > 1 ? 's' : '') . ' detected', Project::MSG_INFO);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Outputs the messages of a specific type for one file
|
|
* @param array $messages
|
|
* @param string $type
|
|
*/
|
|
protected function outputCustomFormatMessages($messages, $type) {
|
|
foreach ($messages as $line => $messagesPerLine) {
|
|
foreach ($messagesPerLine as $column => $messagesPerColumn) {
|
|
foreach ($messagesPerColumn as $message) {
|
|
$msgType = $message['type'];
|
|
if ($type == $msgType) {
|
|
$logLevel = Project::MSG_INFO;
|
|
if ($msgType == 'ERROR') {
|
|
$logLevel = Project::MSG_ERR;
|
|
} else if ($msgType == 'WARNING') {
|
|
$logLevel = Project::MSG_WARN;
|
|
}
|
|
$text = $message['message'];
|
|
$string = $msgType . ' in line ' . $line . ' column ' . $column . ': ' . $text;
|
|
$this->log($string, $logLevel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} //end phpCodeSnifferTask
|
|
|
|
class PhpCodeSnifferTask_FormatterElement extends DataType {
|
|
|
|
/**
|
|
* Type of output to generate
|
|
* @var string
|
|
*/
|
|
protected $type = "";
|
|
|
|
/**
|
|
* Output to file?
|
|
* @var bool
|
|
*/
|
|
protected $useFile = true;
|
|
|
|
/**
|
|
* Output file.
|
|
* @var string
|
|
*/
|
|
protected $outfile = "";
|
|
|
|
/**
|
|
* Validate config.
|
|
*/
|
|
public function parsingComplete () {
|
|
if(empty($this->type)) {
|
|
throw new BuildException("Format missing required 'type' attribute.");
|
|
}
|
|
if ($useFile && empty($this->outfile)) {
|
|
throw new BuildException("Format requires 'outfile' attribute when 'useFile' is true.");
|
|
}
|
|
|
|
}
|
|
|
|
public function setType ($type) {
|
|
$this->type = $type;
|
|
}
|
|
|
|
public function getType () {
|
|
return $this->type;
|
|
}
|
|
|
|
public function setUseFile ($useFile) {
|
|
$this->useFile = $useFile;
|
|
}
|
|
|
|
public function getUseFile () {
|
|
return $this->useFile;
|
|
}
|
|
|
|
public function setOutfile (PhingFile $outfile) {
|
|
$this->outfile = $outfile;
|
|
}
|
|
|
|
public function getOutfile () {
|
|
return $this->outfile;
|
|
}
|
|
|
|
} //end FormatterElement
|