├── .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 | [](https://travis-ci.org/exussum12/coverageChecker)
5 | [](https://coveralls.io/github/exussum12/coverageChecker?branch=master)
6 | [](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 |
--------------------------------------------------------------------------------