diff --git a/.github/workflows/build_site.yml b/.github/workflows/build_site.yml new file mode 100644 index 000000000..44cddc2b6 --- /dev/null +++ b/.github/workflows/build_site.yml @@ -0,0 +1,64 @@ +name: Build website + +on: + push: + branches: + - "master" + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [7.4] + dependency-version: [prefer-stable] + + name: Build website + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }} + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Copy files + run: cp src/site/rst/* src/site/resources/web/ -r + + - name: Build website + run: composer build-website + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CNAME: phpmd.org + + - name: Archive generated website + uses: actions/upload-artifact@v2 + with: + name: Website + path: dist/website/* + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist/website/ diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml new file mode 100644 index 000000000..c69b9d498 --- /dev/null +++ b/.github/workflows/codestyle.yml @@ -0,0 +1,59 @@ +name: Codestyle + +on: + push: + branches: + - "*" + paths: + - '**.php' + - 'composer.json' + - 'phpcs.xml.dist' + - '.github/workflows/codestyle.yml' + pull_request: + branches: + - "*" + paths: + - '**.php' + - 'composer.json' + - 'phpcs.xml.dist' + - '.github/workflows/codestyle.yml' + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [7.4] + dependency-version: [prefer-stable] + + name: Codestyle check + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }} + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPCS for the codestyle + run: vendor/bin/phpcs diff --git a/.github/workflows/generate_phar.yml b/.github/workflows/generate_phar.yml new file mode 100644 index 000000000..b8cd79e77 --- /dev/null +++ b/.github/workflows/generate_phar.yml @@ -0,0 +1,68 @@ +name: Generate phar + +on: + push: + tags: + - "*" + branches: + - "*" + release: + types: + - created + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [5.4] + dependency-version: [prefer-stable] + + name: Release phar + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }} + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Git submodules + run: git submodule update --init + + - name: Ant + run: ant package -D-phar:filename=./phpmd.phar && ./phpmd.phar --version + + - name: Archive generated phar + uses: actions/upload-artifact@v2 + with: + name: phpmd.phar + path: phpmd.phar + + - name: Release phpmd.phar + if: github.event_name == 'release' + uses: skx/github-action-publish-binaries@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: 'phpmd.phar' diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 000000000..978734111 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,56 @@ +name: Unit Tests + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [5.3, 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, 7.3, 7.4] + dependency-version: [prefer-lowest, prefer-stable] + exclude: + - dependency-version: prefer-lowest + php: 7.2 + - dependency-version: prefer-lowest + php: 7.3 + - dependency-version: prefer-lowest + php: 7.4 + + name: P${{ matrix.php }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }} + + - name: Install dependencies + run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress + + - name: Execute Unit Tests + run: vendor/bin/phpunit diff --git a/.github/workflows/phpunit_coverage.yml b/.github/workflows/phpunit_coverage.yml new file mode 100644 index 000000000..49936b70b --- /dev/null +++ b/.github/workflows/phpunit_coverage.yml @@ -0,0 +1,57 @@ +name: Unit Tests with coverage + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [7.4] + dependency-version: [prefer-stable] + + name: P${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-${{ matrix.dependency-version }} + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Execute Unit Tests + run: phpdbg -qrr vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml --coverage-html=coverage_html + + - name: Archive code coverage results + uses: actions/upload-artifact@v2 + with: + name: code-coverage-report + path: | + coverage.xml + coverage_html diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa44103b8..000000000 --- a/.travis.yml +++ /dev/null @@ -1,93 +0,0 @@ -language: php - -matrix: - include: - - php: 5.3 - dist: precise - - php: 5.3 - dist: precise - env: DEPENDENCIES=low - - php: 5.4 - dist: trusty - - php: 5.4 - dist: trusty - env: DEPENDENCIES=low - - php: 5.5 - dist: trusty - - php: 5.5 - dist: trusty - env: DEPENDENCIES=low - - php: 5.6 - - php: 5.6 - env: DEPENDENCIES=low - - php: 7.0 - - php: 7.0 - env: DEPENDENCIES=low - - php: 7.1 - - php: 7.1 - env: DEPENDENCIES=low - - php: 7.2 - # Could be enabled when we'll upgrade PHPUnit - # - php: 7.2 - # env: DEPENDENCIES=low - - php: 7.3 - env: COVERAGE=true - # Could be enabled when we'll upgrade PHPUnit - # - php: 7.3 - # env: DEPENDENCIES=low - - php: 7.4 - - php: 5.4 - dist: trusty - env: BUILD_PHAR=true - - php: 7.3 - env: WEBSITE=true - fast_finish: true - -sudo: false - -env: - global: - TEST_CONFIG="phpunit.xml.dist" - -before_script: - - phpenv config-rm xdebug.ini || echo "XDebug is not enabled" - - composer self-update - - if [[ $DEPENDENCIES = low ]]; then composer update --prefer-dist --prefer-lowest --prefer-stable; fi - - if [[ ! $DEPENDENCIES ]]; then composer install; fi - -script: - - if [[ $WEBSITE = 'true' ]]; then cp src/site/rst/* src/site/resources/web/ -r && composer build-website; fi - - if [[ $WEBSITE != 'true' && $BUILD_PHAR != 'true' && $COVERAGE != 'true' ]]; then vendor/bin/phpunit --configuration $TEST_CONFIG --colors; fi - - if [[ $WEBSITE != 'true' && $BUILD_PHAR != 'true' && $COVERAGE = 'true' ]]; then phpdbg -qrr vendor/bin/phpunit --configuration $TEST_CONFIG --colors --coverage-text --coverage-clover=coverage.xml; fi - - if [[ $BUILD_PHAR = 'true' ]]; then git submodule update --init && ant package -D-phar:filename=./phpmd.phar && ./phpmd.phar --version; fi - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/5a993c0b870b2fa9141e # PHPMD Gitter Core Channel - - https://webhooks.gitter.im/e/1c62ab29700f53c70ec5 # PHPMD Gitter Community Channel - on_success: change - on_failure: always - on_start: never - -deploy: -- provider: pages - skip_cleanup: true - github_token: $GITHUB_TOKEN - local_dir: dist/website - on: - branch: master - condition: $WEBSITE -- provider: releases - api_key: $GITHUB_TOKEN - file: phpmd.phar - skip_cleanup: true - on: - tags: true - repo: phpmd/phpmd - condition: "$BUILD_PHAR" - -addons: - snaps: - - name: ant - classic: true diff --git a/CHANGELOG b/CHANGELOG index 6a10a8458..a6b344008 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,42 @@ +phpmd-2.9.1 (2020/09/23) +======================== + +- Fixed #714: Improved static member detection +- Fixed #816: Fixed undefined index referring + +phpmd-2.9.0 (2020/09/02) +======================== + +- Added #496: Added rule for PHP's @ operator +- Added #737: Allowed custom exclusion for StaticAccess by extending the class +- Added #749: Added allow-underscore option for CamelCaseParameterName & CamelCaseVariableName +- Added #747: Long variable subtract suffix +- Added #763 via #765: Added rules LongClassName and ShortClassName +- Fixed #743: Output for version +- Fixed #754: Fixed #720 undefined variable in foreach when passed by reference +- Fixed #764: Fixed #718 Handle anonymous class in "undefined variable" rule +- Fixed #770: Fixed #769 Handle deconstruction assignation for undefined variable +- Fixed #781: Fixed #714 static:: and self:: properties access +- Fixed #784: Fixed #672 Handle passing-by-reference in native PHP functions +- Fixed #793: Fixed #580 Raise UnusedFormalParameter instead UnusedLocalVariable for unused closure parameter +- Fixed #794: Fixed #540 Detect unused variable declared multiple times +- Fixed #805: Fixed #802 Prevent an error with nested arrays +- Fixed #807: Fixed #790 Fix for short variables rule inside foreach statements +- Fixed #809: Fixed #808 Ignore rule path for supression annotation +- Updated different parts of the documentation. #717 #736 #748 #811 +- Changed: #529 : Replaced HTML renderer with new "pretty HTML" renderer +- Changed: #806 : Changed #44 Change private methods to protected in rules. Make rules extendable +- Changed: Internal code improvement #750 #752 #756 #757 #758 #759 #768 #773 #775 #785 #787 #791 #792 +- Deprecated all the PHPMD exceptions that aren't part of the PHPMD\Exceptions namespace. See #775 + +### A potential BC change: +With the clean-up in #768 we have a potential BC break in an unsupported part that we want to give attention for. +> The class aliases ``PHP_PMD_*`` used for PHPMD 1.x backwards PEAR compatibility were removed. If you happen to still depend on these, please adjust your code like so: +> +> From ``PHP_PMD_[Component]_[Class]'`` to ``PHPMD\[Component]\[Class]``, +> as in ``PHP_PMD_Renderer_HTMLRenderer'`` to ``PHPMD\Renderer\HTMLRenderer``. +See #768 + phpmd-2.8.2 (2020/02/24) ======================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0921f53b4..daa623165 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,12 +7,12 @@ 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. -* The code must follow the [PSR-2 coding standard](http://www.php-fig.org/psr/psr-2/). +* The code must follow the [coding standard](https://github.com/phpmd/phpmd/blob/master/phpcs.xml.dist), that is based on [PSR-2 coding standard](http://www.php-fig.org/psr/psr-2/) with additional rules. * All code changes should be covered by unit tests Issues @@ -22,19 +22,20 @@ 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 --------------- -Make sure your code changes comply with the PSR-2 coding standard by +Make sure your code changes comply with the [coding standard](https://github.com/phpmd/phpmd/blob/master/phpcs.xml.dist) by using [PHP Codesniffer](https://github.com/squizlabs/PHP_CodeSniffer) from within your PHPMD folder: - vendor/bin/phpcs -p --extensions=php --standard=PSR2 src > phpcs.txt + vendor/bin/phpcs -p --extensions=php src > phpcs.txt Linux / OS X users may extend this command to exclude files, that are not part of a commit: - vendor/bin/phpcs -p --extensions=php --standard=PSR2 --ignore=src/tests/resources $(git ls-files -om --exclude-standard | grep '\.php$') > phpcs.txt + vendor/bin/phpcs -p --extensions=php --ignore=src/tests/resources $(git ls-files -om --exclude-standard | grep '\.php$') > phpcs.txt Check the ``phpcs.txt`` once it finished. @@ -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 317112c29..d07bca165 100644 --- a/README.rst +++ b/README.rst @@ -127,9 +127,20 @@ Command line options - ``--strict`` - Also report those nodes with a @SuppressWarnings annotation. + - ``--ignore-errors-on-exit`` - will exit with a zero code, even on error. + - ``--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 + 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. + An example command line: :: phpmd PHP/Depend/DbusUI xml codesize --reportfile phpmd.xml --suffixes php,phtml @@ -170,18 +181,21 @@ to create one output for certain parts of your code :: Exit codes ---------- -PHPMD's command line tool currently defines three different exit codes. +PHPMD's command line tool currently defines four different exit codes. - *0*, This exit code indicates that everything worked as expected. This means there was no error/exception and PHPMD hasn't detected any rule violation in the code under test. -- *1*, This exit code indicates that an error/exception occured which has +- *1*, This exit code indicates that an exception occurred which has interrupted PHPMD during execution. - *2*, This exit code means that PHPMD has processed the code under test - without the occurence of an error/exception, but it has detected rule + without the occurrence of an error/exception, but it has detected rule violations in the analyzed source code. You can also prevent this behaviour with the ``--ignore-violations-on-exit`` flag, which will result to a *0* even if any violations are found. +- *3*, This exit code means that one or multiple files under test could not + be processed because of an error. There may also be violations in other + files that could be processed correctly. Renderers --------- @@ -193,6 +207,29 @@ At the moment PHPMD comes with the following renderers: - *html*, single HTML file with possible problems. - *json*, formats JSON report. - *ansi*, a command line friendly format. +- *github*, a format that GitHub Actions understands. +- *sarif*, the Static Analysis Results Interchange 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:: + + ~ $ phpmd /path/to/source text phpmd.xml --generate-baseline + +To specify a custom baseline filepath for export:: + + ~ $ phpmd /path/to/source text phpmd.xml --generate-baseline --baseline-file /path/to/source/phpmd.baseline.xml + +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 PHPMD for enterprise -------------------- diff --git a/build.properties b/build.properties index 1b7adce0c..147fc3cb5 100644 --- a/build.properties +++ b/build.properties @@ -1,7 +1,7 @@ project.dir = project.uri = phpmd.org project.name = phpmd -project.version = 2.8.1 +project.version = 2.9.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 3d9e796f6..557fd87f6 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "minimum-stability": "stable", "require": { "php": ">=5.3.9", - "pdepend/pdepend": "^2.7.1", + "pdepend/pdepend": "^2.8.0", "ext-xml": "*", "composer/xdebug-handler": "^1.0" }, diff --git a/src/main/php/PHPMD/AbstractNode.php b/src/main/php/PHPMD/AbstractNode.php index 3c22b5ff0..50a59e638 100644 --- a/src/main/php/PHPMD/AbstractNode.php +++ b/src/main/php/PHPMD/AbstractNode.php @@ -17,6 +17,7 @@ namespace PHPMD; +use PDepend\Source\AST\AbstractASTArtifact; use PDepend\Source\AST\ASTVariable; use PHPMD\Node\ASTNode; @@ -206,11 +207,17 @@ public function getEndLine() /** * Returns the name of the declaring source file. * - * @return string + * @return string|null */ public function getFileName() { - return (string)$this->node->getCompilationUnit()->getFileName(); + $compilationUnit = $this->node instanceof AbstractASTArtifact + ? $this->node->getCompilationUnit() + : null; + + return $compilationUnit + ? (string)$compilationUnit->getFileName() + : null; // @TODO: Find the name from some parent node https://github.com/phpmd/phpmd/issues/837 } /** diff --git a/src/main/php/PHPMD/AbstractRule.php b/src/main/php/PHPMD/AbstractRule.php index a748f0e84..f8cb4c709 100644 --- a/src/main/php/PHPMD/AbstractRule.php +++ b/src/main/php/PHPMD/AbstractRule.php @@ -314,6 +314,7 @@ protected function getProperty($name, $default = null) if (isset($this->properties[$name])) { return $this->properties[$name]; } + if ($default !== null) { return $default; } @@ -386,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/BaselineFileFinder.php b/src/main/php/PHPMD/Baseline/BaselineFileFinder.php new file mode 100644 index 000000000..677402a54 --- /dev/null +++ b/src/main/php/PHPMD/Baseline/BaselineFileFinder.php @@ -0,0 +1,91 @@ +options = $options; + } + + /** + * The baseline filepath should point to an existing file (or null) + * @return $this + */ + public function existingFile() + { + $this->existingFile = true; + return $this; + } + + /** + * if true, the finder `must` find a file path, but doesn't necessarily exist + * @return $this + */ + public function notNull() + { + $this->notNull = true; + return $this; + } + + /** + * Find the violation baseline file + * + * @return string|null + * @throws RuntimeException + */ + public function find() + { + $file = $this->tryFind(); + if ($file === null && $this->notNull === true) { + throw new RuntimeException('Unable to find the baseline file. Use --baseline-file to specify the filepath'); + } + + return $file; + } + + /** + * Try to find the violation baseline file + * + * @return string|null + * @throws RuntimeException + */ + private function tryFind() + { + // read baseline file from cli arguments + $file = $this->options->baselineFile(); + if ($file !== null) { + return $file; + } + + // find baseline file next to the (first) ruleset + $ruleSets = explode(',', $this->options->getRuleSets()); + $rulePath = realpath($ruleSets[0]); + if ($rulePath === false) { + return null; + } + + // create file path and check for existence + $baselinePath = dirname($rulePath) . '/' . self::DEFAULT_FILENAME; + if ($this->existingFile === true && file_exists($baselinePath) === false) { + return null; + } + + return $baselinePath; + } +} 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 @@ + */ + private $violations = array(); + + public function addEntry(ViolationBaseline $entry) + { + $this->violations[$entry->getRuleName()][] = $entry; + } + + /** + * @param string $ruleName + * @param string $fileName + * @param string|null $methodName + * @return bool + */ + public function contains($ruleName, $fileName, $methodName) + { + if (isset($this->violations[$ruleName]) === false) { + return false; + } + + // normalize slashes in file name + $fileName = str_replace('\\', '/', $fileName); + + foreach ($this->violations[$ruleName] as $baseline) { + if ($baseline->getFileName() === $fileName && $baseline->getMethodName() === $methodName) { + return true; + } + } + + return false; + } +} diff --git a/src/main/php/PHPMD/Baseline/BaselineSetFactory.php b/src/main/php/PHPMD/Baseline/BaselineSetFactory.php new file mode 100644 index 000000000..68bfa5ffd --- /dev/null +++ b/src/main/php/PHPMD/Baseline/BaselineSetFactory.php @@ -0,0 +1,57 @@ +children() as $node) { + if ($node->getName() !== 'violation') { + continue; + } + + if (isset($node['rule']) === false) { + throw new RuntimeException('Missing `rule` attribute in `violation` in ' . $fileName); + } + + if (isset($node['file']) === false) { + throw new RuntimeException('Missing `file` attribute in `violation` in ' . $fileName); + } + + $ruleName = (string)$node['rule']; + $filePath = Paths::concat($basePath, (string)$node['file']); + $methodName = null; + if (isset($node['method']) === true && ((string)$node['method']) !== '') { + $methodName = (string)($node['method']); + } + + $baselineSet->addEntry(new ViolationBaseline($ruleName, $filePath, $methodName)); + } + + return $baselineSet; + } +} diff --git a/src/main/php/PHPMD/Baseline/BaselineValidator.php b/src/main/php/PHPMD/Baseline/BaselineValidator.php new file mode 100644 index 000000000..ea3237289 --- /dev/null +++ b/src/main/php/PHPMD/Baseline/BaselineValidator.php @@ -0,0 +1,47 @@ +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/Baseline/ViolationBaseline.php b/src/main/php/PHPMD/Baseline/ViolationBaseline.php new file mode 100644 index 000000000..de40a8788 --- /dev/null +++ b/src/main/php/PHPMD/Baseline/ViolationBaseline.php @@ -0,0 +1,51 @@ +ruleName = $ruleName; + $this->fileName = $fileName; + $this->methodName = $methodName; + } + + /** + * @return string + */ + public function getRuleName() + { + return $this->ruleName; + } + + /** + * @return string + */ + public function getFileName() + { + return $this->fileName; + } + + /** + * @return string|null + */ + public function getMethodName() + { + return $this->methodName; + } +} diff --git a/src/main/php/PHPMD/Node/Annotation.php b/src/main/php/PHPMD/Node/Annotation.php index 3339371d4..dc3207b95 100644 --- a/src/main/php/PHPMD/Node/Annotation.php +++ b/src/main/php/PHPMD/Node/Annotation.php @@ -80,7 +80,10 @@ private function isSuppressed(Rule $rule) { if (in_array($this->value, array('PHPMD', 'PMD'))) { return true; - } elseif (preg_match('/^(PH)?PMD\.' . $rule->getName() . '/', $this->value)) { + } elseif (preg_match( + '/^(PH)?PMD\.' . preg_replace('/^.*\/([^\/]*)$/', '$1', $rule->getName()) . '/', + $this->value + )) { return true; } diff --git a/src/main/php/PHPMD/PHPMD.php b/src/main/php/PHPMD/PHPMD.php index ff2722143..a61ad52d9 100644 --- a/src/main/php/PHPMD/PHPMD.php +++ b/src/main/php/PHPMD/PHPMD.php @@ -27,6 +27,15 @@ class PHPMD */ const VERSION = '@package_version@'; + /** + * This property will be set to true when an error + * was found in the processed source code. + * + * @var boolean + * @since 2.10.0 + */ + private $errors = false; + /** * List of valid file extensions for analyzed files. * @@ -49,7 +58,7 @@ class PHPMD private $input; /** - * This property will be set to true when an error or a violation + * This property will be set to true when a violation * was found in the processed source code. * * @var boolean @@ -65,6 +74,18 @@ class PHPMD */ private $options = array(); + /** + * This method will return true when the processed source code + * contains errors. + * + * @return boolean + * @since 2.10.0 + */ + public function hasErrors() + { + return $this->errors; + } + /** * This method will return true when the processed source code * contains violations. @@ -114,8 +135,20 @@ public function setFileExtensions(array $fileExtensions) * * @return string[] * @since 0.2.0 + * @deprecated 3.0.0 Use getIgnorePatterns() instead, you always get a list of patterns. */ public function getIgnorePattern() + { + return $this->getIgnorePatterns(); + } + + /** + * Returns an array with string patterns that mark a file path invalid. + * + * @return string[] + * @since 2.9.0 + */ + public function getIgnorePatterns() { return $this->ignorePatterns; } @@ -126,13 +159,29 @@ public function getIgnorePattern() * * @param array $ignorePatterns List of ignore patterns. * @return void + * @deprecated 3.0.0 Use addIgnorePatterns() instead, both will add an not set the patterns. */ public function setIgnorePattern(array $ignorePatterns) + { + $this->addIgnorePatterns($ignorePatterns); + } + + /** + * Add a list of ignore patterns which is used to exclude directories from + * the source analysis. + * + * @param array $ignorePatterns List of ignore patterns. + * @return $this + * @since 2.9.0 + */ + public function addIgnorePatterns(array $ignorePatterns) { $this->ignorePatterns = array_merge( $this->ignorePatterns, $ignorePatterns ); + + return $this; } /** @@ -165,22 +214,21 @@ public function setOptions(array $options) * @param string $ruleSets * @param \PHPMD\AbstractRenderer[] $renderers * @param \PHPMD\RuleSetFactory $ruleSetFactory + * @param \PHPMD\Report $report * @return void */ public function processFiles( $inputPath, $ruleSets, array $renderers, - RuleSetFactory $ruleSetFactory + RuleSetFactory $ruleSetFactory, + Report $report ) { - // Merge parsed excludes - $this->setIgnorePattern($ruleSetFactory->getIgnorePattern($ruleSets)); + $this->addIgnorePatterns($ruleSetFactory->getIgnorePattern($ruleSets)); $this->input = $inputPath; - $report = new Report(); - $factory = new ParserFactory(); $parser = $factory->create($this); @@ -204,6 +252,7 @@ public function processFiles( $renderer->end(); } + $this->errors = $report->hasErrors(); $this->violations = !$report->isEmpty(); } } diff --git a/src/main/php/PHPMD/ParserFactory.php b/src/main/php/PHPMD/ParserFactory.php index f40e66d8a..7c29a2c46 100644 --- a/src/main/php/PHPMD/ParserFactory.php +++ b/src/main/php/PHPMD/ParserFactory.php @@ -65,11 +65,11 @@ private function createInstance() { $application = new Application(); - $currentWorkingDirectory = getcwd(); - if (file_exists($currentWorkingDirectory . self::PDEPEND_CONFIG_FILE_NAME)) { - $application->setConfigurationFile($currentWorkingDirectory . self::PDEPEND_CONFIG_FILE_NAME); - } elseif (file_exists($currentWorkingDirectory . self::PDEPEND_CONFIG_FILE_NAME_DIST)) { - $application->setConfigurationFile($currentWorkingDirectory . self::PDEPEND_CONFIG_FILE_NAME_DIST); + $workingDirectory = getcwd(); + if (file_exists($workingDirectory . self::PDEPEND_CONFIG_FILE_NAME)) { + $application->setConfigurationFile($workingDirectory . self::PDEPEND_CONFIG_FILE_NAME); + } elseif (file_exists($workingDirectory . self::PDEPEND_CONFIG_FILE_NAME_DIST)) { + $application->setConfigurationFile($workingDirectory . self::PDEPEND_CONFIG_FILE_NAME_DIST); } return $application->getEngine(); @@ -120,9 +120,9 @@ private function initInput(Engine $pdepend, PHPMD $phpmd) */ private function initIgnores(Engine $pdepend, PHPMD $phpmd) { - if (count($phpmd->getIgnorePattern()) > 0) { + if (count($phpmd->getIgnorePatterns()) > 0) { $pdepend->addFileFilter( - new ExcludePathFilter($phpmd->getIgnorePattern()) + new ExcludePathFilter($phpmd->getIgnorePatterns()) ); } } diff --git a/src/main/php/PHPMD/Renderer/BaselineRenderer.php b/src/main/php/PHPMD/Renderer/BaselineRenderer.php new file mode 100644 index 000000000..0ca5ba139 --- /dev/null +++ b/src/main/php/PHPMD/Renderer/BaselineRenderer.php @@ -0,0 +1,54 @@ +basePath = $basePath; + } + + public function renderReport(Report $report) + { + // keep track of which violations have been written, to avoid duplicates in the baseline + $registered = array(); + + $writer = $this->getWriter(); + $writer->write('' . PHP_EOL); + $writer->write('' . PHP_EOL); + + foreach ($report->getRuleViolations() as $violation) { + $ruleName = get_class($violation->getRule()); + $filePath = Paths::getRelativePath($this->basePath, $violation->getFileName()); + $methodName = $violation->getMethodName(); + + // deduplicate similar violations + $key = $ruleName . $filePath . $methodName; + if (isset($registered[$key])) { + continue; + } + + $xmlTag = sprintf( + ' ' . PHP_EOL, + $ruleName, + $filePath, + $methodName === null ? '' : ' method="' . $methodName . '"' + ); + $writer->write($xmlTag); + $registered[$key] = true; + } + + $writer->write('' . PHP_EOL); + } +} diff --git a/src/main/php/PHPMD/Renderer/GitHubRenderer.php b/src/main/php/PHPMD/Renderer/GitHubRenderer.php new file mode 100644 index 000000000..99fcfd0a8 --- /dev/null +++ b/src/main/php/PHPMD/Renderer/GitHubRenderer.php @@ -0,0 +1,58 @@ +. + * 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\AbstractRenderer; +use PHPMD\Report; + +/** + * This renderer outputs all violations in a format that GitHub Actions + * understands to display and highlight as problems. + */ +class GitHubRenderer extends AbstractRenderer +{ + /** + * This method will be called when the engine has finished the source analysis + * phase. + * + * @param \PHPMD\Report $report + * @return void + */ + public function renderReport(Report $report) + { + $writer = $this->getWriter(); + + foreach ($report->getRuleViolations() as $violation) { + $writer->write('::warning file='); + $writer->write($violation->getFileName()); + $writer->write(',line='); + $writer->write($violation->getBeginLine()); + $writer->write('::'); + $writer->write($violation->getDescription()); + $writer->write(PHP_EOL); + } + + foreach ($report->getErrors() as $error) { + $writer->write('::error file='); + $writer->write($error->getFile()); + $writer->write('::'); + $writer->write($error->getMessage()); + $writer->write(PHP_EOL); + } + } +} diff --git a/src/main/php/PHPMD/Renderer/HTMLRenderer.php b/src/main/php/PHPMD/Renderer/HTMLRenderer.php index a25f93ef8..5d11565de 100644 --- a/src/main/php/PHPMD/Renderer/HTMLRenderer.php +++ b/src/main/php/PHPMD/Renderer/HTMLRenderer.php @@ -318,13 +318,13 @@ function toggle(id) { */ public function renderReport(Report $report) { - $w = $this->getWriter(); + $writer = $this->getWriter(); $index = 0; $violations = $report->getRuleViolations(); $count = count($violations); - $w->write(sprintf('

%d problems found

', $count)); + $writer->write(sprintf('

%d problems found

', $count)); // If no problems were found, don't bother with rendering anything else. if (!$count) { @@ -332,7 +332,7 @@ public function renderReport(Report $report) } // Render summary tables. - $w->write("

Summary

"); + $writer->write("

Summary

"); $categorized = self::sumUpViolations($violations); $this->writeTable('By priority', 'Priority', $categorized[self::CATEGORY_PRIORITY]); $this->writeTable('By namespace', 'PHP Namespace', $categorized[self::CATEGORY_NAMESPACE]); @@ -340,8 +340,8 @@ public function renderReport(Report $report) $this->writeTable('By name', 'Rule name', $categorized[self::CATEGORY_RULE]); // Render details of each violation and place the "Details" display toggle. - $w->write("

Details

"); - $w->write(" + $writer->write("

Details

"); + $writer->write(" Show details ▼ "); - $w->write("