├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── LICENCE.md ├── README.md ├── bin └── phpcs-diff ├── composer.json ├── phpunit.xml ├── src ├── Filter │ ├── Exception │ │ ├── FilterException.php │ │ └── InvalidRuleException.php │ ├── Filter.php │ └── Rule │ │ ├── Exception │ │ ├── InvalidArgumentException.php │ │ ├── RuleException.php │ │ └── RuntimeException.php │ │ ├── FileRule.php │ │ ├── HasMessagesRule.php │ │ ├── PhpFileRule.php │ │ └── RuleInterface.php ├── Mapper │ ├── MapperInterface.php │ └── PhpcsViolationsMapper.php ├── PhpcsDiff.php └── Validator │ ├── AbstractValidator.php │ ├── Exception │ ├── InvalidArgumentException.php │ └── ValidatorException.php │ ├── RuleValidator.php │ └── ValidatorInterface.php └── tests ├── Filter ├── FilterTest.php └── Rule │ ├── FileRuleTest.php │ ├── HasMessagesRuleTest.php │ └── PhpFileRuleTest.php ├── Mapper └── PhpcsViolationsMapperTest.php ├── PhpcsDiffTest.php ├── TestBase.php └── Validator └── RuleValidatorTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | vendor 4 | .phpunit.cache 5 | coverage.clover 6 | composer.lock 7 | 8 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | tests: 5 | override: 6 | - php-scrutinizer-run 7 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: xenial 4 | 5 | sudo: false 6 | 7 | php: 8 | - 7.3 9 | - 7.4 10 | - 8.0 11 | - nightly 12 | 13 | matrix: 14 | allow_failures: 15 | - php: nightly 16 | 17 | before_script: 18 | - wget https://scrutinizer-ci.com/ocular.phar 19 | - composer update --no-interaction 20 | 21 | script: 22 | - XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover=coverage.clover --stop-on-failure 23 | - if [ -f coverage.clover ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi; 24 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Oliver Tappin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version](https://img.shields.io/github/tag/olivertappin/phpcs-diff.svg?style=flat&label=release)](https://github.com/olivertappin/phpcs-diff/tags) 2 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.md) 3 | [![Build Status](https://travis-ci.org/olivertappin/phpcs-diff.svg?branch=master)](https://travis-ci.org/olivertappin/phpcs-diff) 4 | [![Quality Score](https://img.shields.io/scrutinizer/g/olivertappin/phpcs-diff.svg?style=flat)](https://scrutinizer-ci.com/g/olivertappin/phpcs-diff) 5 | [![GitHub issues](https://img.shields.io/github/issues/olivertappin/phpcs-diff.svg)](https://github.com/olivertappin/phpcs-diff/issues) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/olivertappin/phpcs-diff.svg?style=flat)](https://packagist.org/packages/olivertappin/phpcs-diff) 7 | 8 | ## Installation 9 | 10 | The recommended method of installing this library is via [Composer](https://getcomposer.org/). 11 | 12 | ### Composer 13 | 14 | #### Global Installation 15 | 16 | Run the following command from your project root: 17 | 18 | composer global require olivertappin/phpcs-diff 19 | 20 | #### Manual Installation 21 | 22 | Alternatively, you can manually include a dependency for `olivertappin/phpcs-diff` in your `composer.json` file. For example: 23 | 24 | ```json 25 | { 26 | "require-dev": { 27 | "olivertappin/phpcs-diff": "^2.0" 28 | } 29 | } 30 | ``` 31 | 32 | And run `composer update olivertappin/phpcs-diff`. 33 | 34 | ### Git Clone 35 | 36 | You can also download the `phpcs-diff` source and create a symlink to your `/usr/bin` directory: 37 | 38 | git clone https://github.com/olivertappin/phpcs-diff.git 39 | ln -s phpcs-diff/bin/phpcs-diff /usr/bin/phpcs-diff 40 | cd /var/www/project 41 | phpcs-diff master -v 42 | 43 | ## Usage 44 | 45 | ### Basic Usage 46 | 47 | ```shell 48 | phpcs-diff -v 49 | ``` 50 | 51 | Where the current branch you are on is the branch you are comparing with, and `develop` is the base branch. In this example, `phpcs-diff` would run the following diff statement behind the scenes: 52 | 53 | ```shell 54 | git diff my-current-branch develop 55 | ``` 56 | 57 | _Please note:_ 58 | - The `-v` flag is optional. This returns a verbose output during processing. 59 | - The `current-branch` parameter is optional. If this is not defined, `phpcs-diff` will use the current commit hash via `git rev-parse --verify HEAD`. 60 | - You must have a `ruleset.xml` defined in your project base directory. 61 | 62 | After running `phpcs-diff`, the executable will return an output similar to the following: 63 | 64 | ``` 65 | ########## START OF PHPCS CHECK ########## 66 | module/Poject/src/Console/Script.php 67 | - Line 28 (WARNING) Line exceeds 120 characters; contains 190 characters 68 | - Line 317 (ERROR) Blank line found at end of control structure 69 | ########### END OF PHPCS CHECK ########### 70 | ``` 71 | 72 | Currently this is the only supported format however, I will look into adding additional formats (much like `phpcs`) in the near future. 73 | 74 | ### Travis CI Usage 75 | 76 | To use this as part of your CI/CD pipeline, create a script with the following: 77 | 78 | ```bash 79 | #!/bin/bash 80 | set -e 81 | if [ ! -z "$TRAVIS_PULL_REQUEST_BRANCH" ]; then 82 | git fetch `git config --get remote.origin.url` $TRAVIS_BRANCH\:refs/remotes/origin/$TRAVIS_BRANCH; 83 | composer global require olivertappin/phpcs-diff; 84 | ~/.composer/vendor/bin/phpcs-diff $TRAVIS_BRANCH; 85 | else 86 | echo "This test does not derive from a pull-request." 87 | echo "Unable to run phpcs-diff (as there's no diff)." 88 | 89 | # Here you might consider running phpcs instead: 90 | # composer global require squizlabs/php_codesniffer; 91 | # ~/.composer/vendor/bin/phpcs . 92 | fi; 93 | ``` 94 | 95 | Which will allow you to run `phpcs-diff` against the diff of your pull-request. 96 | 97 | Here's a sample of how this might look within Travis CI: 98 | 99 | ![Travis CI Example](https://user-images.githubusercontent.com/9773040/70551339-43bcfc00-1b6f-11ea-90c7-bc660e8dea28.png) 100 | 101 | ## About 102 | `phpcs-diff` detects violations of a defined set of coding standards based on a `git diff`. It uses `phpcs` from the [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) project. 103 | 104 | This project helps by achieving the following: 105 | - Speeds up your CI/CD pipeline validating changed files only, rather than the whole code base. 106 | - Allows you to migrate legacy code bases that cannot risk changing everything at once to become fully compliant to a coding standard. 107 | 108 | This executable works by only checking the changed lines, compared to the base branch, against all failed violations for those files, so you can be confident that any new or changed code will be compliant. 109 | 110 | This will hopefully put you in a position where your codebase will become more compliant to that coding standard over time, and maybe you will find the resource to eventually change everything, and just run `phpcs` on its own. 111 | 112 | ## Requirements 113 | 114 | The latest version of `phpcs-diff` requires PHP version 5.6.0 or later. 115 | 116 | This project also depends on `squizlabs/php_codesniffer` which is used internally to fetch the failed violations via `phpcs`. 117 | 118 | Finally, the `league/climate` package is also installed. This is to deal with console output, but this dependency may be removed in a future release. 119 | 120 | ## Contributing 121 | 122 | See [CONTRIBUTING.md](CONTRIBUTING.md) for information. 123 | -------------------------------------------------------------------------------- /bin/phpcs-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * @license https://github.com/olivertappin/phpcs-diff/blob/master/LICENCE.md MIT Licence 8 | */ 9 | 10 | if (is_file(__DIR__ . '/../../../autoload.php')) { 11 | $autoload = __DIR__ . '/../../../autoload.php'; 12 | } elseif (is_file(__DIR__ . '/../autoload.php')) { 13 | $autoload = __DIR__ . '/../autoload.php'; 14 | } elseif (is_file(__DIR__ . '/../vendor/autoload.php')) { 15 | $autoload = __DIR__ . '/../vendor/autoload.php'; 16 | } 17 | 18 | if (isset($autoload)) { 19 | require $autoload; 20 | } else { 21 | echo 'Can not find autoloader, did you run composer? ' . __DIR__; 22 | die(1); 23 | } 24 | 25 | $climate = new League\CLImate\CLImate(); 26 | 27 | $phpcsDiff = new PhpcsDiff\PhpcsDiff($argv, $climate); 28 | $phpcsDiff->run(); 29 | 30 | exit($phpcsDiff->getExitCode()); 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "olivertappin/phpcs-diff", 3 | "description": "Detects violations of a defined coding standard based on a git diff.", 4 | "require": { 5 | "php": "^7.3 || ^8.0", 6 | "ext-json": "*", 7 | "ext-gettext": "*", 8 | "squizlabs/php_codesniffer": "^3.5.7", 9 | "league/climate": "^3.4" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^7.5.20 || ^8.5.21 || ^9.5.10", 13 | "phpunit/php-code-coverage": "^9.2" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "PhpcsDiff\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "PhpcsDiff\\Tests\\": "tests/" 23 | } 24 | }, 25 | "bin": [ 26 | "bin/phpcs-diff" 27 | ], 28 | "scripts": { 29 | "test": "phpunit" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 23 | 24 | src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Filter/Exception/FilterException.php: -------------------------------------------------------------------------------- 1 | validate(); 43 | } catch (ValidatorException $exception) { 44 | throw new InvalidRuleException('', 0, $exception); 45 | } 46 | 47 | $this->rules = $rules; 48 | $this->unfilteredData = $unfilteredData; 49 | } 50 | 51 | /** 52 | * @return $this 53 | */ 54 | public function filter(): Filter 55 | { 56 | foreach ($this->unfilteredData as $key => $item) { 57 | foreach ($this->rules as $rule) { 58 | try { 59 | $rule($item); 60 | $this->filteredData[$key] = $item; 61 | } catch (RuleException $exception) { 62 | $this->contaminatedData[$key] = $item; 63 | } 64 | } 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function getFilteredData(): array 74 | { 75 | return $this->filteredData; 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function getContaminatedData(): array 82 | { 83 | return $this->contaminatedData; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Filter/Rule/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | changedLinesPerFile = $changedLinesPerFile; 24 | $this->currentDirectory = $currentDirectory; 25 | } 26 | 27 | /** 28 | * @param array $data 29 | * @return array 30 | */ 31 | public function map(array $data): array 32 | { 33 | $mappedData = []; 34 | 35 | foreach ($data as $file => $report) { 36 | if (!isset($this->changedLinesPerFile[$file]) || !is_array($this->changedLinesPerFile[$file])) { 37 | continue; 38 | } 39 | 40 | $changedLinesFromDiff = $this->changedLinesPerFile[$file]; 41 | 42 | $output = []; 43 | foreach ($report['messages'] as $message) { 44 | if (!in_array($message['line'], $changedLinesFromDiff, true)) { 45 | continue; 46 | } 47 | $output[] = ' - Line ' . $message['line'] . ' (' . $message['type'] . ') ' . $message['message']; 48 | } 49 | 50 | if (empty($output)) { 51 | continue; 52 | } 53 | 54 | $mappedData[] = str_replace($this->currentDirectory . '/', '', $file) . PHP_EOL . 55 | implode(PHP_EOL, $output) . PHP_EOL; 56 | } 57 | 58 | return $mappedData; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PhpcsDiff.php: -------------------------------------------------------------------------------- 1 | argv = $argv; 51 | $this->climate = $climate; 52 | 53 | if ($this->isFlagSet('-v')) { 54 | $this->climate->comment('Running in verbose mode.'); 55 | $this->isVerbose = true; 56 | } 57 | 58 | if (!isset($this->argv[1])) { 59 | $this->error('Please provide a base branch as the first argument.'); 60 | return; 61 | } 62 | 63 | $this->baseBranch = 'origin/' . str_replace('origin/', '', $this->argv[1]); 64 | $this->currentBranch = trim(shell_exec('git rev-parse --verify HEAD')); 65 | 66 | if (empty($this->currentBranch)) { 67 | $this->error('Unable to get current branch.'); 68 | } 69 | } 70 | 71 | /** 72 | * @param string $flag 73 | * @return bool 74 | */ 75 | protected function isFlagSet(string $flag) 76 | { 77 | $isFlagSet = false; 78 | $argv = $this->argv; 79 | 80 | $key = array_search($flag, $argv, true); 81 | if (false !== $key) { 82 | unset($argv[$key]); 83 | $argv = array_values($argv); 84 | 85 | $isFlagSet = true; 86 | } 87 | 88 | $this->argv = $argv; 89 | return $isFlagSet; 90 | } 91 | 92 | /** 93 | * @param int $exitCode 94 | */ 95 | protected function setExitCode(int $exitCode) 96 | { 97 | $this->exitCode = $exitCode; 98 | } 99 | 100 | /** 101 | * @return int 102 | */ 103 | public function getExitCode(): int 104 | { 105 | return $this->exitCode; 106 | } 107 | 108 | /** 109 | * @todo Automatically look at server envs for the travis base branch, if not provided? 110 | * @todo Define custom ruleset from command line argv for runPhpcs() 111 | */ 112 | public function run(): void 113 | { 114 | try { 115 | $filter = new Filter([new PhpFileRule()], $this->getChangedFiles()); 116 | } catch (FilterException $exception) { 117 | $this->error($exception->getMessage()); 118 | return; 119 | } 120 | 121 | $fileDiff = $filter->filter()->getFilteredData(); 122 | 123 | if (empty($fileDiff)) { 124 | $this->climate->info('No difference to compare.'); 125 | return; 126 | } 127 | 128 | if ($this->isVerbose) { 129 | $fileDiffCount = count($fileDiff); 130 | $this->climate->comment( 131 | 'Checking ' . $fileDiffCount . ' ' . 132 | ngettext('file', 'files', $fileDiffCount) . ' for violations.' 133 | ); 134 | } 135 | 136 | $phpcsOutput = $this->runPhpcs($fileDiff); 137 | 138 | if (is_null($phpcsOutput)) { 139 | $this->error('Unable to run phpcs executable.'); 140 | return; 141 | } 142 | 143 | if ($this->isVerbose) { 144 | $this->climate->comment('Filtering phpcs output.'); 145 | } 146 | 147 | try { 148 | $filter = new Filter([new HasMessagesRule()], $phpcsOutput['files']); 149 | } catch (FilterException $exception) { 150 | $this->error($exception->getMessage()); 151 | return; 152 | } 153 | 154 | $files = $filter->filter()->getFilteredData(); 155 | 156 | if ($this->isVerbose) { 157 | $this->climate->comment('Getting changed lines from git diff.'); 158 | } 159 | 160 | $changedLinesPerFile = $this->getChangedLinesPerFile($files); 161 | 162 | if ($this->isVerbose) { 163 | $this->climate->comment('Comparing phpcs output with changes lines from git diff.'); 164 | } 165 | 166 | $violations = (new PhpcsViolationsMapper( 167 | $changedLinesPerFile, 168 | getcwd() 169 | ))->map($files); 170 | 171 | if ($this->isVerbose) { 172 | $this->climate->comment('Preparing report.'); 173 | } 174 | 175 | if (empty($violations)) { 176 | $this->climate->info('No violations to report.'); 177 | return; 178 | } 179 | 180 | $this->outputViolations($violations); 181 | } 182 | 183 | /** 184 | * Run phpcs on a list of files passed into the method 185 | * 186 | * @param array $files 187 | * @param string $ruleset 188 | * @return mixed 189 | */ 190 | protected function runPhpcs(array $files = [], string $ruleset = 'ruleset.xml') 191 | { 192 | $exec = null; 193 | $root = dirname(__DIR__); 194 | 195 | $locations = [ 196 | 'vendor/bin/phpcs', 197 | $root . '/../../bin/phpcs', 198 | $root . '/../bin/phpcs', 199 | $root . '/bin/phpcs', 200 | $root . '/vendor/bin/phpcs', 201 | '~/.config/composer/vendor/bin/phpcs', 202 | '~/.composer/vendor/bin/phpcs', 203 | ]; 204 | 205 | foreach ($locations as $location) { 206 | if (is_file($location)) { 207 | $exec = $location; 208 | break; 209 | } 210 | } 211 | 212 | if (!$exec) { 213 | return null; 214 | } 215 | 216 | if ($this->isVerbose) { 217 | $this->climate->info('Using phpcs executable: ' . $exec); 218 | } 219 | 220 | $exec = PHP_BINARY . ' ' . $exec; 221 | $command = $exec . ' --report=json --standard=' . $ruleset . ' ' . implode(' ', $files); 222 | $output = shell_exec($command); 223 | 224 | if ($this->isVerbose) { 225 | $this->climate->info('Running: ' . $command); 226 | } 227 | 228 | $json = $output ? json_decode($output, true) : null; 229 | if ($json === null && $output) { 230 | $this->climate->error($output); 231 | } 232 | 233 | return $json; 234 | } 235 | 236 | /** 237 | * @param array $output 238 | */ 239 | protected function outputViolations(array $output): void 240 | { 241 | $this->climate->flank(strtoupper('Start of phpcs check'), '#', 10)->br(); 242 | $this->climate->out(implode(PHP_EOL, $output)); 243 | $this->climate->flank(strtoupper('End of phpcs check'), '#', 11)->br(); 244 | 245 | $this->error('Violations have been reported.'); 246 | } 247 | 248 | /** 249 | * Returns a list of files which are within the diff based on the current branch 250 | * 251 | * @return array 252 | */ 253 | protected function getChangedFiles(): array 254 | { 255 | // Get a list of changed files (not including deleted files) 256 | $output = shell_exec( 257 | 'git diff ' . $this->baseBranch . ' ' . $this->currentBranch . ' --name-only --diff-filter=ACM' 258 | ); 259 | 260 | // Convert files into an array 261 | $output = explode(PHP_EOL, $output); 262 | 263 | // Remove any empty values 264 | return array_filter($output); 265 | } 266 | 267 | /** 268 | * Extract the changed lines for each file from the git diff output 269 | * 270 | * @param array $files 271 | * @return array 272 | */ 273 | protected function getChangedLinesPerFile(array $files): array 274 | { 275 | $extract = []; 276 | $pattern = [ 277 | 'basic' => '^@@ (.*) @@', 278 | 'specific' => '@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,([0-9]+))? @@', 279 | ]; 280 | 281 | foreach ($files as $file => $data) { 282 | $command = 'git diff -U0 ' . $this->baseBranch . ' ' . $this->currentBranch . ' ' . $file . 283 | ' | grep -E ' . escapeshellarg($pattern['basic']); 284 | 285 | $lineDiff = shell_exec($command); 286 | $lines = array_filter(explode(PHP_EOL, $lineDiff)); 287 | $linesChanged = []; 288 | 289 | foreach ($lines as $line) { 290 | preg_match('/' . $pattern['specific'] . '/', $line, $matches); 291 | 292 | // If there were no specific matches, skip this line 293 | if ([] === $matches) { 294 | continue; 295 | } 296 | 297 | $start = $end = (int)$matches[1]; 298 | 299 | // Multiple lines were changed, so we need to calculate the end line 300 | if (isset($matches[2])) { 301 | $length = (int)$matches[2]; 302 | $end = $start + $length - 1; 303 | } 304 | 305 | foreach (range($start, $end) as $l) { 306 | $linesChanged[$l] = null; 307 | } 308 | } 309 | 310 | $extract[$file] = array_keys($linesChanged); 311 | } 312 | 313 | return $extract; 314 | } 315 | 316 | /** 317 | * @param string $message 318 | * @param int $exitCode 319 | */ 320 | protected function error(string $message, int $exitCode = 1): void 321 | { 322 | $this->climate->error($message); 323 | $this->setExitCode($exitCode); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/Validator/AbstractValidator.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Validator/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | data)) { 17 | throw new InvalidArgumentException('The data provided is empty.'); 18 | } 19 | 20 | if (!is_array($this->data)) { 21 | throw new InvalidArgumentException('The data provided is not an array.'); 22 | } 23 | 24 | foreach (array_values($this->data) as $i => $rule) { 25 | if (!$rule instanceof RuleInterface) { 26 | throw new InvalidArgumentException('Rule ' . ++$i . ' is not a valid rule class'); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Validator/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 | expectException(FilterException::class); 113 | $this->expectException(InvalidRuleException::class); 114 | new Filter( 115 | [ 116 | new \stdClass(), 117 | ], 118 | [ 119 | 'a' => 1, 120 | 'b' => 2, 121 | 'c' => 3, 122 | ] 123 | ); 124 | } 125 | 126 | /** 127 | * @covers \PhpcsDiff\Filter\Filter::filter 128 | * @covers \PhpcsDiff\Filter\Filter::__construct 129 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 130 | * @covers \PhpcsDiff\Validator\AbstractValidator::__construct 131 | * @covers \PhpcsDiff\Validator\RuleValidator::validate 132 | * @throws FilterException 133 | */ 134 | public function testFilterInstance(): void 135 | { 136 | $filter = (new Filter( 137 | [ 138 | new FileRule(), 139 | ], 140 | [ 141 | 'a' => 1, 142 | 'b' => 2, 143 | 'c' => 3, 144 | ] 145 | ))->filter(); 146 | 147 | $this->assertInstanceOf(Filter::class, $filter); 148 | } 149 | 150 | /** 151 | * @covers \PhpcsDiff\Filter\Filter::__construct 152 | * @covers \PhpcsDiff\Filter\Filter::filter 153 | * @covers \PhpcsDiff\Filter\Filter::getFilteredData 154 | * @covers \PhpcsDiff\Filter\Filter::getContaminatedData 155 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 156 | * @covers \PhpcsDiff\Validator\AbstractValidator::__construct 157 | * @covers \PhpcsDiff\Validator\RuleValidator::validate 158 | * @dataProvider unfilteredDataProvider 159 | * @param array $unfilteredData 160 | * @param array $filteredData 161 | * @param array $contaminatedData 162 | * @throws FilterException 163 | */ 164 | public function testFileFilter(array $unfilteredData, array $filteredData, array $contaminatedData): void 165 | { 166 | $filter = (new Filter( 167 | [ 168 | new FileRule(), 169 | ], 170 | $unfilteredData 171 | ))->filter(); 172 | 173 | $this->assertSame($filteredData, array_values($filter->getFilteredData())); 174 | $this->assertSame($contaminatedData, array_values($filter->getContaminatedData())); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/Filter/Rule/FileRuleTest.php: -------------------------------------------------------------------------------- 1 | expectExceptionMessage('The data argument provided is not a string.'); 43 | $this->expectException(RuleException::class); 44 | $this->expectException(InvalidArgumentException::class); 45 | $rule = new FileRule(); 46 | $rule(null); 47 | } 48 | 49 | /** 50 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 51 | * @throws RuleException 52 | */ 53 | public function testNonExistentFile() 54 | { 55 | $this->expectExceptionMessage('The file provided does not exist.'); 56 | $this->expectException(RuleException::class); 57 | $this->expectException(RuntimeException::class); 58 | $rule = new FileRule(); 59 | $rule(''); 60 | } 61 | 62 | /** 63 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 64 | * @throws RuleException 65 | */ 66 | public function testNonFile() 67 | { 68 | $this->expectException(RuntimeException::class); 69 | $this->expectException(RuleException::class); 70 | $this->expectExceptionMessage('The file provided is not a regular file.'); 71 | $rule = new FileRule(); 72 | $rule('test'); 73 | } 74 | 75 | /** 76 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 77 | * @throws RuleException 78 | */ 79 | public function testFile() 80 | { 81 | $rule = new FileRule(); 82 | $actual = $rule('test.txt'); 83 | 84 | $this->assertNull($actual); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Filter/Rule/HasMessagesRuleTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 19 | $this->expectException(RuleException::class); 20 | $this->expectExceptionMessage('The data argument provided has no messages.'); 21 | $rule = new HasMessagesRule(); 22 | $rule([]); 23 | } 24 | 25 | /** 26 | * @covers \PhpcsDiff\Filter\Rule\HasMessagesRule::__invoke 27 | * @throws RuleException 28 | */ 29 | public function testEmptyMessages(): void 30 | { 31 | $this->expectException(InvalidArgumentException::class); 32 | $this->expectException(RuleException::class); 33 | $this->expectExceptionMessage('The data argument provided has no messages.'); 34 | $rule = new HasMessagesRule(); 35 | $rule(['messages' => []]); 36 | } 37 | 38 | /** 39 | * @covers \PhpcsDiff\Filter\Rule\HasMessagesRule::__invoke 40 | * @throws RuleException 41 | */ 42 | public function testNullMessages(): void 43 | { 44 | $this->expectException(InvalidArgumentException::class); 45 | $this->expectException(RuleException::class); 46 | $this->expectExceptionMessage('The data argument provided has no messages.'); 47 | $rule = new HasMessagesRule(); 48 | $rule(['messages' => null]); 49 | } 50 | 51 | /** 52 | * @covers \PhpcsDiff\Filter\Rule\HasMessagesRule::__invoke 53 | * @throws RuleException 54 | */ 55 | public function testMessages(): void 56 | { 57 | $rule = new HasMessagesRule(); 58 | $actual = $rule(['messages' => ['message']]); 59 | 60 | $this->assertNull($actual); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Filter/Rule/PhpFileRuleTest.php: -------------------------------------------------------------------------------- 1 | expectException(RuntimeException::class); 52 | $this->expectException(RuleException::class); 53 | $this->expectExceptionMessage('The file provided does not have a .php extension.'); 54 | $rule = new PhpFileRule(); 55 | $rule('test.txt'); 56 | } 57 | 58 | /** 59 | * @covers \PhpcsDiff\Filter\Rule\PhpFileRule::__invoke 60 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 61 | * @throws RuleException 62 | */ 63 | public function testIncorrectMimeType(): void 64 | { 65 | $this->expectException(RuntimeException::class); 66 | $this->expectException(RuleException::class); 67 | $this->expectExceptionMessage('The file provided does not have the text/x-php mime type.'); 68 | $rule = new PhpFileRule(); 69 | $rule('image.php'); 70 | } 71 | 72 | /** 73 | * @covers \PhpcsDiff\Filter\Rule\PhpFileRule::__invoke 74 | * @covers \PhpcsDiff\Filter\Rule\FileRule::__invoke 75 | * @throws RuleException 76 | */ 77 | public function testPhpFile(): void 78 | { 79 | $rule = new PhpFileRule(); 80 | $actual = $rule('test.php'); 81 | 82 | $this->assertNull($actual); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Mapper/PhpcsViolationsMapperTest.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'messages' => [ 14 | [ 15 | 'line' => 100, 16 | 'message' => 'This is the message', 17 | 'type' => 'ERROR', 18 | ] 19 | ], 20 | ], 21 | ]; 22 | 23 | private $mappedData2 = [ 24 | 'SomeImportantClass.php' => [ 25 | 'messages' => [ 26 | [ 27 | 'line' => 230, 28 | 'message' => 'This is the message', 29 | 'type' => 'ERROR', 30 | ], 31 | ], 32 | ], 33 | 'index.php' => [ 34 | 'messages' => [ 35 | [ 36 | 'line' => 230, 37 | 'message' => 'This is the message', 38 | 'type' => 'ERROR', 39 | ], 40 | [ 41 | 'line' => 250, 42 | 'message' => 'Some other warning', 43 | 'type' => 'WARNING', 44 | ] 45 | ], 46 | ], 47 | ]; 48 | 49 | /** 50 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::__construct 51 | */ 52 | public function testMapperInstance(): void 53 | { 54 | $mapper = new PhpcsViolationsMapper([], ''); 55 | 56 | $this->assertInstanceOf(MapperInterface::class, $mapper); 57 | } 58 | 59 | /** 60 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::__construct 61 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::map 62 | */ 63 | public function testEmptyMapper(): void 64 | { 65 | $mappedData = (new PhpcsViolationsMapper([], ''))->map([]); 66 | 67 | $this->assertEmpty($mappedData); 68 | } 69 | 70 | /** 71 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::__construct 72 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::map 73 | */ 74 | public function testMapperLineMatch(): void 75 | { 76 | $changedLinesPerFile = [ 77 | 'file.php' => [ 78 | 100, 79 | ] 80 | ]; 81 | 82 | $mappedData = (new PhpcsViolationsMapper($changedLinesPerFile, ''))->map($this->mappedData1); 83 | 84 | self::assertIsArray($mappedData); 85 | self::assertCount(1, $mappedData); 86 | self::assertIsString($mappedData[0]); 87 | self::assertSame($mappedData[0], 'file.php' . PHP_EOL . ' - Line 100 (ERROR) This is the message' . PHP_EOL); 88 | } 89 | 90 | /** 91 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::__construct 92 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::map 93 | */ 94 | public function testMapperNoLineMatch(): void 95 | { 96 | $changedLinesPerFile = [ 97 | 'file.php' => [ 98 | 101, 99 | ] 100 | ]; 101 | 102 | $mappedData = (new PhpcsViolationsMapper($changedLinesPerFile, ''))->map($this->mappedData1); 103 | 104 | self::assertIsArray($mappedData); 105 | self::assertCount(0, $mappedData); 106 | } 107 | 108 | /** 109 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::__construct 110 | * @covers \PhpcsDiff\Mapper\PhpcsViolationsMapper::map 111 | */ 112 | public function testMapperNoFileMatch(): void 113 | { 114 | $changedLinesPerFile = [ 115 | 'NonMatchedClass.php' => [ 116 | 101, 117 | ] 118 | ]; 119 | 120 | $mappedData = (new PhpcsViolationsMapper($changedLinesPerFile, ''))->map($this->mappedData2); 121 | 122 | self::assertIsArray($mappedData); 123 | self::assertCount(0, $mappedData); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/PhpcsDiffTest.php: -------------------------------------------------------------------------------- 1 | cliMate = $this->createMock(CLImate::class); 19 | } 20 | 21 | /** 22 | * @covers \PhpcsDiff\PhpcsDiff::__construct 23 | * @covers \PhpcsDiff\PhpcsDiff::error 24 | * @covers \PhpcsDiff\PhpcsDiff::getExitCode 25 | * @covers \PhpcsDiff\PhpcsDiff::isFlagSet 26 | * @covers \PhpcsDiff\PhpcsDiff::setExitCode 27 | */ 28 | public function testExitCodeBeforeRun() 29 | { 30 | $phpcsDiff = new PhpcsDiff([], $this->cliMate); 31 | 32 | self::assertSame(1, $phpcsDiff->getExitCode()); 33 | } 34 | 35 | /** 36 | * @covers \PhpcsDiff\PhpcsDiff::__construct 37 | * @covers \PhpcsDiff\PhpcsDiff::error 38 | * @covers \PhpcsDiff\PhpcsDiff::getExitCode 39 | * @covers \PhpcsDiff\PhpcsDiff::isFlagSet 40 | * @covers \PhpcsDiff\PhpcsDiff::setExitCode 41 | */ 42 | public function testPhpcsDiffNoCurrent() 43 | { 44 | $phpcsDiff = new PhpcsDiff(['fakeBranch'], $this->cliMate); 45 | 46 | self::assertSame(1, $phpcsDiff->getExitCode()); 47 | } 48 | 49 | /** 50 | * @covers \PhpcsDiff\PhpcsDiff::__construct 51 | * @covers \PhpcsDiff\PhpcsDiff::error 52 | * @covers \PhpcsDiff\PhpcsDiff::getExitCode 53 | * @covers \PhpcsDiff\PhpcsDiff::isFlagSet 54 | * @covers \PhpcsDiff\PhpcsDiff::setExitCode 55 | */ 56 | public function testVerboseNoCurrent() 57 | { 58 | $this->cliMate 59 | ->expects(self::exactly(2)) 60 | ->method('__call') 61 | ->withConsecutive( 62 | ['comment', ['Running in verbose mode.']], 63 | ['error', ['Please provide a base branch as the first argument.']] 64 | ); 65 | ; 66 | 67 | $phpcsDiff = new PhpcsDiff(['-v', 'fakeBranch'], $this->cliMate); 68 | 69 | self::assertSame(1, $phpcsDiff->getExitCode()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/TestBase.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(RuleValidator::class, $actual); 23 | } 24 | 25 | /** 26 | * @covers \PhpcsDiff\Validator\RuleValidator::__construct 27 | * @covers \PhpcsDiff\Validator\RuleValidator::validate 28 | */ 29 | public function testEmptyRuleValidator(): void 30 | { 31 | $this->expectException(InvalidArgumentException::class); 32 | $this->expectException(ValidatorException::class); 33 | $this->expectExceptionMessage('The data provided is empty.'); 34 | (new RuleValidator([]))->validate(); 35 | } 36 | 37 | /** 38 | * @covers \PhpcsDiff\Validator\RuleValidator::__construct 39 | * @covers \PhpcsDiff\Validator\RuleValidator::validate 40 | */ 41 | public function testNonArrayRuleValidator(): void 42 | { 43 | $this->expectException(ValidatorException::class); 44 | $this->expectException(InvalidArgumentException::class); 45 | $this->expectExceptionMessage('The data provided is not an array.'); 46 | (new RuleValidator('string'))->validate(); 47 | } 48 | 49 | /** 50 | * @covers \PhpcsDiff\Validator\RuleValidator::__construct 51 | * @covers \PhpcsDiff\Validator\RuleValidator::validate 52 | * @throws ValidatorException 53 | */ 54 | public function testRuleValidator(): void 55 | { 56 | $this->expectNotToPerformAssertions(); 57 | 58 | // If this throws an exception, the test will fail 59 | (new RuleValidator([ 60 | new FileRule(), 61 | new PhpFileRule(), 62 | new HasMessagesRule(), 63 | ]))->validate(); 64 | } 65 | } 66 | --------------------------------------------------------------------------------