├── .coveralls.yml ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CreatePhar.php ├── GitHooks ├── pre-commit └── pre-receive ├── LICENSE ├── README.md ├── autoload.php ├── bin └── diffFilter ├── build.sh ├── composer.json ├── phpunit.xml └── src ├── ArgParser.php ├── CodeLimits.php ├── CoverageCheck.php ├── DiffFileLoader.php ├── DiffFileLoaderOldVersion.php ├── DiffFileState.php ├── DiffFilter.php ├── DiffLineHandle.php ├── DiffLineHandle ├── ContextLine.php ├── NewVersion │ ├── AddedLine.php │ ├── DiffStart.php │ ├── NewFile.php │ └── RemovedLine.php └── OldVersion │ ├── AddedLine.php │ ├── DiffStart.php │ ├── NewFile.php │ └── RemovedLine.php ├── Exceptions ├── ArgumentNotFound.php └── FileNotFound.php ├── FileChecker.php ├── FileMatcher.php ├── FileMatchers ├── EndsWith.php ├── FileMapper.php └── Prefix.php ├── FileParser.php ├── Loaders ├── Buddy.php ├── Checkstyle.php ├── Clover.php ├── CodeClimate.php ├── Generic.php ├── Humbug.php ├── Infection.php ├── Jacoco.php ├── PhanJson.php ├── PhanText.php ├── PhpCs.php ├── PhpCsStrict.php ├── PhpMd.php ├── PhpMdStrict.php ├── PhpMnd.php ├── PhpMndXml.php ├── PhpStan.php ├── PhpUnit.php ├── Phpcpd.php ├── Psalm.php └── Pylint.php ├── Output.php ├── Outputs ├── Json.php ├── Phpcs.php └── Text.php ├── PhpunitFilter.php ├── Runners └── generic.php └── functions.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: report/coverage.xml 2 | json_path: report/coveralls-upload.json 3 | service_name: travis-ci 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/ export-ignore 2 | examples/ export-ignore 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | tests: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: ['ubuntu-latest', 'windows-latest', 'macOS-latest'] 11 | php-version: ['8.1', '8.2', '8.3'] 12 | name: PHP ${{ matrix.php-version }} Test on ${{ matrix.operating-system }} 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Cache dependencies 17 | uses: actions/cache@v1 18 | with: 19 | path: /tmp/composer-cache 20 | key: dependencies-composer-${{ hashFiles('composer.json') }} 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php-version }} 25 | tools: composer:v2 26 | coverage: xdebug 27 | - name: Install Composer dependencies 28 | run: composer install --prefer-dist --no-interaction --no-suggest 29 | - name: Install phpcs 30 | run: composer global require squizlabs/php_codesniffer 31 | - name: Install phpmd 32 | run: composer global require phpmd/phpmd 33 | - name: Run the build 34 | run: ./build.sh 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache 2 | clover.xml 3 | composer.lock 4 | crap.xml 5 | diff.txt 6 | diffFilter.phar 7 | html/ 8 | humbug.json 9 | humbug.json.dist 10 | humbuglog.txt 11 | infection.json.dist 12 | infection.log 13 | infection.phar 14 | phpmd-tests.xml 15 | phpmd.xml 16 | report 17 | vendor 18 | -------------------------------------------------------------------------------- /CreatePhar.php: -------------------------------------------------------------------------------- 1 | addFile('autoload.php'); 18 | $phar->addFile('bin/diffFilter'); 19 | 20 | 21 | $dirs = [ 22 | 'src', 23 | 'vendor', 24 | ]; 25 | 26 | foreach($dirs as $dir) { 27 | addDir($dir, $phar); 28 | } 29 | 30 | $phar->setStub( 31 | "#!/usr/bin/env php 32 | getPathname(); 54 | $path = $dir . substr($fullPath, $codeLength); 55 | 56 | if (strpos($path, '/test/') !== false) { 57 | continue; 58 | } 59 | 60 | if (is_file($path)) { 61 | $phar->addFromString($path, php_strip_whitespace($path)); 62 | } 63 | } 64 | } 65 | 66 | function cleanUp($pharName) 67 | { 68 | shell_exec("rm -rf vendor"); 69 | shell_exec("rm $pharName"); 70 | shell_exec("composer install --no-dev -o"); 71 | } 72 | -------------------------------------------------------------------------------- /GitHooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # This is a PSR-2 checking example for just the code about to be committed 4 | 5 | # Get the changes 6 | files=$(mktemp) 7 | diff=$(mktemp) 8 | 9 | git diff --cached --name-only --diff-filter=ACMR -- "*.php" > ${files} 10 | git diff --cached > ${diff} 11 | 12 | # Run the phpcs report 13 | phpcs=$(mktemp) 14 | ./vendor/bin/phpcs --file-list=${files} --parallel=2 --standard=psr2 --report=json > ${phpcs} || true 15 | 16 | # check for differences 17 | ./vendor/bin/diffFilter --phpcs ${diff} ${phpcs} 18 | -------------------------------------------------------------------------------- /GitHooks/pre-receive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a PSR-2 checking example for just the code about to be committed 4 | 5 | # Get the changes 6 | files=$(mktemp) 7 | diff=$(mktemp) 8 | 9 | git diff --name-only --diff-filter=ACMR -- "*.php" $1...$2 > ${files} 10 | git diff $1...$2 > ${diff} 11 | 12 | # Run the phpcs report 13 | phpcs=$(mktemp) 14 | ./vendor/bin/phpcs --file-list=${files} --parallel=2 --standard=psr2 --report=json > ${phpcs} || true 15 | 16 | # check for differences 17 | ./vendor/bin/diffFilter --phpcs ${diff} ${phpcs} 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Scott Dutton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coverageChecker 2 | Allows old code to use new standards 3 | 4 | [![Build Status](https://travis-ci.org/exussum12/coverageChecker.svg?branch=master)](https://travis-ci.org/exussum12/coverageChecker) 5 | [![Coverage Status](https://coveralls.io/repos/github/exussum12/coverageChecker/badge.svg?branch=master)](https://coveralls.io/github/exussum12/coverageChecker?branch=master) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/exussum12/coverageChecker/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/exussum12/coverageChecker/?branch=master) 7 | 8 | Coverage checker allows new standards to be implemented incrementally, by only enforcing them on new / edited code. 9 | 10 | Tools like phpcs and phpmd are an all or nothing approach, coverage checker allows this to work with the diff i.e. enforce all of the pull request / change request. 11 | 12 | This is sometimes called "Baselining" 13 | 14 | Also working with PHPunit to allow, for example 90% of new/edited code to be covered. which will increase the overall coverage over time. 15 | 16 | # Installing 17 | 18 | ## Composer 19 | With composer simply 20 | 21 | composer require --dev exussum12/coverage-checker 22 | 23 | then call the script you need 24 | 25 | ## Using Phar 26 | Phar is a packaged format which should be a single download. The latest Phar can be found [Here](https://github.com/exussum12/coverageChecker/releases). 27 | 28 | After downloading run `chmod +x diffFilter.phar` and then call as `./diffFilter.phar` followed by the normal options 29 | 30 | ## Manually 31 | Clone this repository somewhere your your build plan can be accessed, composer install is preferred but there is a non composer class loader which will be used if composer is not installed. If composer is not used some PHP specific features will not work as expected. 32 | Then call the script you need 33 | 34 | 35 | # Usage 36 | 37 | First of all a diff is needed 38 | 39 | git diff origin/master... > diff.txt 40 | 41 | See [here](https://github.com/exussum12/coverageChecker/wiki/Generating-a-diff) for a more in depth examples of what diff you should generate 42 | 43 | Then the output for the tool you wish to check (such as phpcs, PHPUnit, phpmd etc) for example 44 | 45 | phpcs --standard=psr2 --report=json > phpcs.json || true 46 | 47 | Here the `|| true` ensures that the whole build will not fail if phpcs fails. 48 | 49 | Then call diffFilter 50 | 51 | ./vendor/bin/diffFilter --phpcs diff.txt phpcs.json 100 52 | 53 | The last argument (100 in this case) is optional, the default is 100. This can be lowered to 90 for example to ensure that at least 90% of the changed code conforms to the standard. 54 | diffFilter will exit with a `0` status if the changed code passes the minimum coverage. `2` otherwise 55 | 56 | ## Extended guide 57 | A more in depth guide can be [found on the wiki](https://github.com/exussum12/coverageChecker/wiki) also some tips for speeding up the build. 58 | 59 | ## Installing as a git hook 60 | 61 | There are 2 examples hooks in the GitHooks directory, if you symlink to these diffFilter will run locally. 62 | 63 | pre-commit is before the commit happens 64 | pre-receive will prevent you pushing 65 | 66 | # Full list of available diff filters 67 | 68 | Below is a list of all tools and a brief description 69 | 70 | ``` 71 | --buddy Parses buddy (magic number detection) output 72 | --checkstyle Parses a report in checkstyle format 73 | --clover Parses text output in clover (xml) format 74 | --codeclimate Parse codeclimate output 75 | --humbug Parses the json report format of humbug (mutation testing) 76 | --infecton Parses the infection text log format 77 | --jacoco Parses xml coverage report produced by Jacoco 78 | --phan Parse the default phan(static analysis) output 79 | --phanJson Parses phan (static analysis) in json format 80 | --phpcpd Parses the text output from phpcpd (Copy Paste Detect) 81 | --phpcs Parses the json report format of phpcs, this mode only reports errors as violations 82 | --phpcsStrict Parses the json report format of phpcs, this mode reports errors and warnings as violations 83 | --phpmd Parses the xml report format of phpmd, this mode reports multi line violations once per diff, instead of on each line 84 | the violation occurs 85 | --phpmdStrict Parses the xml report format of phpmd, this mode reports multi line violations once per line they occur 86 | --phpmnd Parses the text output of phpmnd (Magic Number Detection) 87 | --phpstan Parses the text output of phpstan 88 | --phpunit Parses text output in clover (xml) format generated with coverage-clover=file.xml 89 | --pylint Parses PyLint output 90 | --psalm Parses Psalm output 91 | ``` 92 | 93 | 94 | # Running in information mode 95 | Simply pass the 3rd argument in as 0, this will give output showing failed lines but will not fail the build 96 | 97 | 98 | # Why not run the auto fixers 99 | Auto fixers do exist for some of these tools, but on larger code bases there are many instances where these can not be auto fixed. CoverageChecker allows to go to these new standards in the most used parts of the code by enforcing all changes to comply to the new standards 100 | 101 | # What is a diff filtered test 102 | 103 | A diff filtered test is a test where the execution and diffence (diff) is used from a known point. 104 | This information can be used to only run the tests which have been changed. Saving in many cases minutes running tests. 105 | 106 | A good workflow is to branch, run the tests with `--coverage-php=php-coverage.php` and then when running your tests run `git diff origin/master... > diff.txt && ./composer/bin/phpunit` 107 | 108 | This saves the coverage information in the first step which the diff then filters to runnable tests. 109 | 110 | This one time effort saves running unnecessary tests on each run, tests for which the code has not changed. 111 | 112 | Check the [Wiki](https://github.com/exussum12/coverageChecker/wiki/PHPUnit-or-Clover#speeding-up-builds-with-phpunit) for more information on installation and usage 113 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | diff.txt 8 | phpcs --standard=psr2 src 9 | phpcs --standard=psr2 --ignore=bootstrap.php,fixtures/* tests 10 | 11 | phpmd src text cleancode,codesize,controversial,unusedcode 12 | phpmd tests text cleancode,codesize,controversial,unusedcode --exclude fixtures 13 | 14 | ./vendor/bin/phpunit 15 | 16 | [ -z "$UPDATE_COVERAGE" ] || bin/diffFilter --phpunit diff.txt report/coverage.xml 17 | 18 | [ -z "$UPDATE_COVERAGE" ] || php vendor/bin/coveralls -v 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exussum12/coverage-checker", 3 | "description": "Allows checking the code coverage of a single pull request", 4 | "require-dev": { 5 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0" 6 | }, 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Scott Dutton", 11 | "email": "scott@exussum.co.uk" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-4": { 16 | "exussum12\\CoverageChecker\\": "src/", 17 | "exussum12\\CoverageChecker\\tests\\": "tests/" 18 | } 19 | }, 20 | "bin": ["bin/diffFilter"], 21 | "require": { 22 | "php": ">=8.1", 23 | "ext-xmlreader": "*", 24 | "ext-json": "*", 25 | "nikic/php-parser": "^3.1||^4.0||^5.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ArgParser.php: -------------------------------------------------------------------------------- 1 | args = $args; 13 | } 14 | 15 | /** 16 | * @throws ArgumentNotFound 17 | */ 18 | public function getArg(string $name): string 19 | { 20 | if (is_numeric($name)) { 21 | $name = (int) $name; 22 | return $this->numericArg($name); 23 | } 24 | 25 | return $this->letterArg($name); 26 | } 27 | 28 | protected function numericArg(int $position): string 29 | { 30 | foreach ($this->args as $arg) { 31 | if ($arg[0] != '-' && $position-- == 0) { 32 | return $arg; 33 | } 34 | } 35 | 36 | throw new ArgumentNotFound(); 37 | } 38 | 39 | protected function letterArg($name): string 40 | { 41 | $name = $this->getAdjustedArg($name); 42 | foreach ($this->args as $arg) { 43 | list($value, $arg) = $this->splitArg($arg); 44 | 45 | if ($arg[0] == '-' && $name == $arg) { 46 | return $value; 47 | } 48 | } 49 | 50 | throw new ArgumentNotFound(); 51 | } 52 | 53 | protected function splitArg(string $arg): array 54 | { 55 | $value = '1'; 56 | if (strpos($arg, '=') > 0) { 57 | list($arg, $value) = explode('=', $arg, 2); 58 | } 59 | 60 | return array($value, $arg); 61 | } 62 | 63 | protected function getAdjustedArg(string $name): string 64 | { 65 | $name = strlen($name) == 1 ? 66 | '-' . $name : 67 | '--' . $name; 68 | return $name; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/CodeLimits.php: -------------------------------------------------------------------------------- 1 | startLine = $startLine; 12 | $this->endLine = $endLine; 13 | } 14 | 15 | public function getStartLine(): int 16 | { 17 | return $this->startLine; 18 | } 19 | 20 | public function getEndLine(): int 21 | { 22 | return $this->endLine; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CoverageCheck.php: -------------------------------------------------------------------------------- 1 | diff = $diff; 55 | $this->fileChecker = $fileChecker; 56 | $this->matcher = $matcher; 57 | $this->cache = new stdClass; 58 | } 59 | 60 | /** 61 | * array of uncoveredLines and coveredLines 62 | */ 63 | public function getCoveredLines(): array 64 | { 65 | $this->getDiff(); 66 | 67 | $coveredFiles = $this->fileChecker->parseLines(); 68 | $this->uncoveredLines = []; 69 | $this->coveredLines = []; 70 | 71 | $diffFiles = array_keys($this->cache->diff); 72 | foreach ($diffFiles as $file) { 73 | $matchedFile = $this->findFile($file, $coveredFiles); 74 | if ($matchedFile !== '') { 75 | $this->matchLines($file, $matchedFile); 76 | } 77 | } 78 | 79 | return [ 80 | 'uncoveredLines' => $this->uncoveredLines, 81 | 'coveredLines' => $this->coveredLines, 82 | ]; 83 | } 84 | 85 | protected function addUnCoveredLine(string $file, int $line, array $message) 86 | { 87 | if (!isset($this->uncoveredLines[$file])) { 88 | $this->uncoveredLines[$file] = []; 89 | } 90 | 91 | $this->uncoveredLines[$file][$line] = $message; 92 | } 93 | 94 | protected function addCoveredLine(string $file, int $line) 95 | { 96 | if (!isset($this->coveredLines[$file])) { 97 | $this->coveredLines[$file] = []; 98 | } 99 | 100 | $this->coveredLines[$file][] = $line; 101 | } 102 | 103 | protected function matchLines(string $fileName, string $matchedFile) 104 | { 105 | foreach ($this->cache->diff[$fileName] as $line) { 106 | $messages = $this->fileChecker->getErrorsOnLine($matchedFile, $line); 107 | 108 | if (is_null($messages)) { 109 | continue; 110 | } 111 | 112 | if (count($messages) == 0) { 113 | $this->addCoveredLine($fileName, $line); 114 | continue; 115 | } 116 | 117 | 118 | $this->addUnCoveredLine( 119 | $fileName, 120 | $line, 121 | $messages 122 | ); 123 | } 124 | } 125 | 126 | protected function addCoveredFile(string $file) 127 | { 128 | foreach ($this->cache->diff[$file] as $line) { 129 | $this->addCoveredLine($file, $line); 130 | } 131 | } 132 | 133 | protected function addUnCoveredFile(string $file) 134 | { 135 | foreach ($this->cache->diff[$file] as $line) { 136 | $this->addUnCoveredLine($file, $line, ['No Cover']); 137 | } 138 | } 139 | 140 | protected function getDiff(): array 141 | { 142 | if (empty($this->cache->diff)) { 143 | $this->cache->diff = $this->diff->getChangedLines(); 144 | } 145 | 146 | return $this->cache->diff; 147 | } 148 | 149 | protected function handleFileNotFound(string $file) 150 | { 151 | $unMatchedFile = $this->fileChecker->handleNotFoundFile(); 152 | 153 | if ($unMatchedFile === true) { 154 | $this->addCoveredFile($file); 155 | } 156 | 157 | if ($unMatchedFile === false) { 158 | $this->addUnCoveredFile($file); 159 | } 160 | } 161 | 162 | protected function findFile(string $file, array $coveredFiles): string 163 | { 164 | try { 165 | return $this->matcher->match($file, $coveredFiles); 166 | } catch (Exceptions\FileNotFound $e) { 167 | $this->handleFileNotFound($file); 168 | return ''; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/DiffFileLoader.php: -------------------------------------------------------------------------------- 1 | fileLocation = $fileName; 22 | $this->diff = new DiffFileState(); 23 | } 24 | 25 | public function getChangedLines(): array 26 | { 27 | if (( 28 | !is_readable($this->fileLocation) && 29 | strpos($this->fileLocation, "php://") !== 0 30 | )) { 31 | throw new InvalidArgumentException("Can't read file {$this->fileLocation}", 1); 32 | } 33 | 34 | $handle = fopen($this->fileLocation, 'r'); 35 | 36 | while (($line = fgets($handle)) !== false) { 37 | // process the line read. 38 | $lineHandle = $this->getLineHandle($line); 39 | $lineHandle->handle($line); 40 | $this->diff->incrementCurrentPosition(); 41 | } 42 | 43 | fclose($handle); 44 | 45 | return $this->diff->getChangedLines(); 46 | } 47 | 48 | private function getLineHandle(string $line): DiffLineHandle 49 | { 50 | foreach ($this->diffLines as $lineType) { 51 | $lineType = $this->getClass($lineType); 52 | if ($lineType->isValid($line)) { 53 | return $lineType; 54 | } 55 | } 56 | // the line doesn't have a special meaning, its probably context 57 | return $this->getClass(DiffLineHandle\ContextLine::class); 58 | } 59 | 60 | private function getClass(string $className): DiffLineHandle 61 | { 62 | if (!isset($this->handles[$this->getFileHandleName($className)])) { 63 | $this->handles[ 64 | $this->getFileHandleName($className) 65 | ] = new $className($this->diff); 66 | } 67 | 68 | return $this->handles[$this->getFileHandleName($className)]; 69 | } 70 | 71 | private function getFileHandleName(string $namespace): string 72 | { 73 | $namespace = explode('\\', $namespace); 74 | return end($namespace); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/DiffFileLoaderOldVersion.php: -------------------------------------------------------------------------------- 1 | new, this returns what used to be there 7 | */ 8 | class DiffFileLoaderOldVersion extends DiffFileLoader 9 | { 10 | protected $diffLines = [ 11 | DiffLineHandle\OldVersion\NewFile::class, 12 | DiffLineHandle\OldVersion\AddedLine::class, 13 | DiffLineHandle\OldVersion\RemovedLine::class, 14 | DiffLineHandle\OldVersion\DiffStart::class, 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/DiffFileState.php: -------------------------------------------------------------------------------- 1 | currentPosition = $position; 13 | } 14 | 15 | public function setCurrentFile(string $currentFile) 16 | { 17 | $this->currentFile = $currentFile; 18 | } 19 | 20 | public function addChangeLine() 21 | { 22 | if (!isset($this->changeLines[$this->currentFile])) { 23 | $this->changeLines[$this->currentFile] = []; 24 | } 25 | $this->changeLines[$this->currentFile][] = $this->currentPosition; 26 | } 27 | 28 | public function incrementCurrentPosition() 29 | { 30 | $this->currentPosition++; 31 | } 32 | 33 | public function decrementCurrentPosition() 34 | { 35 | $this->currentPosition--; 36 | } 37 | 38 | public function getChangedLines(): array 39 | { 40 | return array_map('array_unique', $this->changeLines); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DiffFilter.php: -------------------------------------------------------------------------------- 1 | modifiedTests = $coverage->getTestsForRunning($fuzziness); 28 | $this->modifiedSuites = array_keys($this->modifiedTests); 29 | unset($coverage); 30 | } catch (Exception $exception) { 31 | //Something has gone wrong, Don't filter 32 | echo "Missing required diff / php coverage, Running all tests\n"; 33 | } 34 | } 35 | 36 | public function addError(Test $test, Exception $exception, $time) 37 | { 38 | } 39 | public function addFailure(Test $test, AssertionFailedError $exception, $time) 40 | { 41 | } 42 | public function addRiskyTest(Test $test, Exception $exception, $time) 43 | { 44 | } 45 | public function startTestSuite(TestSuite $suite) 46 | { 47 | if (!is_array($this->modifiedTests)) { 48 | return; 49 | } 50 | 51 | $suiteName = $suite->getName(); 52 | $runTests = []; 53 | if (empty($suiteName)) { 54 | return; 55 | } 56 | 57 | $tests = $suite->tests(); 58 | 59 | foreach ($tests as $test) { 60 | $skipTest = 61 | $test instanceof TestCase && 62 | !$this->hasTestChanged( 63 | $test 64 | ); 65 | 66 | if ($skipTest) { 67 | continue; 68 | } 69 | $runTests[] = $test; 70 | } 71 | 72 | $suite->setTests($runTests); 73 | } 74 | public function startTest(Test $test) 75 | { 76 | } 77 | public function endTest(Test $test, $time) 78 | { 79 | } 80 | public function addIncompleteTest(Test $test, Exception $e, $time) 81 | { 82 | } 83 | public function addSkippedTest(Test $test, Exception $e, $time) 84 | { 85 | } 86 | public function endTestSuite(TestSuite $suite) 87 | { 88 | } 89 | public function onFatalError() 90 | { 91 | } 92 | public function onCancel() 93 | { 94 | } 95 | 96 | public function startsWith($haystack, $needle) 97 | { 98 | $length = strlen($needle); 99 | return (substr($haystack, 0, $length) === $needle); 100 | } 101 | 102 | protected function shouldRunTest($modifiedTest, $currentTest, $class) 103 | { 104 | foreach ($modifiedTest as $test) { 105 | $testName = $currentTest->getName(); 106 | $testMatches = 107 | strpos($class, get_class($currentTest)) !== false && 108 | ( 109 | empty($test) || 110 | strpos($test, $testName) !== false 111 | ) 112 | ; 113 | if ($testMatches) { 114 | return true; 115 | } 116 | } 117 | return false; 118 | } 119 | 120 | private function hasTestChanged(TestCase $test) 121 | { 122 | foreach ($this->modifiedTests as $class => $modifiedTest) { 123 | if ($this->shouldRunTest($modifiedTest, $test, $class)) { 124 | return true; 125 | } 126 | } 127 | 128 | return false; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/DiffLineHandle.php: -------------------------------------------------------------------------------- 1 | diffFileState = $diff; 11 | } 12 | 13 | /** 14 | * If the line is valid, this function will run on that file 15 | */ 16 | abstract public function handle(string $line); 17 | 18 | /** 19 | * Check if the line is valid in the current context 20 | */ 21 | abstract public function isValid(string $line); 22 | } 23 | -------------------------------------------------------------------------------- /src/DiffLineHandle/ContextLine.php: -------------------------------------------------------------------------------- 1 | diffFileState->addChangeLine(); 11 | } 12 | 13 | public function isValid(string $line): bool 14 | { 15 | return $line[0] == '+'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DiffLineHandle/NewVersion/DiffStart.php: -------------------------------------------------------------------------------- 1 | diffFileState->setCurrentPosition($newFrom - 1); 18 | } 19 | 20 | public function isValid(string $line): bool 21 | { 22 | return $line[0] == '@' && $line[1] == '@'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DiffLineHandle/NewVersion/NewFile.php: -------------------------------------------------------------------------------- 1 | diffFileState->setCurrentFile($currentFileName); 19 | } 20 | } 21 | 22 | public function isValid(string $line): bool 23 | { 24 | return $line[0] == '+' && $line[1] == '+'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DiffLineHandle/NewVersion/RemovedLine.php: -------------------------------------------------------------------------------- 1 | diffFileState->decrementCurrentPosition(); 11 | } 12 | 13 | public function isValid(string $line): bool 14 | { 15 | return $line[0] == '-'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DiffLineHandle/OldVersion/AddedLine.php: -------------------------------------------------------------------------------- 1 | diffFileState->decrementCurrentPosition(); 12 | $this->diffFileState->addChangeLine(); 13 | } 14 | 15 | public function isValid(string $line): bool 16 | { 17 | return $line[0] == '+' && $line[1] != "+"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/DiffLineHandle/OldVersion/DiffStart.php: -------------------------------------------------------------------------------- 1 | diffFileState->setCurrentPosition($oldFrom - 1); 19 | } 20 | 21 | public function isValid(string $line): bool 22 | { 23 | return $line[0] == '@' && $line[1] == '@'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/DiffLineHandle/OldVersion/NewFile.php: -------------------------------------------------------------------------------- 1 | .*)#', $line, $match)) { 13 | $this->diffFileState->setCurrentFile($match['filename']); 14 | } 15 | } 16 | 17 | public function isValid(string $line): bool 18 | { 19 | return $line[0] == '-' && $line[1] == '-'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DiffLineHandle/OldVersion/RemovedLine.php: -------------------------------------------------------------------------------- 1 | diffFileState->addChangeLine(); 12 | } 13 | 14 | public function isValid(string $line): bool 15 | { 16 | return $line[0] == '-'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Exceptions/ArgumentNotFound.php: -------------------------------------------------------------------------------- 1 | fileEndsWith($file, $needle)) { 21 | return $file; 22 | } 23 | } 24 | 25 | throw new FileNotFound(); 26 | } 27 | 28 | /** 29 | * Find if two strings end in the same way 30 | */ 31 | protected function fileEndsWith(string $haystack, string $needle): bool 32 | { 33 | $length = strlen($needle); 34 | if (strlen($haystack) < $length) { 35 | return $this->fileEndsWith($needle, $haystack); 36 | } 37 | 38 | $haystack = str_replace('\\', '/', $haystack); 39 | $needle = str_replace('\\', '/', $needle); 40 | 41 | return (substr($haystack, -$length) === $needle); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/FileMatchers/FileMapper.php: -------------------------------------------------------------------------------- 1 | originalPath = $originalPath; 25 | $this->newPath = $newPath; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function match(string $needle, array $haystack): string 32 | { 33 | foreach ($haystack as $file) { 34 | if ($this->checkMapping($file, $needle)) { 35 | return $file; 36 | } 37 | } 38 | 39 | throw new FileNotFound(); 40 | } 41 | 42 | private function checkMapping(string $file, string $needle): bool 43 | { 44 | return $file == str_replace( 45 | $this->originalPath, 46 | $this->newPath, 47 | $needle 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/FileMatchers/Prefix.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function match(string $needle, array $haystack): string 31 | { 32 | foreach ($haystack as $file) { 33 | if ($file == $this->prefix . $needle) { 34 | return $file; 35 | } 36 | } 37 | 38 | throw new FileNotFound(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/FileParser.php: -------------------------------------------------------------------------------- 1 | sourceCode = $sourceCode; 21 | $parser = (new ParserFactory)->createForHostVersion(); 22 | $this->parse($parser); 23 | } 24 | 25 | /** 26 | * @return CodeLimits[] 27 | */ 28 | public function getClassLimits() 29 | { 30 | return $this->classes; 31 | } 32 | 33 | /** 34 | * @return CodeLimits[] 35 | */ 36 | public function getFunctionLimits() 37 | { 38 | return $this->functions; 39 | } 40 | 41 | protected function parse(Parser $parser) 42 | { 43 | try { 44 | $ast = $parser->parse($this->sourceCode); 45 | } catch (Error $exception) { 46 | return; 47 | } 48 | 49 | foreach ($ast as $node) { 50 | $this->handleNode($node); 51 | } 52 | } 53 | 54 | protected function getCodeLimits(Node $node): CodeLimits 55 | { 56 | $startLine = $node->getAttribute('startLine'); 57 | $endLine = $node->getAttribute('endLine'); 58 | if ($node->getDocComment()) { 59 | $startLine = $node->getDocComment()->getStartLine(); 60 | } 61 | 62 | return new CodeLimits($startLine, $endLine); 63 | } 64 | 65 | protected function addClass($classLimits) 66 | { 67 | $this->classes[] = $classLimits; 68 | } 69 | 70 | protected function addFunction($classLimits) 71 | { 72 | $this->functions[] = $classLimits; 73 | } 74 | 75 | protected function handleClass(ClassLike $node) 76 | { 77 | $this->addClass($this->getCodeLimits($node)); 78 | 79 | foreach ($node->getMethods() as $function) { 80 | $this->handleNode($function); 81 | } 82 | } 83 | 84 | protected function handleFunction(FunctionLike $node) 85 | { 86 | $this->addFunction($this->getCodeLimits($node)); 87 | } 88 | 89 | private function handleNamespace(Namespace_ $node) 90 | { 91 | foreach ($node->stmts as $part) { 92 | $this->handleNode($part); 93 | } 94 | } 95 | 96 | protected function handleNode(Node $node) 97 | { 98 | $type = $node->getType(); 99 | if ($type == 'Stmt_Namespace') { 100 | $this->handleNamespace($node); 101 | } 102 | 103 | if ($type == 'Stmt_Class') { 104 | $this->handleClass($node); 105 | } 106 | 107 | if ($type == "Stmt_Function" || $type == "Stmt_ClassMethod") { 108 | $this->handleFunction($node); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Loaders/Buddy.php: -------------------------------------------------------------------------------- 1 | .*?):(?P[0-9]+) \| (?P.*)$#'; 14 | 15 | public static function getDescription(): string 16 | { 17 | return 'Parses buddy (magic number detection) output'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Loaders/Checkstyle.php: -------------------------------------------------------------------------------- 1 | file = $file; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function parseLines(): array 36 | { 37 | $this->coveredLines = []; 38 | $reader = new XMLReader; 39 | $reader->open($this->file); 40 | $currentFile = ''; 41 | while ($reader->read()) { 42 | $currentFile = $this->handleFile($reader, $currentFile); 43 | 44 | $this->handleErrors($reader, $currentFile); 45 | } 46 | 47 | return array_keys($this->coveredLines); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getErrorsOnLine(string $file, int $line) 54 | { 55 | $errors = []; 56 | if (isset($this->coveredLines[$file][$line])) { 57 | $errors = $this->coveredLines[$file][$line]; 58 | } 59 | 60 | return $errors; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function handleNotFoundFile() 67 | { 68 | return true; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public static function getDescription(): string 75 | { 76 | return 'Parses a report in checkstyle format'; 77 | } 78 | 79 | protected function handleErrors(XMLReader $reader, string $currentFile) 80 | { 81 | if ($reader->name === "error") { 82 | $this->coveredLines 83 | [$currentFile] 84 | [$reader->getAttribute('line')][] 85 | = $reader->getAttribute("message"); 86 | } 87 | } 88 | 89 | protected function handleFile(XMLReader $reader, string $currentFile): string 90 | { 91 | if (( 92 | $reader->name === "file" && 93 | $reader->nodeType == XMLReader::ELEMENT 94 | )) { 95 | $currentFile = $reader->getAttribute('name'); 96 | $trim = './'; 97 | $currentFile = substr($currentFile, strlen($trim)); 98 | $this->coveredLines[$currentFile] = []; 99 | } 100 | return $currentFile; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Loaders/Clover.php: -------------------------------------------------------------------------------- 1 | file = $file; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function parseLines(): array 36 | { 37 | $this->coveredLines = []; 38 | $reader = new XMLReader; 39 | $reader->open($this->file); 40 | $currentFile = ''; 41 | while ($reader->read()) { 42 | $currentFile = $this->checkForNewFiles($reader, $currentFile); 43 | 44 | $this->handleStatement($reader, $currentFile); 45 | } 46 | 47 | return array_keys($this->coveredLines); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getErrorsOnLine(string $file, int $lineNumber) 54 | { 55 | if (!isset($this->coveredLines[$file][$lineNumber])) { 56 | return null; 57 | } 58 | return $this->coveredLines[$file][$lineNumber] > 0 ? 59 | []: 60 | ['No unit test covering this line'] 61 | ; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function handleNotFoundFile() 68 | { 69 | return null; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public static function getDescription(): string 76 | { 77 | return 'Parses text output in clover (xml) format'; 78 | } 79 | 80 | protected function checkForNewFiles(XMLReader $reader, string $currentFile) 81 | { 82 | if (( 83 | $reader->name === "file" && 84 | $reader->nodeType == XMLReader::ELEMENT 85 | )) { 86 | $currentFile = $reader->getAttribute('name'); 87 | $this->coveredLines[$currentFile] = []; 88 | } 89 | return $currentFile; 90 | } 91 | 92 | protected function addLine(XMLReader $reader, string $currentFile) 93 | { 94 | $covered = $reader->getAttribute('count') > 0; 95 | $line = $this->coveredLines 96 | [$currentFile] 97 | [$reader->getAttribute('num')] ?? 0; 98 | 99 | $this->coveredLines 100 | [$currentFile] 101 | [$reader->getAttribute('num')] = $line + $covered; 102 | } 103 | 104 | protected function handleStatement(XMLReader $reader, string $currentFile) 105 | { 106 | if (( 107 | $reader->name === "line" && 108 | $reader->getAttribute("type") == "stmt" 109 | )) { 110 | $this->addLine($reader, $currentFile); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Loaders/CodeClimate.php: -------------------------------------------------------------------------------- 1 | convertToJson(file_get_contents($file)); 29 | $this->file = json_decode($json); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function parseLines(): array 36 | { 37 | foreach ($this->file as $line) { 38 | $this->addError($line); 39 | } 40 | 41 | return array_keys($this->errors); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function getErrorsOnLine(string $file, int $lineNumber) 48 | { 49 | $errors = []; 50 | if (isset($this->errors[$file][$lineNumber])) { 51 | $errors = $this->errors[$file][$lineNumber]; 52 | } 53 | 54 | return $errors; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function handleNotFoundFile() 61 | { 62 | return true; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public static function getDescription(): string 69 | { 70 | return 'Parse codeclimate output'; 71 | } 72 | 73 | private function addError($line) 74 | { 75 | $trim = './'; 76 | $fileName = substr($line->location->path, strlen($trim)); 77 | $start = $line->location->lines->begin; 78 | $end = $line->location->lines->end; 79 | $message = $line->description; 80 | 81 | for ($lineNumber = $start; $lineNumber <= $end; $lineNumber++) { 82 | $this->errors[$fileName][$lineNumber][] = $message; 83 | } 84 | } 85 | 86 | private function convertToJson($codeClimateFormat) 87 | { 88 | $codeClimateFormat = str_replace("\0", ',', $codeClimateFormat); 89 | 90 | return '[' . $codeClimateFormat . ']'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Loaders/Generic.php: -------------------------------------------------------------------------------- 1 | file = $file; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function parseLines(): array 38 | { 39 | $handle = fopen($this->file, 'r'); 40 | while (($line = fgets($handle)) !== false) { 41 | if (!$this->checkForFile($line)) { 42 | continue; 43 | } 44 | 45 | $this->addError($line); 46 | } 47 | 48 | return array_keys($this->errors); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getErrorsOnLine(string $file, int $lineNumber) 55 | { 56 | $errors = []; 57 | if (isset($this->errors[$file][$lineNumber])) { 58 | $errors = $this->errors[$file][$lineNumber]; 59 | } 60 | 61 | return $errors; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function handleNotFoundFile() 68 | { 69 | return true; 70 | } 71 | 72 | 73 | private function checkForFile(string $line) 74 | { 75 | return preg_match($this->lineMatch, $line); 76 | } 77 | 78 | private function addError(string $line) 79 | { 80 | $matches = []; 81 | if (preg_match($this->lineMatch, $line, $matches)) { 82 | $this->errors 83 | [$matches['fileName']] 84 | [$matches['lineNumber']][] = trim($matches['message']); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Loaders/Humbug.php: -------------------------------------------------------------------------------- 1 | json = json_decode(file_get_contents($filePath)); 35 | if (json_last_error() !== JSON_ERROR_NONE) { 36 | throw new InvalidArgumentException( 37 | "Can't Parse Humbug json - " . json_last_error_msg() 38 | ); 39 | } 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function parseLines(): array 46 | { 47 | $this->invalidLines = []; 48 | foreach ($this->errorMethods as $failures) { 49 | foreach ($this->json->$failures as $errors) { 50 | $fileName = $errors->file; 51 | $lineNumber = $errors->line; 52 | $error = "Failed on $failures check"; 53 | if (!empty($errors->diff)) { 54 | $error .= "\nDiff:\n" . $errors->diff; 55 | } 56 | 57 | $this->invalidLines[$fileName][$lineNumber] = $error; 58 | } 59 | } 60 | 61 | return array_keys($this->invalidLines); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getErrorsOnLine(string $file, int $lineNumber) 68 | { 69 | $errors = []; 70 | if (isset($this->invalidLines[$file][$lineNumber])) { 71 | $errors = (array) $this->invalidLines[$file][$lineNumber]; 72 | } 73 | 74 | return $errors; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function handleNotFoundFile() 81 | { 82 | return true; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public static function getDescription(): string 89 | { 90 | return 'Parses the json report format of humbug (mutation testing)'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Loaders/Infection.php: -------------------------------------------------------------------------------- 1 | file = fopen($filePath, 'r'); 29 | } 30 | 31 | /** 32 | * @return array the list of files from this change 33 | */ 34 | public function parseLines(): array 35 | { 36 | $this->currentFile = ''; 37 | $this->currentLine = 0; 38 | $this->partialError = ''; 39 | $this->currentType = ''; 40 | 41 | while (($line = fgets($this->file)) !== false) { 42 | $this->handleLine($line); 43 | } 44 | // the last error in the file 45 | $this->addError(); 46 | 47 | return array_keys($this->errors); 48 | } 49 | 50 | /** 51 | * Method to determine if the line is valid in the context 52 | * returning null does not include the line in the stats 53 | * Returns an array containing errors on a certain line - empty array means no errors 54 | * 55 | * @return array|null 56 | */ 57 | public function getErrorsOnLine(string $file, int $lineNumber) 58 | { 59 | if (!isset($this->errors[$file][$lineNumber])) { 60 | return []; 61 | } 62 | 63 | return $this->errors[$file][$lineNumber]; 64 | } 65 | 66 | /** 67 | * Method to determine what happens to files which have not been found 68 | * true adds as covered 69 | * false adds as uncovered 70 | * null does not include the file in the stats 71 | * @return bool|null 72 | */ 73 | public function handleNotFoundFile() 74 | { 75 | return true; 76 | } 77 | 78 | /** 79 | * Shows the description of the class, used for explaining why 80 | * this checker would be used 81 | * @return string 82 | */ 83 | public static function getDescription(): string 84 | { 85 | return 'Parses the infection text log format'; 86 | } 87 | 88 | protected function updateType($line) 89 | { 90 | $matches = []; 91 | if (preg_match('/^([a-z ]+):$/i', $line, $matches)) { 92 | $this->addError(); 93 | $this->currentFile = ''; 94 | $this->currentLine = ''; 95 | $this->partialError = ''; 96 | $this->currentType = $matches[1]; 97 | 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | 104 | protected function updateFile($line) 105 | { 106 | $matches = []; 107 | if (preg_match('/^[0-9]+\) (.*?):([0-9]+) (.*)/i', $line, $matches)) { 108 | $this->addError(); 109 | $this->currentFile = $matches[1]; 110 | $this->currentLine = $matches[2]; 111 | $this->partialError = ''; 112 | 113 | return true; 114 | } 115 | 116 | return false; 117 | } 118 | 119 | protected function addError() 120 | { 121 | if (!($this->currentFile && $this->currentLine)) { 122 | return; 123 | } 124 | 125 | if (!in_array($this->currentType, $this->errorTypes)) { 126 | return; 127 | } 128 | 129 | $this->errors 130 | [$this->currentFile] 131 | [$this->currentLine][] = $this->currentType . $this->partialError; 132 | } 133 | 134 | protected function handleLine($line) 135 | { 136 | if ($this->updateType($line)) { 137 | return; 138 | } 139 | 140 | if ($this->updateFile($line)) { 141 | return; 142 | } 143 | 144 | $this->partialError .= $line; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Loaders/Jacoco.php: -------------------------------------------------------------------------------- 1 | coveredLines = []; 19 | $reader = new XMLReader; 20 | $reader->open($this->file); 21 | $currentNamespace = ''; 22 | $currentFile = ''; 23 | 24 | while ($reader->read()) { 25 | $currentNamespace = $this->findNamespace($reader, $currentNamespace); 26 | 27 | $currentFile = $this->findFile($reader, $currentNamespace, $currentFile); 28 | 29 | $this->addLine($reader, $currentFile); 30 | } 31 | 32 | return array_keys($this->coveredLines); 33 | } 34 | 35 | public static function getDescription(): string 36 | { 37 | return 'Parses xml coverage report produced by Jacoco'; 38 | } 39 | 40 | /** 41 | * @param XMLReader $reader 42 | * @param string $currentFile 43 | */ 44 | protected function addLine(XMLReader $reader, string $currentFile) 45 | { 46 | if (( 47 | $reader->name === "line" 48 | )) { 49 | $this->coveredLines 50 | [$currentFile] 51 | [$reader->getAttribute('nr')] 52 | = $reader->getAttribute("mi") == 0; 53 | } 54 | } 55 | 56 | protected function findFile(XMLReader $reader, string $currentNamespace, string $currentFile): string 57 | { 58 | if (( 59 | $reader->name === "sourcefile" && 60 | $reader->nodeType == XMLReader::ELEMENT 61 | )) { 62 | $currentFile = $currentNamespace . '/' . $reader->getAttribute('name'); 63 | $this->coveredLines[$currentFile] = []; 64 | } 65 | 66 | return $currentFile; 67 | } 68 | 69 | protected function findNamespace(XMLReader $reader, string $currentNamespace): string 70 | { 71 | if (( 72 | $reader->name === "package" && 73 | $reader->nodeType == XMLReader::ELEMENT 74 | )) { 75 | $currentNamespace = $reader->getAttribute('name'); 76 | } 77 | return $currentNamespace; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Loaders/PhanJson.php: -------------------------------------------------------------------------------- 1 | file = json_decode(file_get_contents($file)); 14 | } 15 | 16 | public static function getDescription(): string 17 | { 18 | return 'Parses phan (static analysis) in json format'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Loaders/PhanText.php: -------------------------------------------------------------------------------- 1 | .*?):(?P[0-9]+)(?P.*)#'; 14 | 15 | /* 16 | * @inheritdoc 17 | */ 18 | public static function getDescription(): string 19 | { 20 | return 'Parse the default phan(static analysis) output'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Loaders/PhpCs.php: -------------------------------------------------------------------------------- 1 | json = json_decode(file_get_contents($filePath)); 75 | if (json_last_error() !== JSON_ERROR_NONE) { 76 | throw new InvalidArgumentException( 77 | "Can't Parse phpcs json - " . json_last_error_msg() 78 | ); 79 | } 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function parseLines(): array 86 | { 87 | $this->invalidLines = []; 88 | foreach ($this->json->files as $fileName => $file) { 89 | foreach ($file->messages as $message) { 90 | $this->addInvalidLine($fileName, $message); 91 | } 92 | } 93 | 94 | return array_unique(array_merge( 95 | array_keys($this->invalidLines), 96 | array_keys($this->invalidFiles), 97 | array_keys($this->invalidRanges) 98 | )); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function getErrorsOnLine(string $file, int $lineNumber) 105 | { 106 | $errors = []; 107 | if (!empty($this->invalidFiles[$file])) { 108 | $errors = $this->invalidFiles[$file]; 109 | } 110 | 111 | if (!empty($this->invalidLines[$file][$lineNumber])) { 112 | $errors = array_merge($errors, $this->invalidLines[$file][$lineNumber]); 113 | } 114 | 115 | $errors = array_merge($errors, $this->getRangeErrors($file, $lineNumber)); 116 | 117 | return $errors; 118 | } 119 | 120 | protected function addInvalidLine(string $file, stdClass $message) 121 | { 122 | if (!in_array($message->type, $this->failOnTypes)) { 123 | return; 124 | } 125 | 126 | $line = $message->line; 127 | 128 | $error = $this->messageStartsWith($message->source, $this->lookupErrorPrefix); 129 | 130 | if ($error && !in_array($message->source, $this->functionIgnoreComments, true)) { 131 | $this->handleLookupError($file, $message, $error); 132 | return; 133 | } 134 | 135 | if (!isset($this->invalidLines[$file][$line])) { 136 | $this->invalidLines[$file][$line] = []; 137 | } 138 | 139 | $this->invalidLines[$file][$line][] = $message->message; 140 | 141 | if (in_array($message->source, $this->wholeFileErrors)) { 142 | $this->invalidFiles[$file][] = $message->message; 143 | } 144 | } 145 | 146 | /** 147 | * @return bool|string 148 | */ 149 | protected function messageStartsWith(string $message, array $list) 150 | { 151 | foreach ($list as $item) { 152 | if (strpos($message, $item) === 0) { 153 | return $item; 154 | } 155 | } 156 | return false; 157 | } 158 | 159 | protected function handleLookupError($file, $message, $error) 160 | { 161 | if ($error == 'Squiz.Commenting.FileComment') { 162 | $this->invalidFiles[$file][] = $message->message; 163 | } 164 | try { 165 | $fileParser = $this->getFileParser($file); 166 | $lookup = $this->getMessageRanges($error, $fileParser); 167 | 168 | $this->addRangeError($file, $lookup, $message); 169 | } catch (FileNotFound $exception) { 170 | error_log("Can't find file, may have missed an error"); 171 | } 172 | } 173 | 174 | protected function getFileParser($filename) 175 | { 176 | if (!isset($this->parsedFiles[$filename])) { 177 | if (!file_exists($filename)) { 178 | throw new FileNotFound(); 179 | } 180 | 181 | $this->parsedFiles[$filename] = new FileParser( 182 | file_get_contents($filename) 183 | ); 184 | } 185 | 186 | return $this->parsedFiles[$filename]; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function handleNotFoundFile() 193 | { 194 | return true; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public static function getDescription(): string 201 | { 202 | return 'Parses the json report format of phpcs, this mode ' . 203 | 'only reports errors as violations'; 204 | } 205 | 206 | /** 207 | * @param string $file 208 | * @param CodeLimits[] $lookup 209 | * @param stdClass $message 210 | */ 211 | protected function addRangeError($file, $lookup, $message) 212 | { 213 | $line = $message->line; 214 | foreach ($lookup as $limit) { 215 | if ($line >= $limit->getStartLine() && $line <= $limit->getEndLine()) { 216 | $this->invalidRanges[$file][] = [ 217 | 'from' => $limit->getStartLine(), 218 | 'to' => $limit->getEndLine(), 219 | 'message' => $message->message, 220 | ]; 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * @param string $error 227 | * @param FileParser $fileParser 228 | * @return mixed 229 | */ 230 | protected function getMessageRanges($error, $fileParser) 231 | { 232 | if ($error == 'Squiz.Commenting.ClassComment') { 233 | return $fileParser->getClassLimits(); 234 | } 235 | 236 | return $fileParser->getFunctionLimits(); 237 | } 238 | 239 | /** 240 | * @param string $file 241 | * @param int $lineNumber 242 | * @return array errors on the line 243 | */ 244 | protected function getRangeErrors($file, $lineNumber) 245 | { 246 | $errors = []; 247 | 248 | if (!empty($this->invalidRanges[$file])) { 249 | foreach ($this->invalidRanges[$file] as $invalidRange) { 250 | $inRange = $lineNumber >= $invalidRange['from'] && 251 | $lineNumber <= $invalidRange['to']; 252 | if ($inRange) { 253 | $errors[] = $invalidRange['message']; 254 | } 255 | } 256 | } 257 | 258 | return $errors; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/Loaders/PhpCsStrict.php: -------------------------------------------------------------------------------- 1 | file = $file; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function parseLines(): array 42 | { 43 | $this->errors = []; 44 | $this->errorRanges = []; 45 | $reader = new XMLReader; 46 | $reader->open($this->file); 47 | $currentFile = ""; 48 | while ($reader->read()) { 49 | $currentFile = $this->checkForNewFile($reader, $currentFile); 50 | $this->checkForViolation($reader, $currentFile); 51 | } 52 | 53 | return array_keys($this->errors); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getErrorsOnLine(string $file, int $lineNumber) 60 | { 61 | $errors = []; 62 | if (empty($this->errorRanges[$file])) { 63 | return $errors; 64 | } 65 | 66 | foreach ($this->errorRanges[$file] as $number => $error) { 67 | if (( 68 | $error['start'] <= $lineNumber && 69 | $error['end'] >= $lineNumber 70 | )) { 71 | $errors[] = $error['error']; 72 | unset($this->errorRanges[$file][$number]); 73 | } 74 | } 75 | 76 | return $errors; 77 | } 78 | 79 | /** 80 | * @param XMLReader $reader 81 | * @param string $currentFile 82 | */ 83 | protected function checkForViolation(XMLReader $reader, $currentFile) 84 | { 85 | if (( 86 | $reader->name === 'violation' && 87 | $reader->nodeType == XMLReader::ELEMENT 88 | )) { 89 | $error = trim($reader->readString()); 90 | $start = $reader->getAttribute('beginline'); 91 | $end = $reader->getAttribute('endline'); 92 | $this->errorRanges[$currentFile][] = [ 93 | 'start' => $start, 94 | 'end' => $end, 95 | 'error' => $error, 96 | ]; 97 | 98 | $this->addForAllLines($currentFile, $start, $end, $error); 99 | } 100 | } 101 | 102 | /** 103 | * @param XMLReader $reader 104 | * @param string $currentFile 105 | * @return string the currentFileName 106 | */ 107 | protected function checkForNewFile(XMLReader $reader, $currentFile) 108 | { 109 | if (( 110 | $reader->name === 'file' && 111 | $reader->nodeType == XMLReader::ELEMENT 112 | ) 113 | ) { 114 | $currentFile = $reader->getAttribute('name'); 115 | $this->errors[$currentFile] = []; 116 | return $currentFile; 117 | } 118 | return $currentFile; 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function handleNotFoundFile() 125 | { 126 | return true; 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public static function getDescription(): string 133 | { 134 | return 'Parses the xml report format of phpmd, this mode ' . 135 | 'reports multi line violations once per diff, instead ' . 136 | 'of on each line the violation occurs'; 137 | } 138 | 139 | protected function addForAllLines($currentFile, $start, $end, $error) 140 | { 141 | for ($i = $start; $i <= $end; $i++) { 142 | if (( 143 | !isset($this->errors[$currentFile][$i]) || 144 | !in_array($error, $this->errors[$currentFile][$i]) 145 | )) { 146 | $this->errors[$currentFile][$i][] = $error; 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Loaders/PhpMdStrict.php: -------------------------------------------------------------------------------- 1 | errorRanges[$file] as $error) { 16 | if (( 17 | $error['start'] <= $lineNumber && 18 | $error['end'] >= $lineNumber 19 | )) { 20 | $errors[] = $error['error']; 21 | } 22 | } 23 | 24 | return $errors; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public static function getDescription(): string 31 | { 32 | return 'Parses the xml report format of phpmd, this mode ' . 33 | 'reports multi line violations once per line they occur '; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Loaders/PhpMnd.php: -------------------------------------------------------------------------------- 1 | file = fopen($filename, 'r'); 15 | } 16 | 17 | /** 18 | * @inheritdoc 19 | */ 20 | public function parseLines(): array 21 | { 22 | while (($line = fgets($this->file)) !== false) { 23 | $matches = []; 24 | $pattern = "/^(?[^:]+):(?[0-9]+)\.? (?.+)/"; 25 | if (preg_match($pattern, $line, $matches)) { 26 | $this->invalidLines 27 | [$matches['filename']] 28 | [$matches['lineNo']][] = $matches['message']; 29 | } 30 | } 31 | 32 | return array_keys($this->invalidLines); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function getErrorsOnLine(string $file, int $lineNumber) 39 | { 40 | $errors = []; 41 | if (isset($this->invalidLines[$file][$lineNumber])) { 42 | $errors = $this->invalidLines[$file][$lineNumber]; 43 | } 44 | 45 | return $errors; 46 | } 47 | 48 | /** 49 | * return as true to include files, phpmnd only shows files with errors 50 | */ 51 | public function handleNotFoundFile() 52 | { 53 | return true; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public static function getDescription(): string 60 | { 61 | return 'Parses the text output of phpmnd (Magic Number Detection)'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Loaders/PhpMndXml.php: -------------------------------------------------------------------------------- 1 | file = $filename; 17 | } 18 | 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function parseLines(): array 23 | { 24 | $reader = new XMLReader; 25 | $reader->open($this->file); 26 | $currentFile = ''; 27 | while ($reader->read()) { 28 | $currentFile = $this->checkForNewFiles($reader, $currentFile); 29 | 30 | $this->handleLine($reader, $currentFile); 31 | $this->handleErrors($reader, $currentFile); 32 | } 33 | 34 | return array_keys($this->invalidLines); 35 | } 36 | 37 | protected function checkForNewFiles(XMLReader $reader, $currentFile) 38 | { 39 | if (( 40 | $reader->name === "file" && 41 | $reader->nodeType == XMLReader::ELEMENT 42 | )) { 43 | $currentFile = $reader->getAttribute('path'); 44 | $this->invalidLines[$currentFile] = []; 45 | } 46 | return $currentFile; 47 | } 48 | 49 | protected function handleLine(XMLReader $reader, $currentFile) 50 | { 51 | if ($reader->name === "entry") { 52 | $this->currentLine = $reader->getAttribute("line"); 53 | if (!isset($this->invalidLines[$currentFile][$this->currentLine])) { 54 | $this->invalidLines[$currentFile][$this->currentLine] = []; 55 | } 56 | } 57 | } 58 | 59 | protected function handleErrors(XMLReader $reader, $currentFile) 60 | { 61 | if (( 62 | $reader->name === "snippet" && 63 | $reader->nodeType == XMLReader::ELEMENT 64 | )) { 65 | $this->invalidLines[$currentFile][$this->currentLine][] = $reader->readString(); 66 | } 67 | } 68 | 69 | /** 70 | * @inheritdoc 71 | */ 72 | public function getErrorsOnLine(string $file, int $lineNumber) 73 | { 74 | $errors = []; 75 | if (isset($this->invalidLines[$file][$lineNumber])) { 76 | $errors = $this->invalidLines[$file][$lineNumber]; 77 | } 78 | 79 | return $errors; 80 | } 81 | 82 | /** 83 | * return as true to include files, phpmnd only shows files with errors 84 | */ 85 | public function handleNotFoundFile() 86 | { 87 | return true; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public static function getDescription(): string 94 | { 95 | return 'Parses the XML output of phpmnd (Magic Number Detection)'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Loaders/PhpStan.php: -------------------------------------------------------------------------------- 1 | [0-9]+)/'; 18 | 19 | protected $file; 20 | protected $relatedRegex = '#(function|method) (?:(?P.*?)::)?(?P.*?)[ \(]#'; 21 | 22 | /** 23 | * @var array 24 | */ 25 | protected $invalidLines = []; 26 | 27 | /** 28 | * @param string $filename the path to the phpstan.txt file 29 | */ 30 | public function __construct($filename) 31 | { 32 | $this->file = fopen($filename, 'r'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function parseLines(): array 39 | { 40 | $filename = ''; 41 | $lineNumber = 0; 42 | while (($line = fgets($this->file)) !== false) { 43 | $filename = $this->checkForFilename($line, $filename); 44 | $lineNumber = $this->getLineNumber($line, $lineNumber); 45 | 46 | if ($lineNumber) { 47 | $error = $this->getMessage($line); 48 | if ($this->isExtendedMessage($line)) { 49 | $this->appendError($filename, $lineNumber, $error); 50 | continue; 51 | } 52 | $this->handleRelatedError($filename, $lineNumber, $error); 53 | $this->addError($filename, $lineNumber, $error); 54 | } 55 | } 56 | 57 | $this->trimLines(); 58 | 59 | return array_keys($this->invalidLines); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getErrorsOnLine(string $file, int $lineNumber) 66 | { 67 | $errors = []; 68 | if (isset($this->invalidLines[$file][$lineNumber])) { 69 | $errors = $this->invalidLines[$file][$lineNumber]; 70 | } 71 | 72 | return $errors; 73 | } 74 | 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function handleNotFoundFile() 80 | { 81 | return true; 82 | } 83 | 84 | /** 85 | * @param string $line 86 | * @param string $currentFile 87 | * @return string the currentFileName 88 | */ 89 | protected function checkForFilename($line, $currentFile) 90 | { 91 | if (strpos($line, " Line ")) { 92 | return trim(str_replace('Line', '', $line)); 93 | } 94 | return $currentFile; 95 | } 96 | 97 | protected function getLineNumber(string $line, int $currentLineNumber) 98 | { 99 | $matches = []; 100 | if (!preg_match($this->lineRegex, $line, $matches)) { 101 | if (preg_match('#^\s{3,}#', $line)) { 102 | return $currentLineNumber; 103 | } 104 | 105 | return false; 106 | } 107 | 108 | return (int) $matches['lineNumber']; 109 | } 110 | 111 | protected function getMessage($line) 112 | { 113 | return trim(preg_replace($this->lineRegex, '', $line)); 114 | } 115 | 116 | protected function isExtendedMessage($line) 117 | { 118 | return preg_match($this->lineRegex, $line) === 0; 119 | } 120 | 121 | protected function trimLines() 122 | { 123 | array_walk_recursive($this->invalidLines, function (&$item) { 124 | if (is_string($item)) { 125 | $item = trim($item); 126 | } 127 | }); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public static function getDescription(): string 134 | { 135 | return 'Parses the text output of phpstan'; 136 | } 137 | 138 | protected function handleRelatedError($filename, $line, $error) 139 | { 140 | 141 | $matches = []; 142 | if (preg_match($this->relatedRegex, $error, $matches)) { 143 | $error = sprintf( 144 | '%s (used %s line %d)', 145 | $error, 146 | $filename, 147 | $line 148 | ); 149 | 150 | try { 151 | $reflection = $this->getReflector($matches); 152 | $filename = $reflection->getFileName(); 153 | $currentLine = $reflection->getStartLine(); 154 | 155 | while ($currentLine < $reflection->getEndLine()) { 156 | $this->addError($filename, $currentLine++, $error); 157 | } 158 | } catch (Exception $exception) { 159 | // can't find any more info about this method, so just carry on 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * @param string $filename 166 | * @param int $lineNumber 167 | * @param string $error 168 | */ 169 | protected function addError($filename, $lineNumber, $error) 170 | { 171 | if (!isset($this->invalidLines[$filename][$lineNumber])) { 172 | $this->invalidLines[$filename][$lineNumber] = []; 173 | } 174 | $this->invalidLines[$filename][$lineNumber][] = $error; 175 | } 176 | 177 | protected function getReflector(array $matches): ReflectionFunctionAbstract 178 | { 179 | if ($matches['class']) { 180 | return $this->getClassReflector($matches); 181 | } 182 | 183 | return $this->getFunctionReflector($matches); 184 | } 185 | 186 | private function appendError(string $filename, int $lineNumber, string $error) 187 | { 188 | end($this->invalidLines[$filename][$lineNumber]); 189 | $key = key($this->invalidLines[$filename][$lineNumber]); 190 | $this->invalidLines[$filename][$lineNumber][$key] .= ' ' . $error; 191 | } 192 | 193 | protected function getClassReflector(array $matches): ReflectionMethod 194 | { 195 | if (!method_exists($matches['class'], $matches['function'])) { 196 | throw new Exception("Missing class function"); 197 | } 198 | return new ReflectionMethod( 199 | $matches['class'], 200 | $matches['function'] 201 | ); 202 | } 203 | 204 | protected function getFunctionReflector(array $matches): ReflectionFunction 205 | { 206 | if (!function_exists($matches['function'])) { 207 | throw new Exception("Missing function reflector"); 208 | } 209 | return new ReflectionFunction( 210 | $matches['function'] 211 | ); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Loaders/PhpUnit.php: -------------------------------------------------------------------------------- 1 | file = fopen($file, 'r'); 15 | } 16 | 17 | public function parseLines(): array 18 | { 19 | $block = []; 20 | $this->duplicateCode = []; 21 | while (($line = fgets($this->file)) !== false) { 22 | if (!$this->hasFileName($line)) { 23 | continue; 24 | } 25 | 26 | if ($this->startOfBlock($line)) { 27 | $this->handleEndOfBlock($block); 28 | $block = []; 29 | } 30 | 31 | $block += $this->addFoundBlock($line); 32 | } 33 | 34 | return array_keys($this->duplicateCode); 35 | } 36 | 37 | 38 | public function getErrorsOnLine(string $file, int $lineNumber) 39 | { 40 | $errors = []; 41 | if (isset($this->duplicateCode[$file][$lineNumber])) { 42 | $errors = $this->duplicateCode[$file][$lineNumber]; 43 | } 44 | 45 | return $errors; 46 | } 47 | 48 | public function handleNotFoundFile() 49 | { 50 | return true; 51 | } 52 | 53 | public static function getDescription(): string 54 | { 55 | return "Parses the text output from phpcpd (Copy Paste Detect)"; 56 | } 57 | 58 | private function startOfBlock(string $line) 59 | { 60 | return preg_match('/^\s+-/', $line); 61 | } 62 | 63 | private function hasFileName(string $line) 64 | { 65 | return preg_match('/:\d+-\d+/', $line); 66 | } 67 | 68 | private function addFoundBlock(string $line) 69 | { 70 | $matches = []; 71 | preg_match('/\s+(?:- )?(?.*?):(?\d+)-(?\d+)$/', $line, $matches); 72 | return [$matches['fileName'] => range($matches['startLine'], $matches['endLine'])]; 73 | } 74 | 75 | private function handleEndOfBlock(array $block) 76 | { 77 | foreach ($block as $filename => $lines) { 78 | foreach ($lines as $lineNumber) { 79 | foreach ($block as $duplicate => $dupeLines) { 80 | if ($filename == $duplicate) { 81 | continue; 82 | } 83 | $start = reset($dupeLines); 84 | $end = end($dupeLines); 85 | $message = "Duplicate of " . $duplicate . ':' . $start . '-' . $end; 86 | $this->duplicateCode[$filename][$lineNumber][] = $message; 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Loaders/Psalm.php: -------------------------------------------------------------------------------- 1 | file = $file; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function parseLines(): array 44 | { 45 | $this->errors = []; 46 | $this->errorRanges = []; 47 | $reader = new XMLReader; 48 | $reader->open($this->file); 49 | 50 | while ($reader->read()) { 51 | if ($this->isElementBeginning($reader, 'item')) { 52 | $this->parseItem($reader); 53 | } 54 | } 55 | 56 | return array_keys($this->errors); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function getErrorsOnLine(string $file, int $lineNumber) 63 | { 64 | $errors = []; 65 | foreach ($this->errorRanges[$file] as $number => $error) { 66 | if (( 67 | $error['start'] <= $lineNumber 68 | && $error['end'] >= $lineNumber 69 | )) { 70 | $errors[] = $error['error']; 71 | unset($this->errorRanges[$file][$number]); 72 | } 73 | } 74 | 75 | return $errors; 76 | } 77 | 78 | /** 79 | * @param XMLReader $reader 80 | */ 81 | protected function parseItem(XMLReader $reader) 82 | { 83 | $attributes = []; 84 | 85 | while ($reader->read()) { 86 | if ($this->isElementEnd($reader, 'item')) { 87 | break; 88 | } 89 | 90 | if ($reader->nodeType == XMLReader::ELEMENT) { 91 | $attributes[$reader->name] = $reader->readString(); 92 | } 93 | } 94 | 95 | $error = $attributes['message']; 96 | $start = $attributes['line_from']; 97 | $end = $attributes['line_to']; 98 | $fileName = $attributes['file_name']; 99 | 100 | $this->errorRanges[$fileName][] = [ 101 | 'start' => $start, 102 | 'end' => $end, 103 | 'error' => $error, 104 | ]; 105 | 106 | $this->addForAllLines($fileName, $start, $end, $error); 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function handleNotFoundFile() 113 | { 114 | return true; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public static function getDescription(): string 121 | { 122 | return 'Parses the xml report format of psalm'; 123 | } 124 | 125 | protected function addForAllLines($currentFile, $start, $end, $error) 126 | { 127 | for ($i = $start; $i <= $end; $i++) { 128 | if (( 129 | !isset($this->errors[$currentFile][$i]) 130 | || !in_array($error, $this->errors[$currentFile][$i]) 131 | ) 132 | ) { 133 | $this->errors[$currentFile][$i][] = $error; 134 | } 135 | } 136 | } 137 | 138 | protected function isElementBeginning(XMLReader $reader, string $name): bool 139 | { 140 | return $reader->name === $name && $reader->nodeType == XMLReader::ELEMENT; 141 | } 142 | 143 | protected function isElementEnd(XMLReader $reader, string $name): bool 144 | { 145 | return $reader->name === $name && $reader->nodeType == XMLReader::END_ELEMENT; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Loaders/Pylint.php: -------------------------------------------------------------------------------- 1 | .*?):(?P[0-9]+): \[.*?\](?P.*)#'; 14 | 15 | public static function getDescription(): string 16 | { 17 | return 'Parses PyLint output'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Output.php: -------------------------------------------------------------------------------- 1 | $lines) { 13 | foreach ($lines as $line => $error) { 14 | $violations[$file][] = (object) [ 15 | 'lineNumber' => $line, 16 | 'message' => $error 17 | ]; 18 | } 19 | } 20 | $output = (object) [ 21 | 'coverage' => number_format($percent, 2), 22 | 'status' => $percent >= $minimumPercent ? 23 | 'Passed': 24 | 'Failed', 25 | 'violations' => $violations 26 | ]; 27 | echo json_encode($output) . "\n"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Outputs/Phpcs.php: -------------------------------------------------------------------------------- 1 | violations = ['files' => []]; 14 | $total = 0; 15 | foreach ($coverage as $file => $lines) { 16 | foreach ($lines as $line => $errors) { 17 | $this->displayErrors($errors, $file, $line); 18 | } 19 | $total++; 20 | } 21 | 22 | $this->addTotal($total); 23 | 24 | echo json_encode($this->violations) . "\n"; 25 | } 26 | 27 | protected function displayErrors(array $errors, string $file, int $line) 28 | { 29 | foreach ($errors as $error) { 30 | $current = &$this->violations['files'][$file]; 31 | $current['messages'][] = [ 32 | 'message' => $error, 33 | 'source' => 'diffFilter', 34 | 'severity' => 1, 35 | 'type' => 'ERROR', 36 | 'line' => $line, 37 | 'column' => 1, 38 | 'fixable' => 'false', 39 | ]; 40 | 41 | $current['errors'] = count( 42 | $current['messages'] 43 | ); 44 | $current['warnings'] = 0; 45 | } 46 | } 47 | 48 | protected function addTotal(int $total) 49 | { 50 | $this->violations['totals'] = [ 51 | 'errors' => $total, 52 | 'fixable' => 0, 53 | 'warnings' => 0, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Outputs/Text.php: -------------------------------------------------------------------------------- 1 | $lines) { 15 | $output .= "\n\n'$filename' has no coverage for the following lines:\n"; 16 | foreach ($lines as $line => $message) { 17 | $output .= $this->generateOutputLine($line, $message); 18 | } 19 | } 20 | 21 | echo trim($output) . "\n"; 22 | } 23 | 24 | private function generateOutputLine($line, $message) 25 | { 26 | $output = "Line $line:\n"; 27 | if (!empty($message)) { 28 | foreach ($message as $part) { 29 | $output .= "\t$part\n"; 30 | } 31 | } 32 | 33 | return $output . "\n"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/PhpunitFilter.php: -------------------------------------------------------------------------------- 1 | coverage = include($coveragePhp); 17 | $this->diff = $diff; 18 | $this->matcher = $matcher; 19 | } 20 | 21 | public function getTestsForRunning($fuzziness = 0) 22 | { 23 | $changes = $this->diff->getChangedLines(); 24 | $testData = $this->coverage->getData(); 25 | $fileNames = array_keys($testData); 26 | $runTests = []; 27 | foreach ($changes as $file => $lines) { 28 | try { 29 | $found = $this->matcher->match($file, $fileNames); 30 | if ($found) { 31 | foreach ($lines as $line) { 32 | $runTests = $this->matchFuzzyLines($fuzziness, $testData, $found, $line, $runTests); 33 | } 34 | } 35 | } catch (Exception $e) { 36 | if ($this->endsWith($file, ".php")) { 37 | $runTests[] = $this->stripFileExtension($file); 38 | } 39 | } 40 | } 41 | return $this->groupTestsBySuite($runTests); 42 | } 43 | 44 | protected function endsWith(string $haystack, string $needle) 45 | { 46 | $length = strlen($needle); 47 | return (substr($haystack, -$length) === $needle); 48 | } 49 | 50 | protected function stripFileExtension(string $file) 51 | { 52 | $ext = ".php"; 53 | return str_replace('/', '\\', substr($file, 0, -strlen($ext))); 54 | } 55 | 56 | protected function groupTestsBySuite(array $tests) 57 | { 58 | $groupedTests = []; 59 | foreach ($tests as $test) { 60 | $suite = $test; 61 | $testName = ''; 62 | 63 | if (strpos($test, '::') > 0) { 64 | list ($suite, $testName) = explode('::', $test); 65 | } 66 | $groupedTests[$suite][] = $testName; 67 | } 68 | return $groupedTests; 69 | } 70 | 71 | public function matchFuzzyLines( 72 | int $fuzziness, 73 | array $testData, 74 | string $found, 75 | int $line, 76 | array $runTests 77 | ) { 78 | $index = -$fuzziness; 79 | do { 80 | if (isset($testData[$found][$line + $index])) { 81 | $runTests = array_unique( 82 | array_merge( 83 | $runTests, 84 | $testData[$found][$line] 85 | ) 86 | ); 87 | } 88 | } while (++$index < $fuzziness); 89 | 90 | return $runTests; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Runners/generic.php: -------------------------------------------------------------------------------- 1 | getArg('3')); 21 | } catch (ArgumentNotFound $exception) { 22 | $minimumPercentCovered = 100; 23 | } 24 | 25 | $matcher = new CoverageChecker\FileMatchers\EndsWith(); 26 | 27 | $diff = new CoverageChecker\DiffFileLoader( 28 | CoverageChecker\adjustForStdIn($args->getArg('1')) 29 | ); 30 | 31 | try { 32 | $autoload = $args->getArg('autoload'); 33 | if (file_exists(($autoload))) { 34 | require_once $autoload; 35 | } 36 | } catch (ArgumentNotFound $exception) { 37 | // do nothing, its not a required argument 38 | } 39 | 40 | $checkerArray = [ 41 | 'buddy' => 'Buddy', 42 | 'checkstyle' => 'Checkstyle', 43 | 'clover' => 'Clover', 44 | 'codeclimate' => 'CodeClimate', 45 | 'humbug' => 'Humbug', 46 | 'infecton' => 'Infection', 47 | 'jacoco' => 'Jacoco', 48 | 'phan' => 'PhanText', 49 | 'phanJson' => 'PhanJson', 50 | 'phpcpd' => 'Phpcpd', 51 | 'phpcs' => 'PhpCs', 52 | 'phpcsStrict' => 'PhpCsStrict', 53 | 'phpmd' => 'PhpMd', 54 | 'phpmdStrict' => 'PhpMdStrict', 55 | 'phpmnd' => 'PhpMnd', 56 | 'phpmndXml' => 'PhpMndXml', 57 | 'phpstan' => 'PhpStan', 58 | 'phpunit' => 'PhpUnit', 59 | 'pylint' => 'Pylint', 60 | 'psalm' => 'Psalm', 61 | ]; 62 | 63 | $fileCheck = CoverageChecker\getFileChecker( 64 | $args, 65 | $checkerArray, 66 | CoverageChecker\adjustForStdIn($args->getArg('2')) 67 | ); 68 | 69 | $outputArray = [ 70 | 'text' => Text::class, 71 | 'json' => Json::class, 72 | 'phpcs' => Phpcs::class, 73 | ]; 74 | try { 75 | $report = $args->getArg('report'); 76 | } catch (ArgumentNotFound $exception) { 77 | $report = 'text'; 78 | } 79 | 80 | $report = new $outputArray[$report]; 81 | 82 | $coverageCheck = new CoverageChecker\CoverageCheck($diff, $fileCheck, $matcher); 83 | 84 | $lines = $coverageCheck->getCoveredLines(); 85 | 86 | CoverageChecker\handleOutput($lines, $minimumPercentCovered, $report); 87 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | getArg('1'); 45 | $args->getArg('2'); 46 | } catch (ArgumentNotFound $exception) { 47 | throw new Exception( 48 | "Missing arguments, please call with diff and check file\n" . 49 | "e.g. vendor/bin/diffFilter --phpcs diff.txt phpcs.json", 50 | 1 51 | ); 52 | } 53 | } 54 | 55 | /** 56 | * @codeCoverageIgnore 57 | */ 58 | function adjustForStdIn(string $argument) 59 | { 60 | if ($argument == "-") { 61 | return "php://stdin"; 62 | } 63 | 64 | // @codeCoverageIgnoreStart 65 | if (strpos($argument, '/dev/fd') === 0) { 66 | return str_replace('/dev/fd', 'php://fd', $argument); 67 | } 68 | // @codeCoverageIgnoreEnd 69 | 70 | return $argument; 71 | } 72 | 73 | function getMinPercent($percent) 74 | { 75 | $minimumPercentCovered = 100; 76 | 77 | if (is_numeric($percent)) { 78 | $minimumPercentCovered = min( 79 | $minimumPercentCovered, 80 | max(0, $percent) 81 | ); 82 | } 83 | 84 | return $minimumPercentCovered; 85 | } 86 | 87 | function handleOutput(array $lines, float $minimumPercentCovered, Output $output) 88 | { 89 | $coveredLines = calculateLines($lines['coveredLines']); 90 | $uncoveredLines = calculateLines($lines['uncoveredLines']); 91 | 92 | 93 | if ($coveredLines + $uncoveredLines == 0) { 94 | error_log('No lines found!'); 95 | 96 | $output->output( 97 | $lines['uncoveredLines'], 98 | 100, 99 | $minimumPercentCovered 100 | ); 101 | return; 102 | } 103 | 104 | $percentCovered = 100 * ($coveredLines / ($coveredLines + $uncoveredLines)); 105 | 106 | $output->output( 107 | $lines['uncoveredLines'], 108 | $percentCovered, 109 | $minimumPercentCovered 110 | ); 111 | 112 | if ($percentCovered >= $minimumPercentCovered) { 113 | return; 114 | } 115 | 116 | throw new Exception( 117 | 'Failing due to coverage being lower than threshold', 118 | 2 119 | ); 120 | } 121 | 122 | function calculateLines(array $lines) 123 | { 124 | return array_sum(array_map('count', $lines)); 125 | } 126 | 127 | function addExceptionHandler() 128 | { 129 | if (( 130 | !defined('PHPUNIT_COMPOSER_INSTALL') && 131 | !defined('__PHPUNIT_PHAR__') 132 | )) { 133 | set_exception_handler( 134 | function ($exception) { 135 | // @codeCoverageIgnoreStart 136 | error_log($exception->getMessage()); 137 | exit($exception->getCode()); 138 | // @codeCoverageIgnoreEnd 139 | } 140 | ); 141 | } 142 | } 143 | 144 | function getFileChecker( 145 | ArgParser $args, 146 | array $argMapper, 147 | string $filename 148 | ): FileChecker { 149 | foreach ($argMapper as $arg => $class) { 150 | try { 151 | $args->getArg($arg); 152 | $class = __NAMESPACE__ . '\\Loaders\\' . $class; 153 | return new $class($filename); 154 | } catch (ArgumentNotFound $exception) { 155 | continue; 156 | } 157 | } 158 | printOptions($argMapper); 159 | throw new Exception("Can not find file handler"); 160 | } 161 | 162 | function printOptions(array $arguments) 163 | { 164 | $tabWidth = 8; 165 | $defaultWidth = 80; 166 | 167 | $width = (int) (`tput cols` ?: $defaultWidth); 168 | $width -= 2 * $tabWidth; 169 | foreach ($arguments as $argument => $class) { 170 | $class = __NAMESPACE__ . '\\Loaders\\' . $class; 171 | 172 | $argument = adjustArgument($argument, $tabWidth); 173 | 174 | error_log(sprintf( 175 | "%s\t%s", 176 | $argument, 177 | wordwrap( 178 | $class::getDescription(), 179 | $width, 180 | "\n\t\t", 181 | true 182 | ) 183 | )); 184 | } 185 | } 186 | 187 | function adjustArgument($argument, $tabWidth) 188 | { 189 | $argument = '--' . $argument; 190 | if (strlen($argument) < $tabWidth) { 191 | $argument .= "\t"; 192 | } 193 | return $argument; 194 | } 195 | 196 | function checkForVersion(ArgParser $args) 197 | { 198 | try { 199 | $args->getArg("v"); 200 | } catch (ArgumentNotFound $e) { 201 | return; 202 | } 203 | 204 | throw new Exception('Version: 0.10.3-dev', 0); 205 | } 206 | --------------------------------------------------------------------------------