diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 978734111..5fa6d1698 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - php: [5.3, 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, 7.3, 7.4] + php: [5.3, 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0] dependency-version: [prefer-lowest, prefer-stable] exclude: - dependency-version: prefer-lowest @@ -49,8 +49,12 @@ jobs: restore-keys: | ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }} + - name: Upgrade PHPUnit + if: matrix.php >= 7.2 + run: composer require phpunit/phpunit:^5.7.27 --no-update --no-interaction --dev + - name: Install dependencies - run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress + run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress ${{ matrix.php >= 8 && '--ignore-platform-req=php' || '' }} - name: Execute Unit Tests run: vendor/bin/phpunit diff --git a/CHANGELOG b/CHANGELOG index a6b344008..99748d0eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,34 @@ +phpmd-2.10.1 (2021/05/11) +======================== + +- Implemented #885: Allowing 2.0 version of the composer/xdebug-handler + +phpmd-2.10.0 (2021/04/26) +======================== + +- Added #879: Documentation for Junit export with third party +- Added #836: Provide option to allow reference to a class in the root namespace without an import +- Added #856: Adds checkstyle compatible renderer, suitable for cs2pr or reviewdog +- Added #858 in #865: Add SARIF renderer. +- Added #873 and #876: Added option to baseline existing violations +- Added #861: Docs for GitHub renderer +- Added #868 In #869: Option to ignore globally-namespaced classes from MissingImport +- Added #834 : Add "tool" property to main "pmd" tag in XML report +- Fixed #673 in #782: Ignore dynamic class name from missing imports +- Fixed #577 in #844: Recognize compact variables with double quotes +- Fixed #818 in #822: Fix passing-by-reference detection +- Fixed #826 in #827: Consider foreach exception only for direct children +- Fixed #851 in #852: Fix multiple underscores in method name when allow-underscore-test is allowed +- Fixed #846 in #847: Catch DevelopmentCodeFragment with fully qualified functions +- Fixed #829 in #835: Fatal error while analyzing anonymous class +- Fixed #816 in #818: Fixed undefined index referring +- Changed #786: Add convenience method AbstractNode::findChildrenOfTypeVariable() +- Changed documentation: #874 #849 #724 +- Changed #514 in #872: Change exit code on processing errors +- Changed: Internal code improvement #839 #875 #838 #862 #788 #830 +- Changed #848 #864: Use GitHub actions +- Deprecated: getIgnorePattern and setIgnorePattern on PHPMD\PHPMD see #772 + phpmd-2.9.1 (2020/09/23) ======================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aff339ba9..daa623165 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ The PHPMD project welcomes your contribution. There are several ways to help out if you have found a bug or have an idea for a feature * Write test cases for open bug issues * Write patches for open bug/feature issues -* Participate on the PHPMD IRC Channel +* Participate on the PHPMD Gitter Channel There are a few guidelines that we need contributors to follow, so that we have a chance of keeping on top of things. @@ -22,6 +22,7 @@ Issues * Make sure it does not already exist. * Clearly describe the issue including steps to reproduce, when it is a bug. * Make sure you note the PHPMD version you use. + * Use one of the issue templates. Coding Standard --------------- @@ -44,4 +45,4 @@ Additional Resources * [Existing issues](https://github.com/phpmd/phpmd/issues/) * [General GitHub documentation](https://help.github.com/) * [GitHub pull request documentation](https://help.github.com/articles/creating-a-pull-request/) -* [PHPMD IRC Channel on freenode.org](http://webchat.freenode.net/?channels=phpmd) +* [PHPMD Gitter Channel](https://gitter.im/phpmd/community) diff --git a/README.rst b/README.rst index a825ff770..cec20e3b0 100644 --- a/README.rst +++ b/README.rst @@ -132,9 +132,12 @@ Command line options - ``--ignore-violations-on-exit`` - will exit with a zero code, even if any violations are found. - - ``--generate-baseline`` - will generate a phpmd.baseline.xml for existing violations + - ``--generate-baseline`` - will generate a ``phpmd.baseline.xml`` for existing violations next to the ruleset definition file. + - ``--update-baseline`` - will remove all violations from an existing ``phpmd.baseline.xml`` + that no longer exist. New violations will _not_ be added. + - ``--baseline-file`` - the filepath to a custom baseline xml file. The filepath of all baselined files must be relative to this file location. @@ -205,13 +208,15 @@ At the moment PHPMD comes with the following renderers: - *json*, formats JSON report. - *ansi*, a command line friendly format. - *github*, a format that GitHub Actions understands. +- *sarif*, the Static Analysis Results Interchange Format. +- *checkstyle*, language and tool agnostic XML format Baseline -------- For existing projects a violation baseline can be generated. All violations in this baseline will be ignored in further inspections. -The recommended approach would be a ``phpmd.xml`` in the root of the project. To generate the phpmd.baseline.xml next to it:: +The recommended approach would be a ``phpmd.xml`` in the root of the project. To generate the ``phpmd.baseline.xml`` next to it:: ~ $ phpmd /path/to/source text phpmd.xml --generate-baseline @@ -223,6 +228,10 @@ By default PHPMD will look next to ``phpmd.xml`` for ``phpmd.baseline.xml``. To ~ $ phpmd /path/to/source text phpmd.xml --baseline-file /path/to/source/phpmd.baseline.xml +To clean up an existing baseline file and *only remove* no longer existing violations:: + + ~ $ phpmd /path/to/source text phpmd.xml --update-baseline + PHPMD for enterprise -------------------- diff --git a/build.properties b/build.properties index 147fc3cb5..9918062fd 100644 --- a/build.properties +++ b/build.properties @@ -1,7 +1,7 @@ project.dir = project.uri = phpmd.org project.name = phpmd -project.version = 2.9.1 +project.version = 2.10.1 project.stability = stable # Disable pear support. This cannot be removed as long as setup tool is used diff --git a/composer.json b/composer.json index 557fd87f6..96b9548b6 100644 --- a/composer.json +++ b/composer.json @@ -36,14 +36,14 @@ "minimum-stability": "stable", "require": { "php": ">=5.3.9", - "pdepend/pdepend": "^2.8.0", + "pdepend/pdepend": "^2.9.1", "ext-xml": "*", - "composer/xdebug-handler": "^1.0" + "composer/xdebug-handler": "^1.0 || ^2.0" }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^5.7.27", "squizlabs/php_codesniffer": "^2.0", - "mikey179/vfsstream": "^1.6.4", + "mikey179/vfsstream": "^1.6.8", "gregwar/rst": "^1.0", "ext-simplexml": "*", "ext-json": "*", diff --git a/src/main/php/PHPMD/AbstractNode.php b/src/main/php/PHPMD/AbstractNode.php index dc13f8bc2..78dd3365e 100644 --- a/src/main/php/PHPMD/AbstractNode.php +++ b/src/main/php/PHPMD/AbstractNode.php @@ -17,7 +17,9 @@ namespace PHPMD; +use BadMethodCallException; use PDepend\Source\AST\AbstractASTArtifact; +use PDepend\Source\AST\ASTVariable; use PHPMD\Node\ASTNode; /** @@ -55,14 +57,14 @@ public function __construct($node) * @param string $name * @param array $args * @return mixed - * @throws \BadMethodCallException When the underlying PDepend node + * @throws BadMethodCallException When the underlying PDepend node * does not contain a method named $name. */ public function __call($name, array $args) { $node = $this->getNode(); if (!method_exists($node, $name)) { - throw new \BadMethodCallException( + throw new BadMethodCallException( sprintf('Invalid method %s() called.', $name) ); } @@ -138,6 +140,17 @@ public function findChildrenOfType($type) return $nodes; } + /** + * Searches recursive for all children of this node that are of variable. + * + * @return ASTVariable[] + * @todo Cover by a test. + */ + public function findChildrenOfTypeVariable() + { + return $this->findChildrenOfType('Variable'); + } + /** * Tests if this node represents the the given type. * diff --git a/src/main/php/PHPMD/AbstractRule.php b/src/main/php/PHPMD/AbstractRule.php index bcfd0d46f..f8cb4c709 100644 --- a/src/main/php/PHPMD/AbstractRule.php +++ b/src/main/php/PHPMD/AbstractRule.php @@ -387,14 +387,10 @@ protected function addViolation( array $args = array(), $metric = null ) { - $search = array(); - $replace = array(); - foreach ($args as $index => $value) { - $search[] = '{' . $index . '}'; - $replace[] = $value; - } - - $message = str_replace($search, $replace, $this->message); + $message = array( + 'message' => $this->message, + 'args' => $args, + ); $ruleViolation = new RuleViolation($this, $node, $message, $metric); $this->report->addRuleViolation($ruleViolation); diff --git a/src/main/php/PHPMD/Baseline/BaselineMode.php b/src/main/php/PHPMD/Baseline/BaselineMode.php new file mode 100644 index 000000000..a826ab67c --- /dev/null +++ b/src/main/php/PHPMD/Baseline/BaselineMode.php @@ -0,0 +1,21 @@ +baselineMode = $baselineMode; + $this->baselineSet = $baselineSet; + } + + /** + * @return bool + */ + public function isBaselined(RuleViolation $violation) + { + $contains = $this->baselineSet->contains( + get_class($violation->getRule()), + $violation->getFileName(), + $violation->getMethodName() + ); + + // regular baseline: violations is baselined if it is in the BaselineSet + if ($this->baselineMode === BaselineMode::NONE) { + return $contains; + } + + // update baseline: violation _can_ be baselined if it was already in the BaselineSet + if ($this->baselineMode === BaselineMode::UPDATE) { + return $contains === false; + } + + return false; + } +} diff --git a/src/main/php/PHPMD/PHPMD.php b/src/main/php/PHPMD/PHPMD.php index 1de1a7683..a61ad52d9 100644 --- a/src/main/php/PHPMD/PHPMD.php +++ b/src/main/php/PHPMD/PHPMD.php @@ -17,8 +17,6 @@ namespace PHPMD; -use PHPMD\Baseline\BaselineSet; - /** * This is the main facade of the PHP PMD application */ @@ -216,7 +214,7 @@ public function setOptions(array $options) * @param string $ruleSets * @param \PHPMD\AbstractRenderer[] $renderers * @param \PHPMD\RuleSetFactory $ruleSetFactory - * @param \PHPMD\Baseline\BaselineSet|null $baseline + * @param \PHPMD\Report $report * @return void */ public function processFiles( @@ -224,16 +222,13 @@ public function processFiles( $ruleSets, array $renderers, RuleSetFactory $ruleSetFactory, - BaselineSet $baseline = null + Report $report ) { - // Merge parsed excludes $this->addIgnorePatterns($ruleSetFactory->getIgnorePattern($ruleSets)); $this->input = $inputPath; - $report = new Report($baseline); - $factory = new ParserFactory(); $parser = $factory->create($this); diff --git a/src/main/php/PHPMD/Renderer/AnsiRenderer.php b/src/main/php/PHPMD/Renderer/AnsiRenderer.php index cad14932c..3486e4ccf 100644 --- a/src/main/php/PHPMD/Renderer/AnsiRenderer.php +++ b/src/main/php/PHPMD/Renderer/AnsiRenderer.php @@ -38,6 +38,10 @@ private function writeViolationsReport(Report $report) $previousFile = null; foreach ($report->getRuleViolations() as $violation) { if ($violation->getFileName() !== $previousFile) { + if ($previousFile !== null) { + $this->getWriter()->write(PHP_EOL); + } + $this->writeViolationFileHeader($violation); } diff --git a/src/main/php/PHPMD/Renderer/CheckStyleRenderer.php b/src/main/php/PHPMD/Renderer/CheckStyleRenderer.php new file mode 100644 index 000000000..72ef5b451 --- /dev/null +++ b/src/main/php/PHPMD/Renderer/CheckStyleRenderer.php @@ -0,0 +1,106 @@ + 2 maps to info level severity + * + * @param integer $priority priority of the broken rule + * @return string either error, warning or info + */ + protected function mapPriorityToSeverity($priority) + { + if ($priority > 2) { + return 'info'; + } + + return (int)$priority === 2 ? 'warning' : 'error'; + } + /** + * This method will be called when the engine has finished the source analysis + * phase. + * + * @param \PHPMD\Report $report + */ + public function renderReport(Report $report) + { + $writer = $this->getWriter(); + $writer->write(''); + $writer->write(\PHP_EOL); + + foreach ($report->getRuleViolations() as $violation) { + $fileName = $violation->getFileName(); + + if ($this->fileName !== $fileName) { + // Not first file + if (null !== $this->fileName) { + $writer->write(' ' . \PHP_EOL); + } + // Store current file name + $this->fileName = $fileName; + + $writer->write(' ' . \PHP_EOL); + } + + $rule = $violation->getRule(); + + $writer->write(' write(' line="' . $violation->getBeginLine() . '"'); + $writer->write(' endline="' . $violation->getEndLine() . '"'); + $writer->write(\sprintf(' severity="%s"', $this->mapPriorityToSeverity($rule->getPriority()))); + $writer->write(\sprintf( + ' message="%s (%s, %s) "', + \htmlspecialchars($violation->getDescription()), + $rule->getName(), + $rule->getRuleSetName() + )); + + $this->maybeAdd('package', $violation->getNamespaceName()); + $this->maybeAdd('externalInfoUrl', $rule->getExternalInfoUrl()); + $this->maybeAdd('function', $violation->getFunctionName()); + $this->maybeAdd('class', $violation->getClassName()); + $this->maybeAdd('method', $violation->getMethodName()); + //$this->_maybeAdd('variable', $violation->getVariableName()); + + $writer->write(' />' . \PHP_EOL); + } + + // Last file and at least one violation + if (null !== $this->fileName) { + $writer->write(' ' . \PHP_EOL); + } + + foreach ($report->getErrors() as $error) { + $writer->write(' '); + $writer->write($error->getFile()); + $writer->write('' . \PHP_EOL); + } + + $writer->write('' . \PHP_EOL); + } +} diff --git a/src/main/php/PHPMD/Renderer/HTMLRenderer.php b/src/main/php/PHPMD/Renderer/HTMLRenderer.php index 5d11565de..13f2a22fa 100644 --- a/src/main/php/PHPMD/Renderer/HTMLRenderer.php +++ b/src/main/php/PHPMD/Renderer/HTMLRenderer.php @@ -19,6 +19,7 @@ use PHPMD\AbstractRenderer; use PHPMD\Report; +use SplFileObject; /** * This renderer output a html file with all found violations. @@ -376,7 +377,8 @@ class='info-lnk blck' // Create an external link to rule's help, if there's any provided. $linkHtml = null; - if ($url = $violation->getRule()->getExternalInfoUrl()) { + $url = $violation->getRule()->getExternalInfoUrl(); + if ($url) { $linkHtml = "(help)"; } @@ -429,7 +431,7 @@ protected static function getLineExcerpt($file, $lineNumber, $extra = 0) return array(); } - $file = new \SplFileObject($file); + $file = new SplFileObject($file); // We have to subtract 1 to extract correct lines via SplFileObject. $line = max($lineNumber - 1 - $extra, 0); @@ -548,8 +550,8 @@ protected static function sumUpViolations($violations) foreach ($violations as $v) { // We use "ref" reference to make things somewhat easier to read. // Also, using a reference to non-existing array index doesn't throw a notice. - - if ($namespaceName = $v->getNamespaceName()) { + $namespaceName = $v->getNamespaceName(); + if ($namespaceName) { $ref = &$result[self::CATEGORY_NAMESPACE][$namespaceName]; $ref = isset($ref) ? $ref + 1 : 1; } diff --git a/src/main/php/PHPMD/Renderer/JSONRenderer.php b/src/main/php/PHPMD/Renderer/JSONRenderer.php index d91b9879c..a1c34ea77 100644 --- a/src/main/php/PHPMD/Renderer/JSONRenderer.php +++ b/src/main/php/PHPMD/Renderer/JSONRenderer.php @@ -45,7 +45,7 @@ public function renderReport(Report $report) * * @return array */ - private function initReportData() + protected function initReportData() { $data = array( 'version' => PHPMD::VERSION, @@ -63,7 +63,7 @@ private function initReportData() * @param array $data The report output to add the violations to. * @return array The report output with violations, if any. */ - private function addViolationsToReport(Report $report, array $data) + protected function addViolationsToReport(Report $report, array $data) { $filesList = array(); /** @var RuleViolation $violation */ @@ -97,7 +97,7 @@ private function addViolationsToReport(Report $report, array $data) * @param array $data The report output to add the errors to. * @return array The report output with errors, if any. */ - private function addErrorsToReport(Report $report, array $data) + protected function addErrorsToReport(Report $report, array $data) { $errors = $report->getErrors(); if ($errors) { diff --git a/src/main/php/PHPMD/Renderer/SARIFRenderer.php b/src/main/php/PHPMD/Renderer/SARIFRenderer.php new file mode 100644 index 000000000..8962a976e --- /dev/null +++ b/src/main/php/PHPMD/Renderer/SARIFRenderer.php @@ -0,0 +1,229 @@ +. + * All rights reserved. + * + * Licensed under BSD License + * For full copyright and license information, please see the LICENSE file. + * Redistributions of files must retain the above copyright notice. + * + * @author Lukas Bestle + * @copyright Manuel Pichler. All rights reserved. + * @license https://opensource.org/licenses/bsd-license.php BSD License + * @link http://phpmd.org/ + */ + +namespace PHPMD\Renderer; + +use PHPMD\PHPMD; +use PHPMD\Report; +use PHPMD\Renderer\JSONRenderer; + +/** + * This class will render a SARIF (Static Analysis + * Results Interchange Format) report. + */ +class SARIFRenderer extends JSONRenderer +{ + /** + * Create report data and add renderer meta properties + * + * @return array + */ + protected function initReportData() + { + $data = array( + 'version' => '2.1.0', + '$schema' => + 'https://raw.githubusercontent.com/oasis-tcs/' . + 'sarif-spec/master/Schemata/sarif-schema-2.1.0.json', + 'runs' => array( + array( + 'tool' => array( + 'driver' => array( + 'name' => 'PHPMD', + 'informationUri' => 'https://phpmd.org', + 'version' => PHPMD::VERSION, + 'rules' => array(), + ), + ), + 'originalUriBaseIds' => array( + 'WORKINGDIR' => array( + 'uri' => static::pathToUri(getcwd()) . '/', + ), + ), + 'results' => array(), + ), + ), + ); + + return $data; + } + + /** + * Add violations, if any, to the report data + * + * @param Report $report The report with potential violations. + * @param array $data The report output to add the violations to. + * @return array The report output with violations, if any. + */ + protected function addViolationsToReport(Report $report, array $data) + { + $rules = array(); + $results = array(); + $ruleIndices = array(); + + /** @var RuleViolation $violation */ + foreach ($report->getRuleViolations() as $violation) { + $rule = $violation->getRule(); + $ruleRef = str_replace(' ', '', $rule->getRuleSetName()) . '/' . $rule->getName(); + + if (!isset($ruleIndices[$ruleRef])) { + $ruleIndices[$ruleRef] = count($rules); + + $ruleData = array( + 'id' => $ruleRef, + 'name' => $rule->getName(), + 'shortDescription' => array( + 'text' => $rule->getRuleSetName() . ': ' . $rule->getName(), + ), + 'messageStrings' => array( + 'default' => array( + 'text' => trim($rule->getMessage()), + ), + ), + 'help' => array( + 'text' => trim(str_replace("\n", ' ', $rule->getDescription())), + ), + 'helpUri' => $rule->getExternalInfoUrl(), + 'properties' => array( + 'ruleSet' => $rule->getRuleSetName(), + 'priority' => $rule->getPriority(), + ), + ); + + $examples = $rule->getExamples(); + if (!empty($examples)) { + $ruleData['help']['markdown'] = + $ruleData['help']['text'] . + "\n\n### Example\n\n```php\n" . + implode("\n```\n\n```php\n", array_map('trim', $examples)) . "\n```"; + } + + $since = $rule->getSince(); + if ($since) { + $ruleData['properties']['since'] = 'PHPMD ' . $since; + } + + $rules[] = $ruleData; + } + + $arguments = $violation->getArgs(); + if ($arguments === null) { + $arguments = array(); + } + + $results[] = array( + 'ruleId' => $ruleRef, + 'ruleIndex' => $ruleIndices[$ruleRef], + 'message' => array( + 'id' => 'default', + 'arguments' => array_map('strval', $arguments), + 'text' => $violation->getDescription(), + ), + 'locations' => array( + array( + 'physicalLocation' => array( + 'artifactLocation' => static::pathToArtifactLocation($violation->getFileName()), + 'region' => array( + 'startLine' => $violation->getBeginLine(), + 'endLine' => $violation->getEndLine(), + ), + ), + ) + ), + ); + } + + $data['runs'][0]['tool']['driver']['rules'] = $rules; + $data['runs'][0]['results'] = array_merge($data['runs'][0]['results'], $results); + + return $data; + } + + /** + * Add errors, if any, to the report data + * + * @param Report $report The report with potential errors. + * @param array $data The report output to add the errors to. + * @return array The report output with errors, if any. + */ + protected function addErrorsToReport(Report $report, array $data) + { + $errors = $report->getErrors(); + if ($errors) { + foreach ($errors as $error) { + $data['runs'][0]['results'][] = array( + 'level' => 'error', + 'message' => array( + 'text' => $error->getMessage(), + ), + 'locations' => array( + array( + 'physicalLocation' => array( + 'artifactLocation' => static::pathToArtifactLocation($error->getFile()), + ), + ) + ), + ); + } + } + + return $data; + } + + /** + * Makes an absolute path relative to the working directory + * if possible, otherwise prepends the `file://` protocol + * and returns the result as a SARIF `artifactLocation` + * + * @param string $path + * @return array + */ + protected static function pathToArtifactLocation($path) + { + $workingDir = getcwd(); + if (substr($path, 0, strlen($workingDir)) === $workingDir) { + // relative path + return array( + 'uri' => substr($path, strlen($workingDir) + 1), + 'uriBaseId' => 'WORKINGDIR', + ); + } + + // absolute path with protocol + return array( + 'uri' => static::pathToUri($path), + ); + } + + /** + * Converts an absolute path to a file:// URI + * + * @param string $path + * @return string + */ + protected static function pathToUri($path) + { + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + + // file:///C:/... on Windows systems + if (substr($path, 0, 1) !== '/') { + $path = '/' . $path; + } + + return 'file://' . $path; + } +} diff --git a/src/main/php/PHPMD/Renderer/XMLRenderer.php b/src/main/php/PHPMD/Renderer/XMLRenderer.php index 218e04972..5c529f199 100644 --- a/src/main/php/PHPMD/Renderer/XMLRenderer.php +++ b/src/main/php/PHPMD/Renderer/XMLRenderer.php @@ -120,7 +120,7 @@ public function renderReport(Report $report) * @param string $value The attribute value. * @return void */ - private function maybeAdd($attr, $value) + protected function maybeAdd($attr, $value) { if ($value === null || trim($value) === '') { return; diff --git a/src/main/php/PHPMD/Report.php b/src/main/php/PHPMD/Report.php index 2df24ac9b..e5d6933a6 100644 --- a/src/main/php/PHPMD/Report.php +++ b/src/main/php/PHPMD/Report.php @@ -17,7 +17,8 @@ namespace PHPMD; -use PHPMD\Baseline\BaselineSet; +use ArrayIterator; +use PHPMD\Baseline\BaselineValidator; /** * The report class collects all found violations and further information about @@ -54,12 +55,12 @@ class Report */ private $errors = array(); - /** @var BaselineSet|null */ - private $baseline; + /** @var BaselineValidator|null */ + private $baselineValidator; - public function __construct(BaselineSet $baseline = null) + public function __construct(BaselineValidator $baselineValidator = null) { - $this->baseline = $baseline; + $this->baselineValidator = $baselineValidator; } /** @@ -70,12 +71,11 @@ public function __construct(BaselineSet $baseline = null) */ public function addRuleViolation(RuleViolation $violation) { - $fileName = $violation->getFileName(); - $ruleName = get_class($violation->getRule()); - if ($this->baseline !== null && $this->baseline->contains($ruleName, $fileName, $violation->getMethodName())) { + if ($this->baselineValidator !== null && $this->baselineValidator->isBaselined($violation)) { return; } + $fileName = $violation->getFileName(); if (!isset($this->ruleViolations[$fileName])) { $this->ruleViolations[$fileName] = array(); } @@ -119,7 +119,7 @@ public function getRuleViolations() } } - return new \ArrayIterator($violations); + return new ArrayIterator($violations); } /** @@ -155,7 +155,7 @@ public function hasErrors() */ public function getErrors() { - return new \ArrayIterator($this->errors); + return new ArrayIterator($this->errors); } /** diff --git a/src/main/php/PHPMD/Rule/AbstractLocalVariable.php b/src/main/php/PHPMD/Rule/AbstractLocalVariable.php index 84b71b303..01fe7a212 100644 --- a/src/main/php/PHPMD/Rule/AbstractLocalVariable.php +++ b/src/main/php/PHPMD/Rule/AbstractLocalVariable.php @@ -264,6 +264,33 @@ protected function getNode($node) return $node; } + /** + * Reflect function trying as namespaced function first, then global function. + * + * @SuppressWarnings(PHPMD.EmptyCatchBlock) + * @param string $functionName + * @return ReflectionFunction|null + */ + private function getReflectionFunctionByName($functionName) + { + try { + return new ReflectionFunction($functionName); + } catch (ReflectionException $exception) { + $chunks = explode('\\', $functionName); + + if (count($chunks) > 1) { + try { + return new ReflectionFunction(end($chunks)); + } catch (ReflectionException $exception) { + } + // @TODO: Find a way to handle user-land functions + // @TODO: Find a way to handle methods + } + } + + return null; + } + /** * Return true if the given variable is passed by reference in a native PHP function. * @@ -274,29 +301,28 @@ protected function isPassedByReference($variable) { $parent = $this->getNode($variable->getParent()); - if ($parent && $parent instanceof ASTArguments) { - $argumentPosition = array_search($this->getNode($variable), $parent->getChildren()); - $function = $this->getNode($parent->getParent()); - $functionParent = $this->getNode($function->getParent()); - $functionName = $function->getImage(); + if (!($parent && $parent instanceof ASTArguments)) { + return false; + } - if ($functionParent instanceof ASTMemberPrimaryPrefix) { - // @TODO: Find a way to handle methods - return false; - } + $argumentPosition = array_search($this->getNode($variable), $parent->getChildren()); + $function = $this->getNode($parent->getParent()); + $functionParent = $this->getNode($function->getParent()); + $functionName = $function->getImage(); - try { - $reflectionFunction = new ReflectionFunction($functionName); - $parameters = $reflectionFunction->getParameters(); + if ($functionParent instanceof ASTMemberPrimaryPrefix) { + // @TODO: Find a way to handle methods + return false; + } - if (isset($parameters[$argumentPosition]) && $parameters[$argumentPosition]->isPassedByReference()) { - return true; - } - } catch (ReflectionException $exception) { - // @TODO: Find a way to handle user-land functions - } + $reflectionFunction = $this->getReflectionFunctionByName($functionName); + + if (!$reflectionFunction) { + return false; } - return false; + $parameters = $reflectionFunction->getParameters(); + + return isset($parameters[$argumentPosition]) && $parameters[$argumentPosition]->isPassedByReference(); } } diff --git a/src/main/php/PHPMD/Rule/CleanCode/MissingImport.php b/src/main/php/PHPMD/Rule/CleanCode/MissingImport.php index 423cf9b90..0ac973cd6 100644 --- a/src/main/php/PHPMD/Rule/CleanCode/MissingImport.php +++ b/src/main/php/PHPMD/Rule/CleanCode/MissingImport.php @@ -44,6 +44,7 @@ class MissingImport extends AbstractRule implements MethodAware, FunctionAware public function apply(AbstractNode $node) { $ignoreGlobal = $this->getBooleanProperty('ignore-global'); + foreach ($node->findChildrenOfType('AllocationExpression') as $allocationNode) { if (!$allocationNode) { continue; diff --git a/src/main/php/PHPMD/Rule/CleanCode/UndefinedVariable.php b/src/main/php/PHPMD/Rule/CleanCode/UndefinedVariable.php index 510e0c2df..091714d88 100644 --- a/src/main/php/PHPMD/Rule/CleanCode/UndefinedVariable.php +++ b/src/main/php/PHPMD/Rule/CleanCode/UndefinedVariable.php @@ -72,9 +72,7 @@ public function apply(AbstractNode $node) } } - foreach ($node->findChildrenOfType('Variable') as $variable) { - /** @var ASTVariable $variable */ - + foreach ($node->findChildrenOfTypeVariable() as $variable) { if ($this->isSuperGlobal($variable) || $this->isPassedByReference($variable)) { $this->addVariableDefinition($variable); } elseif (!$this->checkVariableDefined($variable, $node)) { @@ -251,7 +249,7 @@ protected function collectAssignments(AbstractCallableNode $node) $variable = $assignment->getChild(0); if ($variable->getNode() instanceof ASTArray) { - foreach ($variable->findChildrenOfType('Variable') as $unpackedVariable) { + foreach ($variable->findChildrenOfTypeVariable() as $unpackedVariable) { $this->addVariableDefinition($unpackedVariable); } diff --git a/src/main/php/PHPMD/Rule/Controversial/CamelCaseVariableName.php b/src/main/php/PHPMD/Rule/Controversial/CamelCaseVariableName.php index 620711f0b..4dbfd87ec 100644 --- a/src/main/php/PHPMD/Rule/Controversial/CamelCaseVariableName.php +++ b/src/main/php/PHPMD/Rule/Controversial/CamelCaseVariableName.php @@ -56,7 +56,7 @@ class CamelCaseVariableName extends AbstractRule implements MethodAware, Functio */ public function apply(AbstractNode $node) { - foreach ($node->findChildrenOfType('Variable') as $variable) { + foreach ($node->findChildrenOfTypeVariable() as $variable) { if (!$this->isValid($variable)) { $this->addViolation( $node, diff --git a/src/main/php/PHPMD/Rule/Controversial/Superglobals.php b/src/main/php/PHPMD/Rule/Controversial/Superglobals.php index 81224e66d..559b5b14b 100644 --- a/src/main/php/PHPMD/Rule/Controversial/Superglobals.php +++ b/src/main/php/PHPMD/Rule/Controversial/Superglobals.php @@ -58,7 +58,7 @@ class Superglobals extends AbstractRule implements MethodAware, FunctionAware */ public function apply(AbstractNode $node) { - foreach ($node->findChildrenOfType('Variable') as $variable) { + foreach ($node->findChildrenOfTypeVariable() as $variable) { if (in_array($variable->getImage(), $this->superglobals)) { $this->addViolation( $node, diff --git a/src/main/php/PHPMD/Rule/Naming/LongVariable.php b/src/main/php/PHPMD/Rule/Naming/LongVariable.php index 159aac559..3b26c390e 100644 --- a/src/main/php/PHPMD/Rule/Naming/LongVariable.php +++ b/src/main/php/PHPMD/Rule/Naming/LongVariable.php @@ -74,7 +74,7 @@ public function apply(AbstractNode $node) $this->checkNodeImage($declarator); } - $variables = $node->findChildrenOfType('Variable'); + $variables = $node->findChildrenOfTypeVariable(); foreach ($variables as $variable) { $this->checkNodeImage($variable); } diff --git a/src/main/php/PHPMD/Rule/Naming/ShortVariable.php b/src/main/php/PHPMD/Rule/Naming/ShortVariable.php index 29f25e486..b61544882 100644 --- a/src/main/php/PHPMD/Rule/Naming/ShortVariable.php +++ b/src/main/php/PHPMD/Rule/Naming/ShortVariable.php @@ -96,7 +96,7 @@ protected function applyNonClass(AbstractNode $node) $this->checkNodeImage($declarator); } - $variables = $node->findChildrenOfType('Variable'); + $variables = $node->findChildrenOfTypeVariable(); foreach ($variables as $variable) { $this->checkNodeImage($variable); } diff --git a/src/main/php/PHPMD/Rule/UnusedFormalParameter.php b/src/main/php/PHPMD/Rule/UnusedFormalParameter.php index 2538ef959..33498cd9b 100644 --- a/src/main/php/PHPMD/Rule/UnusedFormalParameter.php +++ b/src/main/php/PHPMD/Rule/UnusedFormalParameter.php @@ -190,7 +190,7 @@ protected function removeUsedParameters(AbstractNode $node) */ protected function removeRegularVariables(AbstractNode $node) { - $variables = $node->findChildrenOfType('Variable'); + $variables = $node->findChildrenOfTypeVariable(); foreach ($variables as $variable) { /** @var $variable ASTNode */ diff --git a/src/main/php/PHPMD/Rule/UnusedLocalVariable.php b/src/main/php/PHPMD/Rule/UnusedLocalVariable.php index 8cef7a45d..9d66e4145 100644 --- a/src/main/php/PHPMD/Rule/UnusedLocalVariable.php +++ b/src/main/php/PHPMD/Rule/UnusedLocalVariable.php @@ -116,8 +116,7 @@ protected function removeParameters(AbstractCallableNode $node) */ protected function collectVariables(AbstractCallableNode $node) { - foreach ($node->findChildrenOfType('Variable') as $variable) { - /** @var $variable ASTNode */ + foreach ($node->findChildrenOfTypeVariable() as $variable) { if ($this->isLocal($variable)) { $this->collectVariable($variable); } diff --git a/src/main/php/PHPMD/RuleSet.php b/src/main/php/PHPMD/RuleSet.php index 7058a37e0..644780219 100644 --- a/src/main/php/PHPMD/RuleSet.php +++ b/src/main/php/PHPMD/RuleSet.php @@ -17,6 +17,8 @@ namespace PHPMD; +use ArrayIterator; + /** * This class is a collection of concrete source analysis rules. */ @@ -211,7 +213,7 @@ public function getRules() } } - return new \ArrayIterator($result); + return new ArrayIterator($result); } /** diff --git a/src/main/php/PHPMD/RuleSetFactory.php b/src/main/php/PHPMD/RuleSetFactory.php index 94336e3ef..e0d048c76 100644 --- a/src/main/php/PHPMD/RuleSetFactory.php +++ b/src/main/php/PHPMD/RuleSetFactory.php @@ -17,6 +17,8 @@ namespace PHPMD; +use RuntimeException; + /** * This factory class is used to create the {@link \PHPMD\RuleSet} instance * that PHPMD will use to analyze the source code. @@ -184,7 +186,7 @@ private static function listRuleSetsInDirectory($directory) * * @param string $fileName * @return \PHPMD\RuleSet - * @throws \RuntimeException When loading the XML file fails. + * @throws RuntimeException When loading the XML file fails. */ private function parseRuleSetNode($fileName) { @@ -196,7 +198,7 @@ private function parseRuleSetNode($fileName) // Reset error handling to previous setting libxml_use_internal_errors($libxml); - throw new \RuntimeException(trim(libxml_get_last_error()->message)); + throw new RuntimeException(trim(libxml_get_last_error()->message)); } $ruleSet = new RuleSet(); @@ -505,7 +507,7 @@ private function getPropertyValue(\SimpleXMLElement $propertyNode) * * @param string $fileName The filename of a rule-set definition. * @return array|null - * @throws \RuntimeException Thrown if file is not proper xml + * @throws RuntimeException Thrown if file is not proper xml */ public function getIgnorePattern($fileName) { @@ -521,7 +523,7 @@ public function getIgnorePattern($fileName) // Reset error handling to previous setting libxml_use_internal_errors($libxml); - throw new \RuntimeException(trim(libxml_get_last_error()->message)); + throw new RuntimeException(trim(libxml_get_last_error()->message)); } foreach ($xml->children() as $node) { diff --git a/src/main/php/PHPMD/RuleViolation.php b/src/main/php/PHPMD/RuleViolation.php index 1c7b3c0b9..8865f69e4 100644 --- a/src/main/php/PHPMD/RuleViolation.php +++ b/src/main/php/PHPMD/RuleViolation.php @@ -48,6 +48,14 @@ class RuleViolation */ private $description; + /** + * The arguments for the description/message text or null + * when the arguments are unknown. + * + * @var array|null + */ + private $args = null; + /** * The raw metric value which caused this rule violation. * @@ -83,7 +91,7 @@ class RuleViolation * * @param \PHPMD\Rule $rule * @param \PHPMD\AbstractNode $node - * @param string $violationMessage + * @param string|array $violationMessage * @param mixed $metric */ public function __construct(Rule $rule, AbstractNode $node, $violationMessage, $metric = null) @@ -91,7 +99,20 @@ public function __construct(Rule $rule, AbstractNode $node, $violationMessage, $ $this->rule = $rule; $this->node = $node; $this->metric = $metric; - $this->description = $violationMessage; + + if (is_array($violationMessage) === true) { + $search = array(); + $replace = array(); + foreach ($violationMessage['args'] as $index => $value) { + $search[] = '{' . $index . '}'; + $replace[] = $value; + } + + $this->args = $violationMessage['args']; + $this->description = str_replace($search, $replace, $violationMessage['message']); + } else { + $this->description = $violationMessage; + } if ($node instanceof AbstractTypeNode) { $this->className = $node->getName(); @@ -123,6 +144,17 @@ public function getDescription() return $this->description; } + /** + * Returns the arguments for the description/message text or null + * when the arguments are unknown. + * + * @return array|null + */ + public function getArgs() + { + return $this->args; + } + /** * Returns the raw metric value which caused this rule violation. * diff --git a/src/main/php/PHPMD/TextUI/Command.php b/src/main/php/PHPMD/TextUI/Command.php index 548a506e2..5bb10367b 100644 --- a/src/main/php/PHPMD/TextUI/Command.php +++ b/src/main/php/PHPMD/TextUI/Command.php @@ -18,9 +18,12 @@ namespace PHPMD\TextUI; use PHPMD\Baseline\BaselineFileFinder; +use PHPMD\Baseline\BaselineMode; use PHPMD\Baseline\BaselineSetFactory; +use PHPMD\Baseline\BaselineValidator; use PHPMD\PHPMD; use PHPMD\Renderer\RendererFactory; +use PHPMD\Report; use PHPMD\RuleSetFactory; use PHPMD\Utility\Paths; use PHPMD\Writer\StreamWriter; @@ -80,16 +83,22 @@ public function run(CommandLineOptions $opts, RuleSetFactory $ruleSetFactory) } // Configure baseline violations - $baseline = null; + $report = null; $finder = new BaselineFileFinder($opts); - if ($opts->generateBaseline()) { + if ($opts->generateBaseline() === BaselineMode::GENERATE) { // overwrite any renderer with the baseline renderer $renderers = array(RendererFactory::createBaselineRenderer(new StreamWriter($finder->notNull()->find()))); + } elseif ($opts->generateBaseline() === BaselineMode::UPDATE) { + $baselineFile = $finder->notNull()->existingFile()->find(); + $baseline = BaselineSetFactory::fromFile(Paths::getRealPath($baselineFile)); + $renderers = array(RendererFactory::createBaselineRenderer(new StreamWriter($baselineFile))); + $report = new Report(new BaselineValidator($baseline, BaselineMode::UPDATE)); } else { // try to locate a baseline file and read it $baselineFile = $finder->existingFile()->find(); if ($baselineFile !== null) { $baseline = BaselineSetFactory::fromFile(Paths::getRealPath($baselineFile)); + $report = new Report(new BaselineValidator($baseline, BaselineMode::NONE)); } } @@ -124,14 +133,16 @@ public function run(CommandLineOptions $opts, RuleSetFactory $ruleSetFactory) $opts->getRuleSets(), $renderers, $ruleSetFactory, - $baseline + $report !== null ? $report : new Report() ); if ($phpmd->hasErrors() && !$opts->ignoreErrorsOnExit()) { return self::EXIT_ERROR; } - if ($phpmd->hasViolations() && !$opts->ignoreViolationsOnExit() && !$opts->generateBaseline()) { + if ($phpmd->hasViolations() + && !$opts->ignoreViolationsOnExit() + && $opts->generateBaseline() === BaselineMode::NONE) { return self::EXIT_VIOLATION; } diff --git a/src/main/php/PHPMD/TextUI/CommandLineOptions.php b/src/main/php/PHPMD/TextUI/CommandLineOptions.php index 31c8c7125..055482d29 100644 --- a/src/main/php/PHPMD/TextUI/CommandLineOptions.php +++ b/src/main/php/PHPMD/TextUI/CommandLineOptions.php @@ -17,11 +17,15 @@ namespace PHPMD\TextUI; +use InvalidArgumentException; +use PHPMD\Baseline\BaselineMode; use PHPMD\Renderer\AnsiRenderer; use PHPMD\Renderer\GitHubRenderer; use PHPMD\Renderer\HTMLRenderer; use PHPMD\Renderer\JSONRenderer; +use PHPMD\Renderer\SARIFRenderer; use PHPMD\Renderer\TextRenderer; +use PHPMD\Renderer\CheckStyleRenderer; use PHPMD\Renderer\XMLRenderer; use PHPMD\Rule; @@ -149,9 +153,9 @@ class CommandLineOptions /** * Should PHPMD baseline the existing violations and write them to the $baselineFile - * @var bool + * @var string allowed modes: NONE, GENERATE or UPDATE */ - protected $generateBaseline = false; + protected $generateBaseline = BaselineMode::NONE; /** * The baseline source file to read the baseline violations from. @@ -165,7 +169,7 @@ class CommandLineOptions * * @param string[] $args * @param string[] $availableRuleSets - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function __construct(array $args, array $availableRuleSets = array()) { @@ -225,7 +229,10 @@ public function __construct(array $args, array $availableRuleSets = array()) $this->strict = false; break; case '--generate-baseline': - $this->generateBaseline = true; + $this->generateBaseline = BaselineMode::GENERATE; + break; + case '--update-baseline': + $this->generateBaseline = BaselineMode::UPDATE; break; case '--baseline-file': $this->baselineFile = array_shift($args); @@ -236,11 +243,13 @@ public function __construct(array $args, array $availableRuleSets = array()) case '--ignore-violations-on-exit': $this->ignoreViolationsOnExit = true; break; + case '--reportfile-checkstyle': case '--reportfile-html': + case '--reportfile-json': + case '--reportfile-sarif': case '--reportfile-text': case '--reportfile-xml': - case '--reportfile-json': - preg_match('(^\-\-reportfile\-(xml|html|text|json)$)', $arg, $match); + preg_match('(^\-\-reportfile\-(checkstyle|html|json|sarif|text|xml)$)', $arg, $match); $this->reportFiles[$match[1]] = array_shift($args); break; default: @@ -250,7 +259,7 @@ public function __construct(array $args, array $availableRuleSets = array()) } if (count($arguments) < 3) { - throw new \InvalidArgumentException($this->usage(), self::INPUT_ERROR); + throw new InvalidArgumentException($this->usage(), self::INPUT_ERROR); } $this->inputPath = (string)array_shift($arguments); @@ -387,7 +396,7 @@ public function hasStrict() /** * Should the current violations be baselined * - * @return bool + * @return string */ public function generateBaseline() { @@ -439,25 +448,29 @@ public function ignoreViolationsOnExit() * * @param string $reportFormat * @return \PHPMD\AbstractRenderer - * @throws \InvalidArgumentException When the specified renderer does not exist. + * @throws InvalidArgumentException When the specified renderer does not exist. */ public function createRenderer($reportFormat = null) { $reportFormat = $reportFormat ?: $this->reportFormat; switch ($reportFormat) { - case 'xml': - return $this->createXmlRenderer(); - case 'html': - return $this->createHtmlRenderer(); - case 'text': - return $this->createTextRenderer(); - case 'json': - return $this->createJsonRenderer(); case 'ansi': return $this->createAnsiRenderer(); + case 'checkstyle': + return $this->createCheckStyleRenderer(); case 'github': return $this->createGitHubRenderer(); + case 'html': + return $this->createHtmlRenderer(); + case 'json': + return $this->createJsonRenderer(); + case 'sarif': + return $this->createSarifRenderer(); + case 'text': + return $this->createTextRenderer(); + case 'xml': + return $this->createXmlRenderer(); default: return $this->createCustomRenderer(); } @@ -511,14 +524,30 @@ protected function createJsonRenderer() return new JSONRenderer(); } + /** + * @return \PHPMD\Renderer\JSONRenderer + */ + protected function createCheckStyleRenderer() + { + return new CheckStyleRenderer(); + } + + /** + * @return \PHPMD\Renderer\SARIFRenderer + */ + protected function createSarifRenderer() + { + return new SARIFRenderer(); + } + /** * @return \PHPMD\AbstractRenderer - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function createCustomRenderer() { if ('' === $this->reportFormat) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Can\'t create report with empty format.', self::INPUT_ERROR ); @@ -533,7 +562,7 @@ protected function createCustomRenderer() $fileHandle = @fopen($fileName, 'r', true); if (is_resource($fileHandle) === false) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( sprintf( 'Can\'t find the custom report class: %s', $this->reportFormat @@ -585,6 +614,7 @@ public function usage() 'even if any violations are found' . \PHP_EOL . '--generate-baseline: will generate a phpmd.baseline.xml next ' . 'to the first ruleset file location' . \PHP_EOL . + '--update-baseline: will remove any non-existing violations from the phpmd.baseline.xml' . \PHP_EOL . '--baseline-file: a custom location of the baseline file' . \PHP_EOL; } @@ -599,6 +629,7 @@ protected function getListOfAvailableRenderers() $renderers = array(); foreach (scandir($renderersDirPathName) as $rendererFileName) { + $rendererName = array(); if (preg_match('/^(\w+)Renderer.php$/i', $rendererFileName, $rendererName)) { $renderers[] = strtolower($rendererName[1]); } @@ -639,7 +670,7 @@ protected function logDeprecated($deprecatedName, $newName) * * @param string $inputFile Specified input file name. * @return string - * @throws \InvalidArgumentException If the specified input file does not exist. + * @throws InvalidArgumentException If the specified input file does not exist. * @since 1.1.0 */ protected function readInputFile($inputFile) @@ -647,6 +678,6 @@ protected function readInputFile($inputFile) if (file_exists($inputFile)) { return implode(',', array_map('trim', file($inputFile))); } - throw new \InvalidArgumentException("Input file '{$inputFile}' not exists."); + throw new InvalidArgumentException("Input file '{$inputFile}' not exists."); } } diff --git a/src/main/php/PHPMD/Writer/StreamWriter.php b/src/main/php/PHPMD/Writer/StreamWriter.php index 310e08232..6dce37ace 100644 --- a/src/main/php/PHPMD/Writer/StreamWriter.php +++ b/src/main/php/PHPMD/Writer/StreamWriter.php @@ -18,6 +18,7 @@ namespace PHPMD\Writer; use PHPMD\AbstractWriter; +use RuntimeException; /** * This writer uses PHP's stream api as its output target. @@ -35,7 +36,7 @@ class StreamWriter extends AbstractWriter * Constructs a new stream writer instance. * * @param resource|string $streamResourceOrUri - * @throws \RuntimeException If the output directory cannot be found. + * @throws RuntimeException If the output directory cannot be found. */ public function __construct($streamResourceOrUri) { @@ -50,7 +51,7 @@ public function __construct($streamResourceOrUri) } if (file_exists($dirName) === false) { $message = 'Cannot find output directory "' . $dirName . '".'; - throw new \RuntimeException($message); + throw new RuntimeException($message); } $this->stream = fopen($streamResourceOrUri, 'wb'); diff --git a/src/site/resources/web/junit.xslt b/src/site/resources/web/junit.xslt new file mode 100644 index 000000000..bc4e321e4 --- /dev/null +++ b/src/site/resources/web/junit.xslt @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lines: + + - + + + + + + + diff --git a/src/site/rst/documentation/index.rst b/src/site/rst/documentation/index.rst index b3587eac5..ccd9568e5 100644 --- a/src/site/rst/documentation/index.rst +++ b/src/site/rst/documentation/index.rst @@ -64,9 +64,12 @@ Command line options - ``--ignore-violations-on-exit`` - will exit with a zero code, even if any violations are found. - - ``--generate-baseline`` - will generate a phpmd.baseline.xml for existing violations + - ``--generate-baseline`` - will generate a ``phpmd.baseline.xml`` for existing violations next to the ruleset definition file. + - ``--update-baseline`` - will remove all violations from an existing ``phpmd.baseline.xml`` + that no longer exist. New violations will _not_ be added. + - ``--baseline-file`` - the filepath to a custom baseline xml file. The filepath of all baselined files must be relative to this file location. @@ -123,11 +126,17 @@ At the moment PHPMD comes with the following five renderers: - *xml*, which formats the report as XML. - *text*, simple textual format. -- *ansi*, colorful, formated text for the command line. +- *ansi*, colorful, formatted text for the command line. - *html*, single HTML file with possible problems. - *json*, formats JSON report. - *github*, a format that GitHub Actions understands (see `CI Integration `_). +Some more formats can be obtained by conversion such as: + +*junit* can be obtained using `xsltproc` package on the Debian-based systems or `libxslt` on Alpine and CentOS. with this given `junit.xslt config file `_:: + + ~ $ phpmd src xml cleancode | xsltproc junit.xslt - + Baseline ========= @@ -144,3 +153,7 @@ To specify a custom baseline filepath for export:: By default PHPMD will look next to ``phpmd.xml`` for ``phpmd.baseline.xml``. To overwrite this behaviour:: ~ $ phpmd /path/to/source text phpmd.xml --baseline-file /path/to/source/phpmd.baseline.xml + +To clean up an existing baseline file and *only remove* no longer existing violations:: + + ~ $ phpmd /path/to/source text phpmd.xml --update-baseline diff --git a/src/site/rst/rules/cleancode.rst b/src/site/rst/rules/cleancode.rst index 498f92ca6..422e445db 100644 --- a/src/site/rst/rules/cleancode.rst +++ b/src/site/rst/rules/cleancode.rst @@ -134,11 +134,11 @@ Example: :: This rule has the following properties: -+-----------------------------------+---------------+------------------------------------------------------------+ -| Name | Default Value | Description | -+===================================+===============+============================================================+ -| ignore-global | | Ignore classes, interfaces and traits in the global namespace | -+-----------------------------------+---------------+------------------------------------------------------------+ ++-----------------------------------+---------------+---------------------------------------------------------------+ +| Name | Default Value | Description | ++===================================+===============+===============================================================+ +| ignore-global | false | Ignore classes, interfaces and traits in the global namespace | ++-----------------------------------+---------------+---------------------------------------------------------------+ UndefinedVariable ================= diff --git a/src/site/rst/rules/index.rst b/src/site/rst/rules/index.rst index 2af522c53..6d25f2f60 100644 --- a/src/site/rst/rules/index.rst +++ b/src/site/rst/rules/index.rst @@ -18,7 +18,7 @@ Clean Code Rules - `ElseExpression `_: An if expression with an else branch is basically not necessary. You can rewrite the conditions in a way that the else clause is not necessary and the code becomes simpler to read. To achieve this, use early return statements, though you may need to split the code in several smaller methods. For very simple assignments you could also use the ternary operations. - `StaticAccess `_: Static access causes unexchangeable dependencies to other classes and leads to hard to test code. Avoid using static access at all costs and instead inject dependencies through the constructor. The only case when static access is acceptable is when used for factory methods. - `IfStatementAssignment `_: Assignments in if clauses and the like are considered a code smell. Assignments in PHP return the right operand as their result. In many cases, this is an expected behavior, but can lead to many difficult to spot bugs, especially when the right operand could result in zero, null or an empty string. -- `DuplicateArrayKey `_: Defining another value for the same key in an array literal overrides the previous key/value, which makes it effectively an unused code. If it's known from the beginning that the key will have different value, there is usually no point in defining first one. +- `DuplicatedArrayKey `_: Defining another value for the same key in an array literal overrides the previous key/value, which makes it effectively an unused code. If it's known from the beginning that the key will have different value, there is usually no point in defining first one. - `MissingImport `_: Importing all external classes in a file through use statements makes them clearly visible. - `UndefinedVariable `_: Detects when a variable is used that has not been defined before. - `ErrorControlOperator `_: Error suppression should be avoided if possible as it doesn't just suppress the error, that you are trying to stop, but will also suppress errors that you didn't predict would ever occur. Moreover it can slow down the execution of your code. Consider changing error_reporting() level and/or setting up your own error handler. diff --git a/src/test/php/PHPMD/AbstractStaticTest.php b/src/test/php/PHPMD/AbstractStaticTest.php index 1037d736c..31991f549 100644 --- a/src/test/php/PHPMD/AbstractStaticTest.php +++ b/src/test/php/PHPMD/AbstractStaticTest.php @@ -17,6 +17,7 @@ namespace PHPMD; +use Closure; use PHPUnit_Framework_TestCase; /** @@ -66,6 +67,8 @@ protected static function returnToOriginalWorkingDirectory() */ protected static function cleanupTempFiles() { + // cleanup any open resources on temp files + gc_collect_cycles(); foreach (self::$tempFiles as $tempFile) { unlink($tempFile); } @@ -150,21 +153,27 @@ public static function assertXmlEquals($actualOutput, $expectedFileName) * * @param string $actualOutput Generated JSON output. * @param string $expectedFileName File with expected JSON result. + * @param bool|Closure $removeDynamicValues If set to `false`, the actual output is not normalized, + * if set to a closure, the closure is applied on the actual output array. * * @return void */ - public static function assertJsonEquals($actualOutput, $expectedFileName) + public static function assertJsonEquals($actualOutput, $expectedFileName, $removeDynamicValues = true) { $actual = json_decode($actualOutput, true); // Remove dynamic timestamp and duration attribute - if (isset($actual['timestamp'])) { - $actual['timestamp'] = ''; - } - if (isset($actual['duration'])) { - $actual['duration'] = ''; - } - if (isset($actual['version'])) { - $actual['version'] = '@package_version@'; + if ($removeDynamicValues === true) { + if (isset($actual['timestamp'])) { + $actual['timestamp'] = ''; + } + if (isset($actual['duration'])) { + $actual['duration'] = ''; + } + if (isset($actual['version'])) { + $actual['version'] = '@package_version@'; + } + } elseif ($removeDynamicValues instanceof Closure) { + $actual = $removeDynamicValues($actual); } $expected = str_replace( @@ -173,6 +182,7 @@ public static function assertJsonEquals($actualOutput, $expectedFileName) file_get_contents(self::createFileUri($expectedFileName)) ); + $expected = str_replace('#{workingDirectory}', getcwd(), $expected); $expected = str_replace('_DS_', DIRECTORY_SEPARATOR, $expected); self::assertJsonStringEqualsJsonString($expected, json_encode($actual)); @@ -235,11 +245,17 @@ protected static function createFileUri($localPath = '') /** * Creates a file uri for a temporary test file. * + * @param string|null $fileName * @return string */ - protected static function createTempFileUri() + protected static function createTempFileUri($fileName = null) { - return (self::$tempFiles[] = tempnam(sys_get_temp_dir(), 'phpmd.')); + if ($fileName !== null) { + $filePath = sys_get_temp_dir() . '/' . $fileName; + } else { + $filePath = tempnam(sys_get_temp_dir(), 'phpmd.'); + } + return (self::$tempFiles[] = $filePath); } /** diff --git a/src/test/php/PHPMD/Baseline/BaselineFileFinderTest.php b/src/test/php/PHPMD/Baseline/BaselineFileFinderTest.php index 2374a88a1..97452c8d0 100644 --- a/src/test/php/PHPMD/Baseline/BaselineFileFinderTest.php +++ b/src/test/php/PHPMD/Baseline/BaselineFileFinderTest.php @@ -31,10 +31,12 @@ public function testShouldFindExistingFileNearRuleSet() { $args = array('script', 'source', 'xml', static::createResourceUriForTest('testA/phpmd.xml')); $finder = new BaselineFileFinder(new CommandLineOptions($args)); - static::assertSame( - realpath(static::createResourceUriForTest('testA/phpmd.baseline.xml')), - $finder->existingFile()->find() - ); + + // ensure consistent slashes + $expected = str_replace("\\", "/", realpath(static::createResourceUriForTest('testA/phpmd.baseline.xml'))); + $actual = str_replace("\\", "/", $finder->existingFile()->find()); + + static::assertSame($expected, $actual); } /** diff --git a/src/test/php/PHPMD/Baseline/BaselineValidatorTest.php b/src/test/php/PHPMD/Baseline/BaselineValidatorTest.php new file mode 100644 index 000000000..12caac3b4 --- /dev/null +++ b/src/test/php/PHPMD/Baseline/BaselineValidatorTest.php @@ -0,0 +1,66 @@ +getMockFromBuilder( + $this->getMockBuilder('\PHPMD\Rule')->disableOriginalConstructor() + ); + $this->violation = $this->getMockFromBuilder( + $this->getMockBuilder('\PHPMD\RuleViolation')->disableOriginalConstructor() + ); + $this->violation + ->method('getRule') + ->willReturn($rule); + $this->baselineSet = $this->getMockFromBuilder( + $this->getMockBuilder('\PHPMD\Baseline\BaselineSet')->disableOriginalConstructor() + ); + } + + /** + * @covers ::isBaselined + * @dataProvider dataProvider + * @param bool $contains + * @param string $baselineMode + * @param bool $isBaselined + */ + public function testIsBaselined($contains, $baselineMode, $isBaselined) + { + $this->baselineSet->method('contains')->willReturn($contains); + $validator = new BaselineValidator($this->baselineSet, $baselineMode); + static::assertSame($isBaselined, $validator->isBaselined($this->violation)); + } + + /** + * @return array + */ + public function dataProvider() + { + return array( + 'contains: true, mode: none' => array(true, BaselineMode::NONE, true), + 'contains: false, mode: none' => array(false, BaselineMode::NONE, false), + 'contains: true, mode: update' => array(true, BaselineMode::UPDATE, false), + 'contains: false, mode: update' => array(false, BaselineMode::UPDATE, true), + 'contains: true, mode: generate' => array(true, BaselineMode::GENERATE, false), + 'contains: false, mode: generate' => array(false, BaselineMode::GENERATE, false), + ); + } +} diff --git a/src/test/php/PHPMD/PHPMDTest.php b/src/test/php/PHPMD/PHPMDTest.php index f41a5f43c..c9d01b1f6 100644 --- a/src/test/php/PHPMD/PHPMDTest.php +++ b/src/test/php/PHPMD/PHPMDTest.php @@ -17,7 +17,9 @@ namespace PHPMD; +use PHPMD\Baseline\BaselineMode; use PHPMD\Baseline\BaselineSet; +use PHPMD\Baseline\BaselineValidator; use PHPMD\Renderer\XMLRenderer; use PHPMD\Stubs\WriterStub; use PHPUnit_Framework_MockObject_MockObject; @@ -49,7 +51,8 @@ public function testRunWithDefaultSettingsAndXmlRenderer() self::createFileUri('source/ccn_function.php'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertXmlEquals($writer->getData(), 'pmd/default-xml.xml'); @@ -74,7 +77,8 @@ public function testRunWithDefaultSettingsAndXmlRendererAgainstDirectory() self::createFileUri('source'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertXmlEquals($writer->getData(), 'pmd/single-directory.xml'); @@ -99,7 +103,8 @@ public function testRunWithDefaultSettingsAndXmlRendererAgainstSingleFile() self::createFileUri('source/ccn_function.php'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertXmlEquals($writer->getData(), 'pmd/single-file.xml'); @@ -144,7 +149,8 @@ public function testHasViolationsReturnsFalseForSourceWithoutViolations() self::createFileUri('source/source_without_violations.php'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertFalse($phpmd->hasErrors()); @@ -168,7 +174,8 @@ public function testHasViolationsReturnsTrueForSourceWithViolation() self::createFileUri('source/source_with_npath_violation.php'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertFalse($phpmd->hasErrors()); @@ -197,7 +204,7 @@ public function testHasViolationsReturnsFalseWhenViolationIsBaselined() 'pmd-refset1', array($renderer), new RuleSetFactory(), - $baselineSet + new Report(new BaselineValidator($baselineSet, BaselineMode::NONE)) ); static::assertFalse($phpmd->hasViolations()); @@ -220,7 +227,8 @@ public function testHasErrorsReturnsTrueForSourceWithError() self::createFileUri('source/source_with_parse_error.php'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertTrue($phpmd->hasErrors()); @@ -243,7 +251,8 @@ public function testIgnorePattern() self::createFileUri('sourceExcluded/'), 'pmd-refset1', array(), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertFalse($phpmd->hasErrors()); @@ -254,7 +263,8 @@ public function testIgnorePattern() self::createFileUri('sourceExcluded/'), 'exclude-pattern', array(), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); $this->assertFalse($phpmd->hasErrors()); diff --git a/src/test/php/PHPMD/Regression/AcceptsFilesAndDirectoriesAsInputTicket001Test.php b/src/test/php/PHPMD/Regression/AcceptsFilesAndDirectoriesAsInputTicket001Test.php index c8898be7a..9c1c913f1 100644 --- a/src/test/php/PHPMD/Regression/AcceptsFilesAndDirectoriesAsInputTicket001Test.php +++ b/src/test/php/PHPMD/Regression/AcceptsFilesAndDirectoriesAsInputTicket001Test.php @@ -19,6 +19,7 @@ use PHPMD\PHPMD; use PHPMD\Renderer\XMLRenderer; +use PHPMD\Report; use PHPMD\RuleSetFactory; use PHPMD\Stubs\WriterStub; @@ -46,7 +47,8 @@ public function testCliAcceptsDirectoryAsInput() self::createFileUri('source'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); } @@ -67,7 +69,8 @@ public function testCliAcceptsSingleFileAsInput() self::createFileUri('source/FooBar.php'), 'pmd-refset1', array($renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); } } diff --git a/src/test/php/PHPMD/Regression/ExcessivePublicCountWorksCorrectlyWithStaticMethodsTest.php b/src/test/php/PHPMD/Regression/ExcessivePublicCountWorksCorrectlyWithStaticMethodsTest.php index 30a0bb32c..5c2ccf14a 100644 --- a/src/test/php/PHPMD/Regression/ExcessivePublicCountWorksCorrectlyWithStaticMethodsTest.php +++ b/src/test/php/PHPMD/Regression/ExcessivePublicCountWorksCorrectlyWithStaticMethodsTest.php @@ -87,7 +87,8 @@ function (Report $report) use ($self) { __DIR__ . '/Sources/ExcessivePublicCountWorksForPublicStaticMethods.php', 'codesize', array($this->renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); } @@ -127,7 +128,8 @@ function (Report $report) use ($self) { __DIR__ . '/Sources/ExcessivePublicCountSuppressionWorksForPublicStaticMethods.php', 'codesize', array($this->renderer), - new RuleSetFactory() + new RuleSetFactory(), + new Report() ); } } diff --git a/src/test/php/PHPMD/Regression/MaximumNestingLevelTicket24975295Test.php b/src/test/php/PHPMD/Regression/MaximumNestingLevelTicket24975295Test.php index 7afa4d7f6..10bdc932c 100644 --- a/src/test/php/PHPMD/Regression/MaximumNestingLevelTicket24975295Test.php +++ b/src/test/php/PHPMD/Regression/MaximumNestingLevelTicket24975295Test.php @@ -19,6 +19,7 @@ use PHPMD\PHPMD; use PHPMD\Renderer\TextRenderer; +use PHPMD\Report; use PHPMD\RuleSetFactory; use PHPMD\Writer\StreamWriter; @@ -49,6 +50,6 @@ public function testLocalVariableUsedInDoubleQuoteStringGetsNotReported() $factory = new RuleSetFactory(); $phpmd = new PHPMD(); - $phpmd->processFiles($inputs, $rules, $renderes, $factory); + $phpmd->processFiles($inputs, $rules, $renderes, $factory, new Report()); } } diff --git a/src/test/php/PHPMD/Renderer/AnsiRendererTest.php b/src/test/php/PHPMD/Renderer/AnsiRendererTest.php index 95ac53f5d..61ec09c5f 100644 --- a/src/test/php/PHPMD/Renderer/AnsiRendererTest.php +++ b/src/test/php/PHPMD/Renderer/AnsiRendererTest.php @@ -73,6 +73,7 @@ public function testRendererOutputsForReportWithContents() $expectedChunks = array( PHP_EOL . "FILE: /bar.php" . PHP_EOL . "--------------" . PHP_EOL, " 1 | \e[31mVIOLATION\e[0m | Test description" . PHP_EOL, + PHP_EOL, PHP_EOL . "FILE: /foo.php" . PHP_EOL . "--------------" . PHP_EOL, " 2 | \e[31mVIOLATION\e[0m | Test description" . PHP_EOL, " 3 | \e[31mVIOLATION\e[0m | Test description" . PHP_EOL, diff --git a/src/test/php/PHPMD/Renderer/CheckStyleRendererTest.php b/src/test/php/PHPMD/Renderer/CheckStyleRendererTest.php new file mode 100644 index 000000000..16e6f2777 --- /dev/null +++ b/src/test/php/PHPMD/Renderer/CheckStyleRendererTest.php @@ -0,0 +1,105 @@ +. + * All rights reserved. + * + * Licensed under BSD License + * For full copyright and license information, please see the LICENSE file. + * Redistributions of files must retain the above copyright notice. + * + * @author Manuel Pichler + * @copyright Manuel Pichler. All rights reserved. + * @license https://opensource.org/licenses/bsd-license.php BSD License + * @link http://phpmd.org/ + */ + +namespace PHPMD\Renderer; + +use PHPMD\AbstractTest; +use PHPMD\ProcessingError; +use PHPMD\Stubs\WriterStub; + +/** + * Test case for the xml renderer implementation. + * + * @covers \PHPMD\Renderer\XMLRenderer + */ +class CheckStyleRendererTest extends AbstractTest +{ + /** + * testRendererCreatesExpectedNumberOfXmlElements + * + * @return void + */ + public function testRendererCreatesExpectedNumberOfXmlElements() + { + // Create a writer instance. + $writer = new WriterStub(); + + $violations = array( + $this->getRuleViolationMock('/bar.php'), + $this->getRuleViolationMock('/foo.php'), + $this->getRuleViolationMock('/foo.php', 23, 42, null, 'foo getReportWithNoViolation(); + $report->expects($this->once()) + ->method('getRuleViolations') + ->will($this->returnValue(new \ArrayIterator($violations))); + $report->expects($this->once()) + ->method('getErrors') + ->will($this->returnValue(new \ArrayIterator(array()))); + + $renderer = new XMLRenderer(); + $renderer->setWriter($writer); + + $renderer->start(); + $renderer->renderReport($report); + $renderer->end(); + + $this->assertXmlEquals( + $writer->getData(), + 'renderer/xml_renderer_expected1.xml' + ); + } + + /** + * testRendererAddsProcessingErrorsToXmlReport + * + * @return void + * @since 1.2.1 + */ + public function testRendererAddsProcessingErrorsToXmlReport() + { + // Create a writer instance. + $writer = new WriterStub(); + + $processingErrors = array( + new ProcessingError('Failed for file "/tmp/foo.php".'), + new ProcessingError('Failed for file "/tmp/bar.php".'), + new ProcessingError('Failed for file "/tmp/baz.php".'), + ); + + $report = $this->getReportWithNoViolation(); + $report->expects($this->once()) + ->method('getRuleViolations') + ->will($this->returnValue(new \ArrayIterator(array()))); + $report->expects($this->once()) + ->method('getErrors') + ->will($this->returnValue(new \ArrayIterator($processingErrors))); + + $renderer = new XMLRenderer(); + $renderer->setWriter($writer); + + $renderer->start(); + $renderer->renderReport($report); + $renderer->end(); + + $this->assertXmlEquals( + $writer->getData(), + 'renderer/xml_renderer_processing_errors.xml' + ); + } +} diff --git a/src/test/php/PHPMD/Renderer/SARIFRendererTest.php b/src/test/php/PHPMD/Renderer/SARIFRendererTest.php new file mode 100644 index 000000000..0d54b45fd --- /dev/null +++ b/src/test/php/PHPMD/Renderer/SARIFRendererTest.php @@ -0,0 +1,122 @@ +. + * All rights reserved. + * + * Licensed under BSD License + * For full copyright and license information, please see the LICENSE file. + * Redistributions of files must retain the above copyright notice. + * + * @author Lukas Bestle + * @copyright Manuel Pichler. All rights reserved. + * @license https://opensource.org/licenses/bsd-license.php BSD License + * @link http://phpmd.org/ + */ + +namespace PHPMD\Renderer; + +use PHPMD\AbstractTest; +use PHPMD\ProcessingError; +use PHPMD\Stubs\RuleStub; +use PHPMD\Stubs\WriterStub; + +/** + * Test case for the SARIF renderer implementation. + * + * @covers \PHPMD\Renderer\SARIFRenderer + */ +class SARIFRendererTest extends AbstractTest +{ + /** + * testRendererCreatesExpectedNumberOfJsonElements + * + * @return void + */ + public function testRendererCreatesExpectedNumberOfJsonElements() + { + $writer = new WriterStub(); + + $rule = new RuleStub('AnotherRuleStub'); + $rule->addExample(" class Example\n{\n}\n "); + $rule->addExample("\nclass AnotherExample\n{\n public \$var;\n}\n "); + $rule->setSince(null); + + $complexRuleViolationMock = $this->getRuleViolationMock(getcwd() . '/src/foobar.php', 23, 42, $rule); + $complexRuleViolationMock + ->method('getArgs') + ->willReturn(array(123, 3.2, 'awesomeFunction()')); + + $violations = array( + $this->getRuleViolationMock('/bar.php'), + $this->getRuleViolationMock('/foo.php'), + $complexRuleViolationMock, + ); + + $report = $this->getReportWithNoViolation(); + $report->expects($this->once()) + ->method('getRuleViolations') + ->will($this->returnValue(new \ArrayIterator($violations))); + $report->expects($this->once()) + ->method('getErrors') + ->will($this->returnValue(new \ArrayIterator(array()))); + + $renderer = new SARIFRenderer(); + $renderer->setWriter($writer); + + $renderer->start(); + $renderer->renderReport($report); + $renderer->end(); + + $this->assertJsonEquals( + $writer->getData(), + 'renderer/sarif_renderer_expected.sarif', + function ($actual) { + $actual['runs'][0]['tool']['driver']['version'] = '@package_version@'; + return $actual; + } + ); + } + + /** + * testRendererAddsProcessingErrorsToJsonReport + * + * @return void + */ + public function testRendererAddsProcessingErrorsToJsonReport() + { + $writer = new WriterStub(); + + $processingErrors = array( + new ProcessingError('Failed for file "/tmp/foo.php".'), + new ProcessingError('Failed for file "/tmp/bar.php".'), + new ProcessingError('Failed for file "' . static::createFileUri('foobar.php') . '".'), + new ProcessingError('Cannot read file "/tmp/foo.php". Permission denied.'), + ); + + $report = $this->getReportWithNoViolation(); + $report->expects($this->once()) + ->method('getRuleViolations') + ->will($this->returnValue(new \ArrayIterator(array()))); + $report->expects($this->once()) + ->method('getErrors') + ->will($this->returnValue(new \ArrayIterator($processingErrors))); + + $renderer = new SARIFRenderer(); + $renderer->setWriter($writer); + + $renderer->start(); + $renderer->renderReport($report); + $renderer->end(); + + $this->assertJsonEquals( + $writer->getData(), + 'renderer/sarif_renderer_processing_errors.sarif', + function ($actual) { + $actual['runs'][0]['tool']['driver']['version'] = '@package_version@'; + return $actual; + } + ); + } +} diff --git a/src/test/php/PHPMD/ReportTest.php b/src/test/php/PHPMD/ReportTest.php index cb86184fa..2ce027427 100644 --- a/src/test/php/PHPMD/ReportTest.php +++ b/src/test/php/PHPMD/ReportTest.php @@ -17,7 +17,9 @@ namespace PHPMD; +use PHPMD\Baseline\BaselineMode; use PHPMD\Baseline\BaselineSet; +use PHPMD\Baseline\BaselineValidator; use PHPMD\Baseline\ViolationBaseline; /** @@ -205,7 +207,7 @@ public function testReportShouldIgnoreBaselineViolation() $baseline->addEntry($violation); // setup report - $report = new Report($baseline); + $report = new Report(new BaselineValidator($baseline, BaselineMode::NONE)); $report->addRuleViolation($ruleA); $report->addRuleViolation($ruleB); @@ -214,4 +216,30 @@ public function testReportShouldIgnoreBaselineViolation() static::assertCount(1, $violations); static::assertSame($ruleB, $violations[0]); } + + /** + * @return void + */ + public function testReportShouldIgnoreNewViolationsOnBaselineUpdate() + { + /** @var RuleViolation $ruleA */ + $ruleA = $this->getRuleViolationMock('foo.txt'); + /** @var RuleViolation $ruleB */ + $ruleB = $this->getRuleViolationMock('bar.txt', 1, 2); + + // setup baseline + $violation = new ViolationBaseline(get_class($ruleA->getRule()), 'foo.txt', null); + $baseline = new BaselineSet(); + $baseline->addEntry($violation); + + // setup report + $report = new Report(new BaselineValidator($baseline, BaselineMode::UPDATE)); + $report->addRuleViolation($ruleA); + $report->addRuleViolation($ruleB); + + // only expect ruleA, as ruleB is new and should not be in the report. + $violations = $report->getRuleViolations(); + static::assertCount(1, $violations); + static::assertSame($ruleA, $violations[0]); + } } diff --git a/src/test/php/PHPMD/Rule/CleanCode/MissingImportTest.php b/src/test/php/PHPMD/Rule/CleanCode/MissingImportTest.php index c6597d1c7..a6d688945 100644 --- a/src/test/php/PHPMD/Rule/CleanCode/MissingImportTest.php +++ b/src/test/php/PHPMD/Rule/CleanCode/MissingImportTest.php @@ -34,7 +34,7 @@ class MissingImportTest extends AbstractTest public function getRule() { $rule = new MissingImport(); - $rule->addProperty('ignore-global', false); + $rule->addProperty('ignore-global', 'false'); return $rule; } @@ -77,16 +77,25 @@ public function testRuleAppliesTwiceToClassWithNotImportedDependencies() } /** - * Tests the rule ignores classes in global namespace with `ignore-global`. + * Tests that it does not apply to a class in root namespace when configured. * - * @param string $file The test file to test against. * @return void - * @dataProvider getApplyingCases + * @covers ::apply + * @covers ::isGlobalNamespace */ - public function testRuleDoesNotApplyWithIgnoreGlobalProperty($file) + public function testRuleDoesNotApplyWhenSuppressed() { - $rule = $this->getRule(); - $rule->addProperty('ignore-global', true); - $this->expectRuleHasViolationsForFile($rule, static::NO_VIOLATION, $file); + $rule = new MissingImport(); + $rule->addProperty('ignore-global', 'true'); + $files = $this->getFilesForCalledClass('testRuleAppliesTo*'); + foreach ($files as $file) { + // Covers case when the new property is set and the rule *should* apply. + if (strpos($file, 'WithNotImportedDeepDependencies')) { + $this->expectRuleHasViolationsForFile($rule, static::ONE_VIOLATION, $file); + continue; + } + // Covers case when the new property is set and the rule *should not* apply. + $this->expectRuleHasViolationsForFile($rule, static::NO_VIOLATION, $file); + } } } diff --git a/src/test/php/PHPMD/Stubs/RuleStub.php b/src/test/php/PHPMD/Stubs/RuleStub.php index 14f35e746..87624be94 100644 --- a/src/test/php/PHPMD/Stubs/RuleStub.php +++ b/src/test/php/PHPMD/Stubs/RuleStub.php @@ -41,6 +41,7 @@ public function __construct($ruleName = 'RuleStub', $ruleSetName = 'TestRuleSet' $this->setRuleSetName($ruleSetName); $this->setSince('42.23'); $this->setDescription('Simple rule stub'); + $this->setMessage('Test description'); } /** diff --git a/src/test/php/PHPMD/TextUI/CommandLineOptionsTest.php b/src/test/php/PHPMD/TextUI/CommandLineOptionsTest.php index 7186df81f..c03c2eed4 100644 --- a/src/test/php/PHPMD/TextUI/CommandLineOptionsTest.php +++ b/src/test/php/PHPMD/TextUI/CommandLineOptionsTest.php @@ -18,6 +18,7 @@ namespace PHPMD\TextUI; use PHPMD\AbstractTest; +use PHPMD\Baseline\BaselineMode; use PHPMD\Rule; /** @@ -275,7 +276,10 @@ public function testCliUsageContainsAutoDiscoveredRenderers() $args = array(__FILE__, __FILE__, 'text', 'codesize'); $opts = new CommandLineOptions($args); - $this->assertContains('Available formats: ansi, baseline, github, html, json, text, xml.', $opts->usage()); + $this->assertContains( + 'Available formats: ansi, baseline, checkstyle, github, html, json, sarif, text, xml.', + $opts->usage() + ); } /** @@ -348,7 +352,7 @@ public function testCliOptionGenerateBaselineFalseByDefault() { $args = array(__FILE__, __FILE__, 'text', 'codesize'); $opts = new CommandLineOptions($args); - static::assertFalse($opts->generateBaseline()); + static::assertSame(BaselineMode::NONE, $opts->generateBaseline()); } /** @@ -358,7 +362,17 @@ public function testCliOptionGenerateBaselineShouldBeSet() { $args = array(__FILE__, __FILE__, 'text', 'codesize', '--generate-baseline'); $opts = new CommandLineOptions($args); - static::assertTrue($opts->generateBaseline()); + static::assertSame(BaselineMode::GENERATE, $opts->generateBaseline()); + } + + /** + * @return void + */ + public function testCliOptionUpdateBaselineShouldBeSet() + { + $args = array(__FILE__, __FILE__, 'text', 'codesize', '--update-baseline'); + $opts = new CommandLineOptions($args); + static::assertSame(BaselineMode::UPDATE, $opts->generateBaseline()); } /** diff --git a/src/test/php/PHPMD/TextUI/CommandTest.php b/src/test/php/PHPMD/TextUI/CommandTest.php index 49e1906f0..dfa789872 100644 --- a/src/test/php/PHPMD/TextUI/CommandTest.php +++ b/src/test/php/PHPMD/TextUI/CommandTest.php @@ -160,6 +160,10 @@ public function testWithMultipleReportFiles() $text = self::createTempFileUri(), '--reportfile-json', $json = self::createTempFileUri(), + '--reportfile-checkstyle', + $checkstyle = self::createTempFileUri(), + '--reportfile-sarif', + $sarif = self::createTempFileUri(), ); Command::main($args); @@ -168,6 +172,8 @@ public function testWithMultipleReportFiles() $this->assertFileExists($html); $this->assertFileExists($text); $this->assertFileExists($json); + $this->assertFileExists($checkstyle); + $this->assertFileExists($sarif); } public function testOutput() @@ -226,7 +232,7 @@ public function dataProviderWithFilter() public function testMainGenerateBaseline() { - $uri = realpath(self::createFileUri('source/source_with_anonymous_class.php')); + $uri = str_replace("\\", "/", realpath(self::createFileUri('source/source_with_anonymous_class.php'))); $temp = self::createTempFileUri(); $exitCode = Command::main(array( __FILE__, @@ -243,6 +249,39 @@ public function testMainGenerateBaseline() static::assertContains($uri, file_get_contents($temp)); } + /** + * Testcase: + * - Class has existing ShortVariable and new BooleanGetMethodName violations + * - Baseline has ShortVariable and LongClassName baseline violations + * Expect in baseline: + * - LongClassName violation should be removed + * - ShortVariable violation should still exist + * - BooleanGetMethodName shouldn't be added + */ + public function testMainUpdateBaseline() + { + $sourceTemp = self::createTempFileUri('ClassWithMultipleViolations.php'); + $baselineTemp = self::createTempFileUri(); + copy(static::createResourceUriForTest('UpdateBaseline/ClassWithMultipleViolations.php'), $sourceTemp); + copy(static::createResourceUriForTest('UpdateBaseline/phpmd.baseline.xml'), $baselineTemp); + + $exitCode = Command::main(array( + __FILE__, + $sourceTemp, + 'text', + 'naming', + '--update-baseline', + '--baseline-file', + $baselineTemp, + )); + + static::assertSame(Command::EXIT_SUCCESS, $exitCode); + static::assertXmlStringEqualsXmlString( + file_get_contents(static::createResourceUriForTest('UpdateBaseline/expected.baseline.xml')), + file_get_contents($baselineTemp) + ); + } + public function testMainBaselineViolationShouldBeIgnored() { $sourceFile = realpath(static::createResourceUriForTest('Baseline/ClassWithShortVariable.php')); diff --git a/src/test/php/bootstrap.php b/src/test/php/bootstrap.php index c35d6c6f9..21a6da5b0 100644 --- a/src/test/php/bootstrap.php +++ b/src/test/php/bootstrap.php @@ -15,6 +15,55 @@ * @link http://phpmd.org/ */ +$replacements = array( + /** + * Patch phpunit/phpunit-mock-objects Generator.php file to not create double nullable tokens: `??` + */ + __DIR__ . '/../../../vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.php' => array( + array( + "if (version_compare(PHP_VERSION, '7.1', '>=') && " . + "\$parameter->allowsNull() && !\$parameter->isVariadic()) {", + "if (version_compare(PHP_VERSION, '7.1', '>=') && version_compare(PHP_VERSION, '8.0', '<') && " . + "\$parameter->allowsNull() && !\$parameter->isVariadic()) {", + ), + ), + /** + * Fix phpunit/phpunit to not trigger warning on `final private function` + */ + __DIR__ . '/../../../vendor/phpunit/phpunit/src/Util/Configuration.php' => array( + array( + 'final private function', + 'private function', + ), + ), +); + +foreach ($replacements as $file => $patterns) { + echo "$file: "; + + if (!file_exists($file)) { + echo "File not found.\n"; + + continue; + } + + foreach ($patterns as $replacement) { + list($from, $to) = $replacement; + + $contents = @file_get_contents($file) ?: ''; + $newContents = str_replace($from, $to, $contents); + + if ($newContents !== $contents) { + file_put_contents($file, $newContents); + echo "Content changed.\n"; + + continue; + } + + echo "Replace pattern not found.\n"; + } +} + require_once __DIR__ . '/../../../vendor/autoload.php'; spl_autoload_register( diff --git a/src/test/resources/files/Rule/CleanCode/MissingImport/testRuleAppliesToFunctionWithNotImportedDeepDependencies.php b/src/test/resources/files/Rule/CleanCode/MissingImport/testRuleAppliesToFunctionWithNotImportedDeepDependencies.php new file mode 100644 index 000000000..30d556605 --- /dev/null +++ b/src/test/resources/files/Rule/CleanCode/MissingImport/testRuleAppliesToFunctionWithNotImportedDeepDependencies.php @@ -0,0 +1,28 @@ +. + * All rights reserved. + * + * Licensed under BSD License + * For full copyright and license information, please see the LICENSE file. + * Redistributions of files must retain the above copyright notice. + * + * @author Manuel Pichler + * @copyright Manuel Pichler. All rights reserved. + * @license https://opensource.org/licenses/bsd-license.php BSD License + * @link http://phpmd.org/ + */ + +namespace PHPMDTest; + +function testRuleAppliesToFunctionWithNotImportedDeepDependencies() +{ + $bbb = new \PHPMDTest\Nested\MyClass(); +} + +namespace PHPMDTest\Nested; + +class MyClass { +} diff --git a/src/test/resources/files/TextUI/Command/UpdateBaseline/ClassWithMultipleViolations.php b/src/test/resources/files/TextUI/Command/UpdateBaseline/ClassWithMultipleViolations.php new file mode 100644 index 000000000..2f89ee6f4 --- /dev/null +++ b/src/test/resources/files/TextUI/Command/UpdateBaseline/ClassWithMultipleViolations.php @@ -0,0 +1,17 @@ + + + + diff --git a/src/test/resources/files/TextUI/Command/UpdateBaseline/phpmd.baseline.xml b/src/test/resources/files/TextUI/Command/UpdateBaseline/phpmd.baseline.xml new file mode 100644 index 000000000..4385c988f --- /dev/null +++ b/src/test/resources/files/TextUI/Command/UpdateBaseline/phpmd.baseline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/resources/files/renderer/sarif_renderer_expected.sarif b/src/test/resources/files/renderer/sarif_renderer_expected.sarif new file mode 100644 index 000000000..41cea6ca7 --- /dev/null +++ b/src/test/resources/files/renderer/sarif_renderer_expected.sarif @@ -0,0 +1,137 @@ +{ + "version": "2.1.0", + "$schema": "https:\/\/raw.githubusercontent.com\/oasis-tcs\/sarif-spec\/master\/Schemata\/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "PHPMD", + "informationUri": "https:\/\/phpmd.org", + "version": "@package_version@", + "rules": [ + { + "id": "TestRuleSet\/RuleStub", + "name": "RuleStub", + "shortDescription": { + "text": "TestRuleSet: RuleStub" + }, + "messageStrings": { + "default": { + "text": "Test description" + } + }, + "help": { + "text": "Simple rule stub" + }, + "helpUri": "https:\/\/phpmd.org\/rules\/index.html", + "properties": { + "ruleSet": "TestRuleSet", + "priority": 5, + "since": "PHPMD 42.23" + } + }, + { + "id": "TestRuleSet\/AnotherRuleStub", + "name": "AnotherRuleStub", + "shortDescription": { + "text": "TestRuleSet: AnotherRuleStub" + }, + "messageStrings": { + "default": { + "text": "Test description" + } + }, + "help": { + "text": "Simple rule stub", + "markdown": "Simple rule stub\n\n### Example\n\n```php\nclass Example\n{\n}\n```\n\n```php\nclass AnotherExample\n{\n public $var;\n}\n```" + }, + "helpUri": "https:\/\/phpmd.org\/rules\/index.html", + "properties": { + "ruleSet": "TestRuleSet", + "priority": 5 + } + } + ] + } + }, + "originalUriBaseIds": { + "WORKINGDIR": { + "uri": "file:\/\/#{workingDirectory}\/" + } + }, + "results": [ + { + "ruleId": "TestRuleSet\/RuleStub", + "ruleIndex": 0, + "message": { + "id": "default", + "arguments": [], + "text": "Test description" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:\/\/\/bar.php" + }, + "region": { + "startLine": 23, + "endLine": 42 + } + } + } + ] + }, + { + "ruleId": "TestRuleSet\/RuleStub", + "ruleIndex": 0, + "message": { + "id": "default", + "arguments": [], + "text": "Test description" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:\/\/\/foo.php" + }, + "region": { + "startLine": 23, + "endLine": 42 + } + } + } + ] + }, + { + "ruleId": "TestRuleSet\/AnotherRuleStub", + "ruleIndex": 1, + "message": { + "id": "default", + "arguments": [ + "123", + "3.2", + "awesomeFunction()" + ], + "text": "Test description" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/foobar.php", + "uriBaseId": "WORKINGDIR" + }, + "region": { + "startLine": 23, + "endLine": 42 + } + } + } + ] + } + ] + } + ] +} diff --git a/src/test/resources/files/renderer/sarif_renderer_processing_errors.sarif b/src/test/resources/files/renderer/sarif_renderer_processing_errors.sarif new file mode 100644 index 000000000..d8d273cac --- /dev/null +++ b/src/test/resources/files/renderer/sarif_renderer_processing_errors.sarif @@ -0,0 +1,84 @@ +{ + "version": "2.1.0", + "$schema": "https:\/\/raw.githubusercontent.com\/oasis-tcs\/sarif-spec\/master\/Schemata\/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "PHPMD", + "informationUri": "https:\/\/phpmd.org", + "version": "@package_version@", + "rules": [] + } + }, + "originalUriBaseIds": { + "WORKINGDIR": { + "uri": "file:\/\/#{workingDirectory}\/" + } + }, + "results": [ + { + "level": "error", + "message": { + "text": "Failed for file \u0022\/tmp\/foo.php\u0022." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:\/\/\/tmp\/foo.php" + } + } + } + ] + }, + { + "level": "error", + "message": { + "text": "Failed for file \u0022\/tmp\/bar.php\u0022." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:\/\/\/tmp\/bar.php" + } + } + } + ] + }, + { + "level": "error", + "message": { + "text": "Failed for file \u0022#{rootDirectory}\/foobar.php\u0022." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "src\/test\/resources\/files\/foobar.php", + "uriBaseId": "WORKINGDIR" + } + } + } + ] + }, + { + "level": "error", + "message": { + "text": "Cannot read file \u0022\/tmp\/foo.php\u0022. Permission denied." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:\/\/\/tmp\/foo.php" + } + } + } + ] + } + ] + } + ] +}