├── .gitignore
├── PhpcsChanged
├── ShellException.php
├── NoChangesException.php
├── InvalidOptionException.php
├── CacheInterface.php
├── CacheObject.php
├── Modes.php
├── Reporter.php
├── PhpcsMessages.php
├── DiffLineType.php
├── functions.php
├── DiffLine.php
├── CacheEntry.php
├── ShellOperator.php
├── LintMessage.php
├── PhpcsMessagesHelpers.php
├── CheckstyleReporter.php
├── JsonReporter.php
├── FileCache.php
├── FullReporter.php
├── XmlReporter.php
├── JunitReporter.php
├── DiffLineMap.php
├── LintMessages.php
├── CacheManager.php
├── CliOptions.php
└── UnixShell.php
├── phpcs.xml
├── phpunit.xml.dist
├── tests
├── helpers
│ ├── TestXmlReporter.php
│ ├── Functions.php
│ ├── helpers.php
│ ├── PhpcsFixture.php
│ ├── TestCache.php
│ ├── GitFixture.php
│ ├── SvnFixture.php
│ └── TestShell.php
├── fixtures
│ ├── review-stuck-orders.diff
│ ├── old-phpcs-output.json
│ └── new-phpcs-output.json
├── CacheManagerTest.php
├── CliTest.php
├── PhpcsMessagesTest.php
├── DiffLineMapTest.php
├── CheckstyleReporterTest.php
├── JsonReporterTest.php
├── FullReporterTest.php
├── JunitReporterTest.php
├── XmlReporterTest.php
└── PhpcsChangedTest.php
├── psalm.xml
├── LICENSE
├── composer.json
├── index.php
├── .github
└── workflows
│ ├── csqa.yml
│ └── test.yml
├── bin
└── phpcs-changed
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/PhpcsChanged/ShellException.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Run phpcs on files, but only report warnings/errors from lines which were changed.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | tests
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/PhpcsChanged/CacheInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/helpers/PhpcsFixture.php:
--------------------------------------------------------------------------------
1 | 'ERROR',
12 | 'severity' => 5,
13 | 'fixable' => false,
14 | 'column' => 5,
15 | 'source' => 'Variables.Defined.RequiredDefined.Unused',
16 | 'line' => $lineNumber,
17 | 'message' => $message,
18 | ];
19 | }, $lineNumbers);
20 | return PhpcsMessages::fromArrays($arrays, $filename);
21 | }
22 |
23 | public function getEmptyResults(): PhpcsMessages {
24 | return PhpcsMessages::fromPhpcsJson('{"totals":{"errors":0,"warnings":0,"fixable":0},"files":{}}');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/PhpcsChanged/DiffLineType.php:
--------------------------------------------------------------------------------
1 | type = $type;
11 | }
12 |
13 | public static function makeAdd(): self {
14 | return new self('add');
15 | }
16 |
17 | public static function makeRemove(): self {
18 | return new self('remove');
19 | }
20 |
21 | public static function makeContext(): self {
22 | return new self('context');
23 | }
24 |
25 | public function isAdd(): bool {
26 | return $this->type === 'add';
27 | }
28 |
29 | public function isRemove(): bool {
30 | return $this->type === 'remove';
31 | }
32 |
33 | public function isContext(): bool {
34 | return $this->type === 'context';
35 | }
36 |
37 | public function __toString(): string {
38 | return $this->type;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Payton Swick
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 |
--------------------------------------------------------------------------------
/PhpcsChanged/functions.php:
--------------------------------------------------------------------------------
1 | type = $type;
31 | $this->line = $line;
32 | if (! $type->isAdd()) {
33 | $this->oldLine = $oldLine;
34 | }
35 | if (! $type->isRemove()) {
36 | $this->newLine = $newLine;
37 | }
38 | }
39 |
40 | public function getOldLineNumber(): ?int {
41 | return $this->oldLine;
42 | }
43 |
44 | public function getNewLineNumber(): ?int {
45 | return $this->newLine;
46 | }
47 |
48 | public function getType(): DiffLineType {
49 | return $this->type;
50 | }
51 |
52 | public function getLine(): string {
53 | return $this->line;
54 | }
55 |
56 | public function __toString(): string {
57 | $oldLine = $this->oldLine ?? 'none';
58 | $newLine = $this->newLine ?? 'none';
59 | $type = (string)$this->type;
60 | return "({$type}) {$oldLine} => {$newLine}: {$this->line}";
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/PhpcsChanged/CacheEntry.php:
--------------------------------------------------------------------------------
1 | $this->path,
36 | 'hash' => $this->hash,
37 | 'type' => $this->type,
38 | 'phpcsStandard' => $this->phpcsStandard,
39 | 'data' => $this->data,
40 | ];
41 | }
42 |
43 | public static function fromJson(array $deserializedJson): self {
44 | $entry = new CacheEntry();
45 | $entry->path = $deserializedJson['path'];
46 | $entry->hash = $deserializedJson['hash'];
47 | $entry->type = $deserializedJson['type'];
48 | $entry->phpcsStandard = $deserializedJson['phpcsStandard'];
49 | $entry->data = $deserializedJson['data'];
50 | return $entry;
51 | }
52 |
53 | public function __toString(): string {
54 | return "Cache entry for file '{$this->path}', type '{$this->type}', hash '{$this->hash}', standard '{$this->phpcsStandard}': {$this->data}";
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sirbrillig/phpcs-changed",
3 | "description": "Run phpcs on files, but only report warnings/errors from lines which were changed.",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Payton Swick",
9 | "email": "payton@foolord.com"
10 | }
11 | ],
12 | "minimum-stability": "dev",
13 | "prefer-stable": true,
14 | "scripts": {
15 | "precommit": "composer test && composer lint && composer static-analysis",
16 | "test": "./vendor/bin/phpunit --color=always --verbose",
17 | "lint": "./vendor/bin/phpcs -s PhpcsChanged bin tests index.php",
18 | "psalm": "./vendor/bin/psalm --no-cache bin/phpcs-changed",
19 | "static-analysis": "composer psalm"
20 | },
21 | "bin": ["bin/phpcs-changed"],
22 | "config": {
23 | "sort-order": true,
24 | "allow-plugins": {
25 | "dealerdirect/phpcodesniffer-composer-installer": true
26 | }
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "PhpcsChanged\\": "PhpcsChanged/"
31 | },
32 | "files": ["PhpcsChanged/Cli.php", "PhpcsChanged/functions.php"]
33 | },
34 | "require": {
35 | "php": "^7.1 || ^8.0"
36 | },
37 | "require-dev": {
38 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1 || ^1.0.0",
39 | "phpunit/phpunit": "^6.4 || ^7.0 || ^8.0 || ^9.5",
40 | "squizlabs/php_codesniffer": "^3.2.1",
41 | "sirbrillig/phpcs-variable-analysis": "^2.1.3",
42 | "vimeo/psalm": "^4.24 || ^5.0 || ^6.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/CacheManagerTest.php:
--------------------------------------------------------------------------------
1 | [
15 | 'standard',
16 | '5',
17 | '5',
18 | 'standard'
19 | ],
20 | 'non-default error and warning severity produces changed key' => [
21 | 'standard',
22 | '1',
23 | '1',
24 | 'standard:w1e1'
25 | ],
26 | 'empty warning severity key gets replaced by default value of 5' => [
27 | 'standard',
28 | '',
29 | '1',
30 | 'standard:w5e1',
31 | ],
32 | 'empty error severity key gets replaced by default value of 5' => [
33 | 'standard',
34 | '1',
35 | '',
36 | 'standard:w1e5',
37 | ],
38 | 'empty error and warning severity key returns unchanged standard value' => [
39 | 'standard',
40 | '',
41 | '',
42 | 'standard'
43 | ]
44 | ];
45 | }
46 |
47 | /**
48 | * @dataProvider providePhpcsStandardCacheKeyGenerationData
49 | */
50 | public function testPhpcsStandardCacheKeyGeneration( $phpcsStandard, $warningSeverity, $errorSeverity, $expected ) {
51 | $cache = new CacheManager( new TestCache() );
52 |
53 | $phpcsStandardCacheKey = $cache->getPhpcsStandardCacheKey( $phpcsStandard, $warningSeverity, $errorSeverity );
54 |
55 | $this->assertEquals( $expected, $phpcsStandardCacheKey );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/PhpcsChanged/ShellOperator.php:
--------------------------------------------------------------------------------
1 | disabled) {
34 | return new CacheObject();
35 | }
36 | $this->didSave = false;
37 | $cacheObject = new CacheObject();
38 | $cacheObject->cacheVersion = $this->cacheVersion ?? getVersion();
39 | foreach(array_values($this->savedFileData) as $entry) {
40 | $cacheObject->entries[] = CacheEntry::fromJson($entry);
41 | }
42 | return $cacheObject;
43 | }
44 |
45 | public function save(CacheObject $cacheObject): void {
46 | if ($this->disabled) {
47 | return;
48 | }
49 | $this->didSave = true;
50 | $this->setCacheVersion($cacheObject->cacheVersion);
51 | $this->savedFileData = [];
52 | foreach($cacheObject->entries as $entry) {
53 | $this->setEntry($entry->path, $entry->type, $entry->hash, $entry->phpcsStandard, $entry->data);
54 | }
55 | }
56 |
57 | public function setEntry(string $path, string $type, string $hash, string $phpcsStandard, string $data): void {
58 | $this->savedFileData[] = [
59 | 'path' => $path,
60 | 'hash' => $hash,
61 | 'data' => $data,
62 | 'type' => $type,
63 | 'phpcsStandard' => $phpcsStandard,
64 | ];
65 | }
66 |
67 | public function setCacheVersion(string $cacheVersion): void {
68 | $this->cacheVersion = $cacheVersion;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tests/CliTest.php:
--------------------------------------------------------------------------------
1 | isFile = $isFile;
13 | }
14 |
15 | public function isFile(): bool {
16 | return $this->isFile;
17 | }
18 | }
19 |
20 | final class CliTest extends TestCase {
21 | public function filesProvider() {
22 | return [
23 | 'PHP File' => ['example.php', true, true],
24 | 'Dir' => ['example', false, false],
25 | 'JS File' => ['example.js', true, true],
26 | 'INC file' => ['example.inc', true, true],
27 | 'Dot File' => ['.example', true, false],
28 | 'Dot INC dot PHP' => ['example.inc.php', true, true],
29 | ];
30 | }
31 |
32 | /**
33 | * @dataProvider filesProvider
34 | */
35 | public function testFileHasValidExtension( $fileName, $isFile, $hasValidExtension ) {
36 |
37 | $file = new MockSplFileInfo($fileName);
38 | $file->setIsFile($isFile);
39 | $this->assertEquals(fileHasValidExtension($file), $hasValidExtension);
40 | }
41 |
42 | public function ignoreProvider(): array {
43 | return [
44 | ['bin/*', 'bin', true],
45 | ['*.php', 'bin/foobar.php', true],
46 | ['.php', 'bin/foobar.php', true],
47 | ['foobar.php', 'bin/foobar.php', true],
48 | ['.inc', 'bin/foobar.php', false],
49 | ['bar.php', 'foo.php', false],
50 | ['bar.phpfoo.php', 'bin/foobar.php', false],
51 | ['foobar.php,bin/', 'bin/foo.php', true],
52 | ];
53 | }
54 |
55 | /**
56 | * @dataProvider ignoreProvider
57 | */
58 | public function testShouldIgnorePath(string $pattern, string $path, bool $expected): void {
59 | $this->assertEquals($expected, shouldIgnorePath($path, $pattern));
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/PhpcsChanged/LintMessage.php:
--------------------------------------------------------------------------------
1 | Additional message properties (message, source, column, severity, fixable, etc.)
24 | */
25 | private $otherProperties;
26 |
27 | public function __construct(int $line, ?string $file, string $type, array $otherProperties) {
28 | $this->line = $line;
29 | $this->file = $file;
30 | $this->type = $type;
31 | $this->otherProperties = $otherProperties;
32 | }
33 |
34 | public function getLineNumber(): int {
35 | return $this->line;
36 | }
37 |
38 | public function getFile(): ?string {
39 | return $this->file;
40 | }
41 |
42 | public function setFile(string $file): void {
43 | $this->file = $file;
44 | }
45 |
46 | public function getType(): string {
47 | return $this->type;
48 | }
49 |
50 | public function getMessage(): string {
51 | return $this->otherProperties['message'] ?? '';
52 | }
53 |
54 | public function getSource(): string {
55 | return $this->otherProperties['source'] ?? '';
56 | }
57 |
58 | public function getColumn(): int {
59 | return $this->otherProperties['column'] ?? 0;
60 | }
61 |
62 | public function getSeverity(): int {
63 | return $this->otherProperties['severity'] ?? 5;
64 | }
65 |
66 | public function getFixable(): bool {
67 | return $this->otherProperties['fixable'] ?? false;
68 | }
69 |
70 | /**
71 | * @return string|int|bool|float|null
72 | */
73 | public function getProperty( string $key ) {
74 | return $this->otherProperties[$key];
75 | }
76 |
77 | public function getOtherProperties(): array {
78 | return $this->otherProperties;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tests/helpers/GitFixture.php:
--------------------------------------------------------------------------------
1 | getFormattedMessages($messages, []);
44 | }
45 |
46 | public static function fromArrays(array $messages, ?string $fileName = null): PhpcsMessages {
47 | return new PhpcsMessages(array_map(function(array $messageArray) use ($fileName) {
48 | return new LintMessage($messageArray['line'] ?? 0, $fileName, $messageArray['type'] ?? 'ERROR', $messageArray);
49 | }, $messages));
50 | }
51 |
52 | public static function messageToPhpcsArray(LintMessage $message): array {
53 | return array_merge([
54 | 'line' => $message->getLineNumber(),
55 | ], $message->getOtherProperties());
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/.github/workflows/csqa.yml:
--------------------------------------------------------------------------------
1 | name: CS and QA
2 |
3 | on:
4 | # Run on all pushes and on all pull requests.
5 | # Prevent the build from running when there are only irrelevant changes.
6 | push:
7 | paths-ignore:
8 | - '**.md'
9 | pull_request:
10 | # Allow manually triggering the workflow.
11 | workflow_dispatch:
12 |
13 | # Cancels all previous workflow runs for the same branch that have not yet completed.
14 | concurrency:
15 | # The concurrency group contains the workflow name and the branch name.
16 | group: ${{ github.workflow }}-${{ github.ref }}
17 | cancel-in-progress: true
18 |
19 | jobs:
20 | checkcs:
21 | name: 'PHP: 7.4 | Basic CS and QA checks'
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - name: Checkout code
26 | uses: actions/checkout@v3
27 |
28 | - name: Install PHP
29 | uses: shivammathur/setup-php@v2
30 | with:
31 | php-version: '7.4'
32 | coverage: none
33 | tools: cs2pr
34 |
35 | # Using PHPCS `3.x-dev` as an early detection system for bugs upstream.
36 | - name: 'Composer: adjust dependencies'
37 | run: composer require --no-update squizlabs/php_codesniffer:"3.x-dev"
38 |
39 | # Install dependencies and handle caching in one go.
40 | # @link https://github.com/marketplace/actions/install-composer-dependencies
41 | - name: Install Composer dependencies
42 | uses: "ramsey/composer-install@v2"
43 |
44 | # Check the code-style consistency of the PHP files.
45 | - name: Check PHP code style
46 | continue-on-error: true
47 | run: composer lint -- --no-cache --report-full --report-checkstyle=./phpcs-report.xml
48 |
49 | - name: Show PHPCS results in PR
50 | run: cs2pr ./phpcs-report.xml
51 |
52 | staticanalysis:
53 | name: "PHP: 7.4 | Static analysis"
54 | runs-on: ubuntu-latest
55 |
56 | steps:
57 | - name: Checkout code
58 | uses: actions/checkout@v3
59 |
60 | - name: Install PHP
61 | uses: shivammathur/setup-php@v2
62 | with:
63 | php-version: '7.4'
64 | coverage: none
65 |
66 | # Install dependencies and handle caching in one go.
67 | # Dependencies need to be installed to make sure the PHPUnit classes are recognized.
68 | # @link https://github.com/marketplace/actions/install-composer-dependencies
69 | - name: Install Composer dependencies
70 | uses: "ramsey/composer-install@v2"
71 |
72 | - name: Run Static analysis
73 | run: composer static-analysis
74 |
--------------------------------------------------------------------------------
/PhpcsChanged/CheckstyleReporter.php:
--------------------------------------------------------------------------------
1 | getFile() ?? 'STDIN';
15 | }, $messages->getMessages()));
16 | if (count($files) === 0) {
17 | $files = ['STDIN'];
18 | }
19 |
20 | $outputByFile = array_reduce($files, function(string $output, string $file) use ($messages): string {
21 | $messagesForFile = array_values(array_filter($messages->getMessages(), static function(LintMessage $message) use ($file): bool {
22 | return ($message->getFile() ?? 'STDIN') === $file;
23 | }));
24 | $output .= $this->getFormattedMessagesForFile($messagesForFile, $file);
25 | return $output;
26 | }, '');
27 |
28 | $output = "\n";
29 | $output .= "\n";
30 | $output .= $outputByFile;
31 | $output .= "\n";
32 |
33 | return $output;
34 | }
35 |
36 | private function getFormattedMessagesForFile(array $messages, string $file): string {
37 | if (count($messages) === 0) {
38 | return '';
39 | }
40 |
41 | $xmlOutputForFile = "\t\n";
42 | $xmlOutputForFile .= array_reduce($messages, function(string $output, LintMessage $message): string {
43 | $line = $message->getLineNumber();
44 | $column = $message->getColumn();
45 | $source = $this->escapeXml($message->getSource());
46 | $messageText = $this->escapeXml($message->getMessage());
47 | $type = $message->getType();
48 |
49 | // Map phpcs types to Checkstyle severity levels
50 | $severity = $type === 'ERROR' ? 'error' : 'warning';
51 |
52 | $output .= sprintf(
53 | "\t\t\n",
54 | $line,
55 | $column,
56 | $severity,
57 | $messageText,
58 | $source
59 | );
60 | return $output;
61 | }, '');
62 | $xmlOutputForFile .= "\t\n";
63 |
64 | return $xmlOutputForFile;
65 | }
66 |
67 | private function escapeXml(string $string): string {
68 | return htmlspecialchars($string, ENT_XML1 | ENT_QUOTES, 'UTF-8');
69 | }
70 |
71 | #[\Override]
72 | public function getExitCode(PhpcsMessages $messages): int {
73 | return (count($messages->getMessages()) > 0) ? 1 : 0;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/PhpcsChanged/JsonReporter.php:
--------------------------------------------------------------------------------
1 | getFile() ?? 'STDIN';
16 | }, $messages->getMessages()));
17 | if (count($files) === 0) {
18 | $files = ['STDIN'];
19 | }
20 |
21 | $outputByFile = array_map(function(string $file) use ($messages): array {
22 | $messagesForFile = array_values(array_filter($messages->getMessages(), function(LintMessage $message) use ($file): bool {
23 | return ($message->getFile() ?? 'STDIN') === $file;
24 | }));
25 | return $this->getFormattedMessagesForFile($messagesForFile, $file);
26 | }, $files);
27 |
28 | $errors = array_values(array_filter($messages->getMessages(), function($message) {
29 | return $message->getType() === 'ERROR';
30 | }));
31 | $warnings = array_values(array_filter($messages->getMessages(), function($message) {
32 | return $message->getType() === 'WARNING';
33 | }));
34 | $dataForJson = [
35 | 'totals' => [
36 | 'errors' => count($errors),
37 | 'warnings' => count($warnings),
38 | 'fixable' => 0,
39 | ],
40 | 'files' => array_merge([], ...$outputByFile),
41 | ];
42 | $output = json_encode($dataForJson, JSON_UNESCAPED_SLASHES);
43 | if ($output === false) {
44 | throw new \Exception('Failed to JSON-encode result messages');
45 | }
46 | return $output;
47 | }
48 |
49 | private function getFormattedMessagesForFile(array $messages, string $file): array {
50 | $errors = array_values(array_filter($messages, function($message) {
51 | return $message->getType() === 'ERROR';
52 | }));
53 | $warnings = array_values(array_filter($messages, function($message) {
54 | return $message->getType() === 'WARNING';
55 | }));
56 | $messageArrays = array_map(function(LintMessage $message): array {
57 | return PhpcsMessagesHelpers::messageToPhpcsArray($message);
58 | }, $messages);
59 | $dataForJson = [
60 | $file => [
61 | 'errors' => count($errors),
62 | 'warnings' => count($warnings),
63 | 'messages' => $messageArrays,
64 | ],
65 | ];
66 | return $dataForJson;
67 | }
68 |
69 | #[\Override]
70 | public function getExitCode(PhpcsMessages $messages): int {
71 | return (count($messages->getMessages()) > 0) ? 1 : 0;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/helpers/SvnFixture.php:
--------------------------------------------------------------------------------
1 | cacheFilePath)) {
21 | return new CacheObject();
22 | }
23 | $contents = file_get_contents($this->cacheFilePath);
24 | if ($contents === false) {
25 | throw new \Exception('Failed to read cache file');
26 | }
27 | /** @var array{cacheVersion: string, entries: Array>} */
28 | $decoded = json_decode($contents, true);
29 | if (! $this->isDecodedDataValid($decoded)) {
30 | throw new \Exception('Invalid cache file');
31 | }
32 | $cacheObject = new CacheObject();
33 | $cacheObject->cacheVersion = $decoded['cacheVersion'];
34 | foreach($decoded['entries'] as $entry) {
35 | if (! $this->isDecodedEntryValid($entry)) {
36 | throw new \Exception('Invalid cache file entry: ' . var_export($entry, true));
37 | }
38 | $cacheObject->entries[] = CacheEntry::fromJson($entry);
39 | }
40 | return $cacheObject;
41 | }
42 |
43 | #[\Override]
44 | public function save(CacheObject $cacheObject): void {
45 | $data = [
46 | 'cacheVersion' => $cacheObject->cacheVersion,
47 | 'entries' => $cacheObject->entries,
48 | ];
49 | $encodedData = json_encode($data);
50 | if ($encodedData === false) {
51 | throw new \Exception('Failed to write cache file; encoding failed');
52 | }
53 | $result = file_put_contents($this->cacheFilePath, $encodedData);
54 | if ($result === false) {
55 | throw new \Exception('Failed to write cache file');
56 | }
57 | }
58 |
59 | /**
60 | * @param mixed $decoded The json-decoded data
61 | */
62 | private function isDecodedDataValid($decoded): bool {
63 | if (! is_array($decoded) ||
64 | ! array_key_exists('cacheVersion', $decoded) ||
65 | ! array_key_exists('entries', $decoded) ||
66 | ! is_array($decoded['entries'])
67 | ) {
68 | return false;
69 | }
70 | if (! is_string($decoded['cacheVersion'])) {
71 | return false;
72 | }
73 | // Note that this does not validate the entries to avoid iterating over
74 | // them twice. That should be done by isDecodedEntryValid.
75 | return true;
76 | }
77 |
78 | private function isDecodedEntryValid(array $entry): bool {
79 | if (! array_key_exists('path', $entry) || ! array_key_exists('data', $entry) || ! array_key_exists('phpcsStandard', $entry) || ! array_key_exists('hash', $entry) || ! array_key_exists('type', $entry)) {
80 | return false;
81 | }
82 | return true;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/PhpcsChanged/FullReporter.php:
--------------------------------------------------------------------------------
1 | getFile() ?? 'STDIN';
16 | }, $messages->getMessages()));
17 | if (count($files) === 0) {
18 | $files = ['STDIN'];
19 | }
20 |
21 | $lineCount = count($messages->getMessages());
22 | if ($lineCount < 1) {
23 | return '';
24 | }
25 |
26 | return implode("\n", array_filter(array_map(function(string $file) use ($messages, $options): ?string {
27 | $messagesForFile = array_values(array_filter($messages->getMessages(), function(LintMessage $message) use ($file): bool {
28 | return ($message->getFile() ?? 'STDIN') === $file;
29 | }));
30 | return $this->getFormattedMessagesForFile($messagesForFile, $file, $options);
31 | }, $files)));
32 | }
33 |
34 | private function getFormattedMessagesForFile(array $messages, string $file, array $options): ?string {
35 | $lineCount = count($messages);
36 | if ($lineCount < 1) {
37 | return null;
38 | }
39 | $errorsCount = count(array_values(array_filter($messages, function($message) {
40 | return $message->getType() === 'ERROR';
41 | })));
42 | $warningsCount = count(array_values(array_filter($messages, function($message) {
43 | return $message->getType() === 'WARNING';
44 | })));
45 |
46 | $linePlural = ($lineCount === 1) ? '' : 'S';
47 | $errorPlural = ($errorsCount === 1) ? '' : 'S';
48 | $warningPlural = ($warningsCount === 1) ? '' : 'S';
49 |
50 | $longestNumber = getLongestString(array_map(function(LintMessage $message): int {
51 | return $message->getLineNumber();
52 | }, $messages));
53 |
54 | $formattedLines = implode("\n", array_map(function(LintMessage $message) use ($longestNumber, $options): string {
55 | $source = $message->getSource() ?: 'Unknown';
56 | $sourceString = array_key_exists('s', $options) ? " ({$source})" : '';
57 | return sprintf(" %{$longestNumber}d | %s | %s%s", $message->getLineNumber(), $message->getType(), $message->getMessage(), $sourceString);
58 | }, $messages));
59 |
60 | return <<getMessages()) > 0) ? 1 : 0;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/PhpcsChanged/XmlReporter.php:
--------------------------------------------------------------------------------
1 | options = $options;
27 | $this->shell = $shell;
28 | }
29 |
30 | #[\Override]
31 | public function getFormattedMessages(PhpcsMessages $messages, array $options): string { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
32 | $files = array_unique(array_map(function(LintMessage $message): string {
33 | return $message->getFile() ?? 'STDIN';
34 | }, $messages->getMessages()));
35 | if (count($files) === 0) {
36 | $files = ['STDIN'];
37 | }
38 |
39 | $outputByFile = array_reduce($files,function(string $output, string $file) use ($messages): string {
40 | $messagesForFile = array_values(array_filter($messages->getMessages(), static function(LintMessage $message) use ($file): bool {
41 | return ($message->getFile() ?? 'STDIN') === $file;
42 | }));
43 | $output .= $this->getFormattedMessagesForFile($messagesForFile, $file);
44 | return $output;
45 | }, '');
46 |
47 | $phpcsVersion = $this->shell->getPhpcsVersion();
48 |
49 | $output = "\n";
50 | $output .= "\n";
51 | $output .= $outputByFile;
52 | $output .= "\n";
53 |
54 | return $output;
55 | }
56 |
57 | private function getFormattedMessagesForFile(array $messages, string $file): string {
58 | $errorCount = count( array_values(array_filter($messages, function(LintMessage $message) {
59 | return $message->getType() === 'ERROR';
60 | })));
61 | $warningCount = count(array_values(array_filter($messages, function(LintMessage $message) {
62 | return $message->getType() === 'WARNING';
63 | })));
64 | $fixableCount = count(array_values(array_filter($messages, function(LintMessage $message) {
65 | return $message->getFixable();
66 | })));
67 | $xmlOutputForFile = "\t\n";
68 | $xmlOutputForFile .= array_reduce($messages, function(string $output, LintMessage $message): string{
69 | $type = strtolower( $message->getType() );
70 | $line = $message->getLineNumber();
71 | $column = $message->getColumn();
72 | $source = $message->getSource();
73 | $severity = $message->getSeverity();
74 | $fixable = $message->getFixable() ? "1" : "0";
75 | $messageString = $message->getMessage();
76 | $output .= "\t\t<{$type} line=\"{$line}\" column=\"{$column}\" source=\"{$source}\" severity=\"{$severity}\" fixable=\"{$fixable}\">{$messageString}{$type}>\n";
77 | return $output;
78 | },'');
79 | $xmlOutputForFile .= "\t\n";
80 |
81 | return $xmlOutputForFile;
82 | }
83 |
84 | #[\Override]
85 | public function getExitCode(PhpcsMessages $messages): int {
86 | return (count($messages->getMessages()) > 0) ? 1 : 0;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tests/helpers/TestShell.php:
--------------------------------------------------------------------------------
1 | registerReadableFileName($fileName);
30 | }
31 | parent::__construct($options);
32 | }
33 |
34 | public function registerExecutable(string $path): void {
35 | $this->executables[$path] = true;
36 | }
37 |
38 | public function registerReadableFileName(string $fileName, bool $override = false): bool {
39 | if (!isset($this->readableFileNames[$fileName]) || $override ) {
40 | $this->readableFileNames[$fileName] = true;
41 | return true;
42 | }
43 | throw new \Exception("Already registered file name: {$fileName}");
44 | }
45 |
46 | public function registerCommand(string $command, string $output, int $return_val = 0, bool $override = false): bool {
47 | if (!isset($this->commands[$command]) || $override) {
48 | $this->commands[$command] = [
49 | 'output' => $output,
50 | 'return_val' => $return_val,
51 | ];
52 | return true;
53 | }
54 | throw new \Exception("Already registered command: {$command}");
55 | }
56 |
57 | public function deregisterCommand(string $command): bool {
58 | if (isset($this->commands[$command])) {
59 | unset($this->commands[$command]);
60 | return true;
61 | }
62 | throw new \Exception("No registered command: {$command}");
63 | }
64 |
65 | public function setFileHash(string $fileName, string $hash): void {
66 | $this->fileHashes[$fileName] = $hash;
67 | }
68 |
69 | public function isReadable(string $fileName): bool {
70 | return isset($this->readableFileNames[$fileName]);
71 | }
72 |
73 | public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis
74 |
75 | public function printError(string $message): void {} // phpcs:ignore VariableAnalysis
76 |
77 | public function validateExecutableExists(string $name, string $command): void {
78 | if (isset($this->executables[$command])) {
79 | return;
80 | }
81 | throw new \Exception("The executable for {$name} with the path '{$command}' has not been mocked.");
82 | }
83 |
84 | public function getFileHash(string $fileName): string {
85 | return $this->fileHashes[$fileName] ?? $fileName;
86 | }
87 |
88 | protected function executeCommand(string $command, ?int &$return_val = null): string {
89 | foreach ($this->commands as $registeredCommand => $return) {
90 | if ($registeredCommand === substr($command, 0, strlen($registeredCommand)) ) {
91 | $return_val = $return['return_val'];
92 | $this->commandsCalled[$registeredCommand] = $command;
93 | return $return['output'];
94 | }
95 | }
96 |
97 | throw new \Exception("Unknown command: {$command}");
98 | }
99 |
100 | public function resetCommandsCalled(): void {
101 | $this->commandsCalled = [];
102 | }
103 |
104 | public function wasCommandCalled(string $registeredCommand): bool {
105 | return isset($this->commandsCalled[$registeredCommand]);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/PhpcsMessagesTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expected, $messages->getLineNumbers());
15 | }
16 |
17 | public function testFromPhpcsJsonWithEmptyJson() {
18 | $expected = [];
19 | $json = '';
20 | $messages = PhpcsMessages::fromPhpcsJson($json);
21 | $this->assertEquals($expected, $messages->getLineNumbers());
22 | }
23 |
24 | public function testGetPhpcsJson() {
25 | $expected = '{"totals":{"errors":0,"warnings":1,"fixable":0},"files":{"STDIN":{"errors":0,"warnings":1,"messages":[{"line":20,"type":"WARNING","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."}]}}}';
26 | $messages = PhpcsMessages::fromArrays([
27 | [
28 | 'type' => 'WARNING',
29 | 'severity' => 5,
30 | 'fixable' => false,
31 | 'column' => 5,
32 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
33 | 'line' => 20,
34 | 'message' => 'Found unused symbol Emergent.',
35 | ],
36 | ]);
37 | $this->assertEquals($expected, $messages->toPhpcsJson());
38 | }
39 |
40 | public function testMerge() {
41 | $expected = PhpcsMessages::fromArrays([
42 | [
43 | 'type' => 'WARNING',
44 | 'severity' => 5,
45 | 'fixable' => false,
46 | 'column' => 5,
47 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
48 | 'line' => 15,
49 | 'message' => 'Found unused symbol Foo.',
50 | ],
51 | [
52 | 'type' => 'WARNING',
53 | 'severity' => 5,
54 | 'fixable' => false,
55 | 'column' => 5,
56 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
57 | 'line' => 18,
58 | 'message' => 'Found unused symbol Baz.',
59 | ],
60 | [
61 | 'type' => 'WARNING',
62 | 'severity' => 5,
63 | 'fixable' => false,
64 | 'column' => 5,
65 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
66 | 'line' => 20,
67 | 'message' => 'Found unused symbol Bar.',
68 | ],
69 | ]);
70 | $messagesA = PhpcsMessages::fromArrays([
71 | [
72 | 'type' => 'WARNING',
73 | 'severity' => 5,
74 | 'fixable' => false,
75 | 'column' => 5,
76 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
77 | 'line' => 15,
78 | 'message' => 'Found unused symbol Foo.',
79 | ],
80 | ]);
81 | $messagesB = PhpcsMessages::fromArrays([
82 | [
83 | 'type' => 'WARNING',
84 | 'severity' => 5,
85 | 'fixable' => false,
86 | 'column' => 5,
87 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
88 | 'line' => 18,
89 | 'message' => 'Found unused symbol Baz.',
90 | ],
91 | ]);
92 | $messagesC = PhpcsMessages::fromArrays([
93 | [
94 | 'type' => 'WARNING',
95 | 'severity' => 5,
96 | 'fixable' => false,
97 | 'column' => 5,
98 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
99 | 'line' => 20,
100 | 'message' => 'Found unused symbol Bar.',
101 | ],
102 | ]);
103 | $messages = PhpcsMessages::merge([$messagesA, $messagesB, $messagesC]);
104 | $this->assertEquals($expected->getMessages(), $messages->getMessages());
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | # Run on all pushes and pull requests.
5 | push:
6 | pull_request:
7 | # Allow manually triggering the workflow.
8 | workflow_dispatch:
9 |
10 | # Cancels all previous workflow runs for the same branch that have not yet completed.
11 | concurrency:
12 | # The concurrency group contains the workflow name and the branch name.
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | #### TEST STAGE ####
18 | test:
19 | runs-on: ubuntu-latest
20 |
21 | strategy:
22 | matrix:
23 | # The GHA matrix works different from Travis.
24 | # You can define jobs here and then augment them with extra variables in `include`,
25 | # as well as add extra jobs in `include`.
26 | # @link https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix
27 | #
28 | # IMPORTANT: test runs shouldn't fail because of PHPCS being incompatible with a PHP version.
29 | # - PHPCS will run without errors on PHP 5.4 - 7.4 on all supported PHPCS versions.
30 | # - PHP 8.0 needs PHPCS 3.5.7+ to run without errors.
31 | # - PHP 8.1 needs PHPCS 3.6.1+ to run without errors.
32 | php: ['7.1', '7.2', '7.3']
33 | phpcs_version: ['3.5.6', '3.x-dev']
34 |
35 | include:
36 | # Make the matrix complete without duplicating builds run in code coverage.
37 | - php: '8.1'
38 | phpcs_version: '3.6.1'
39 |
40 | - php: '8.0'
41 | phpcs_version: '3.x-dev'
42 | - php: '8.0'
43 | phpcs_version: '3.5.7'
44 |
45 | - php: '7.4'
46 | phpcs_version: '3.x-dev'
47 |
48 | # Experimental builds.
49 | - php: '8.2' # Nightly.
50 | phpcs_version: '3.x-dev'
51 |
52 | name: "Test: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }}"
53 |
54 | continue-on-error: ${{ matrix.php == '8.2' }}
55 |
56 | steps:
57 | - name: Checkout code
58 | uses: actions/checkout@v3
59 |
60 | - name: Setup ini config
61 | id: set_ini
62 | run: |
63 | # On stable PHPCS versions, allow for PHP deprecation notices.
64 | # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore.
65 | if [ "${{ matrix.phpcs_version }}" != "3.x-dev" ]; then
66 | echo '::set-output name=PHP_INI::error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On, zend.assertions=1'
67 | else
68 | echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On, zend.assertions=1'
69 | fi
70 |
71 | - name: Install PHP
72 | uses: shivammathur/setup-php@v2
73 | with:
74 | php-version: ${{ matrix.php }}
75 | ini-values: ${{ steps.set_ini.outputs.PHP_INI }}
76 | coverage: none
77 |
78 | - name: 'Composer: adjust dependencies'
79 | run: |
80 | # Set the PHPCS version for this test run.
81 | composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}"
82 |
83 | # Install dependencies and handle caching in one go.
84 | # @link https://github.com/marketplace/actions/install-composer-dependencies
85 | - name: Install Composer dependencies - normal
86 | if: ${{ matrix.php != '8.2' }}
87 | uses: "ramsey/composer-install@v2"
88 |
89 | # For the PHP "nightly", we need to install with ignore platform reqs as not all dependencies allow it yet.
90 | - name: Install Composer dependencies - with ignore platform
91 | if: ${{ matrix.php == '8.2' }}
92 | uses: "ramsey/composer-install@v2"
93 | with:
94 | composer-options: --ignore-platform-req=php
95 |
96 | - name: Run the unit tests
97 | run: composer test
98 |
--------------------------------------------------------------------------------
/PhpcsChanged/JunitReporter.php:
--------------------------------------------------------------------------------
1 | getFile() ?? 'STDIN';
15 | }, $messages->getMessages()));
16 | if (count($files) === 0) {
17 | $files = ['STDIN'];
18 | }
19 |
20 | $totalTests = count($messages->getMessages());
21 | $totalFailures = count(array_filter($messages->getMessages(), function(LintMessage $message): bool {
22 | return $message->getType() === 'WARNING';
23 | }));
24 | $totalErrors = count(array_filter($messages->getMessages(), function(LintMessage $message): bool {
25 | return $message->getType() === 'ERROR';
26 | }));
27 |
28 | // Calculate total time from all files
29 | $totalTime = array_sum($messages->getAllTiming());
30 |
31 | $outputByFile = array_reduce($files, function(string $output, string $file) use ($messages): string {
32 | $messagesForFile = array_values(array_filter($messages->getMessages(), static function(LintMessage $message) use ($file): bool {
33 | return ($message->getFile() ?? 'STDIN') === $file;
34 | }));
35 | $output .= $this->getFormattedMessagesForFile($messagesForFile, $file, $messages);
36 | return $output;
37 | }, '');
38 |
39 | $output = "\n";
40 | $output .= sprintf("\n", $totalTests, $totalFailures, $totalErrors, $totalTime);
41 | $output .= $outputByFile;
42 | $output .= "\n";
43 |
44 | return $output;
45 | }
46 |
47 | private function getFormattedMessagesForFile(array $messages, string $file, PhpcsMessages $allMessages): string {
48 | $testCount = count($messages);
49 | $errorCount = count(array_values(array_filter($messages, function(LintMessage $message) {
50 | return $message->getType() === 'ERROR';
51 | })));
52 | $failureCount = count(array_values(array_filter($messages, function(LintMessage $message) {
53 | return $message->getType() === 'WARNING';
54 | })));
55 |
56 | // Get timing for this specific file
57 | $fileTime = $allMessages->getTiming($file);
58 |
59 | $xmlOutputForFile = sprintf("\t\n",
60 | $file, $testCount, $failureCount, $errorCount, $fileTime);
61 | $xmlOutputForFile .= array_reduce($messages, function(string $output, LintMessage $message): string {
62 | $line = $message->getLineNumber();
63 | $column = $message->getColumn();
64 | $source = $this->escapeXml($message->getSource());
65 | $messageText = $this->escapeXml($message->getMessage());
66 | $type = $message->getType();
67 | $severity = $message->getSeverity();
68 |
69 | // Create a unique test case name using line:column and source
70 | $testCaseName = "line {$line}, column {$column}";
71 | $output .= "\t\t\n";
72 |
73 | if ($type === 'ERROR') {
74 | $output .= "\t\t\tLine {$line}, Column {$column}: {$messageText} (Severity: {$severity})\n";
75 | } else {
76 | $output .= "\t\t\tLine {$line}, Column {$column}: {$messageText} (Severity: {$severity})\n";
77 | }
78 |
79 | $output .= "\t\t\n";
80 | return $output;
81 | }, '');
82 | $xmlOutputForFile .= "\t\n";
83 |
84 | return $xmlOutputForFile;
85 | }
86 |
87 | private function escapeXml(string $string): string {
88 | return htmlspecialchars($string, ENT_XML1 | ENT_QUOTES, 'UTF-8');
89 | }
90 |
91 | #[\Override]
92 | public function getExitCode(PhpcsMessages $messages): int {
93 | return (count($messages->getMessages()) > 0) ? 1 : 0;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/PhpcsChanged/DiffLineMap.php:
--------------------------------------------------------------------------------
1 | diffLines = $diffLines;
14 | }
15 |
16 | public function getOldLineNumberForLine(int $lineNumber): ?int {
17 | foreach ($this->diffLines as $diffLine) {
18 | if ($diffLine->getNewLineNumber() === $lineNumber) {
19 | return $diffLine->getOldLineNumber();
20 | }
21 | }
22 | // go through each changed line in the new file (each DiffLine of type context or add)
23 | // if the new line number is greater than the line number we are looking for
24 | // then add the last difference between the old and new lines to the line number we are looking for
25 | $lineNumberDelta = 0;
26 | $lastOldLine = 0;
27 | $lastNewLine = 0;
28 | foreach ($this->diffLines as $diffLine) {
29 | $lastOldLine = $diffLine->getOldLineNumber() ?? $lastOldLine;
30 | $lastNewLine = $diffLine->getNewLineNumber() ?? $lastNewLine;
31 | if ($diffLine->getType()->isRemove()) {
32 | continue;
33 | }
34 | if (($diffLine->getNewLineNumber() ?? 0) > $lineNumber) {
35 | return intval( $lineNumber + $lineNumberDelta );
36 | }
37 | $lineNumberDelta = ($diffLine->getOldLineNumber() ?? 0) - ($diffLine->getNewLineNumber() ?? 0);
38 | }
39 | return $lastOldLine + ($lineNumber - $lastNewLine);
40 | }
41 |
42 | public static function fromUnifiedDiff(string $unifiedDiff): DiffLineMap {
43 | $diffStringLines = preg_split("/\r\n|\n|\r/", $unifiedDiff) ?: [];
44 | $oldStartLine = $newStartLine = null;
45 | $currentOldLine = $currentNewLine = null;
46 | $lines = [];
47 | foreach ($diffStringLines as $diffStringLine) {
48 |
49 | // Find the start of a hunk
50 | $matches = [];
51 | if (1 === preg_match('/^@@ \-(\d+),(\d+) \+(\d+),(\d+) @@/', $diffStringLine, $matches)) {
52 | $oldStartLine = $matches[1] ?? null;
53 | $newStartLine = $matches[3] ?? null;
54 | $currentOldLine = $oldStartLine;
55 | $currentNewLine = $newStartLine;
56 | continue;
57 | }
58 |
59 | // Ignore headers
60 | if (self::isLineDiffHeader($diffStringLine)) {
61 | continue;
62 | }
63 |
64 | // Parse a hunk
65 | if ($oldStartLine !== null && $newStartLine !== null) {
66 | $lines[] = new DiffLine((int)$currentOldLine, (int)$currentNewLine, self::getDiffLineTypeForLine($diffStringLine), $diffStringLine);
67 | if (self::isLineDiffRemoval($diffStringLine)) {
68 | $currentOldLine ++;
69 | } else if (self::isLineDiffAddition($diffStringLine)) {
70 | $currentNewLine ++;
71 | } else {
72 | $currentOldLine ++;
73 | $currentNewLine ++;
74 | }
75 | }
76 | }
77 | return new DiffLineMap($lines);
78 | }
79 |
80 | public static function getFileNameFromDiff(string $unifiedDiff): ?string {
81 | $diffStringLines = preg_split("/\r\n|\n|\r/", $unifiedDiff) ?: [];
82 | foreach ($diffStringLines as $diffStringLine) {
83 | $matches = [];
84 | if (1 === preg_match('/^\+\+\+ (\S+)/', $diffStringLine, $matches)) {
85 | return $matches[1] ?? null;
86 | }
87 | }
88 | return null;
89 | }
90 |
91 | private static function getDiffLineTypeForLine(string $line): DiffLineType {
92 | if (self::isLineDiffRemoval($line)) {
93 | return DiffLineType::makeRemove();
94 | } else if (self::isLineDiffAddition($line)) {
95 | return DiffLineType::makeAdd();
96 | }
97 | return DiffLineType::makeContext();
98 | }
99 |
100 | private static function isLineDiffHeader(string $line): bool {
101 | return (1 === preg_match('/^Index: /', $line) || 1 === preg_match('/^====/', $line) || 1 === preg_match('/^\-\-\-/', $line) || 1 === preg_match('/^\+\+\+/', $line));
102 | }
103 |
104 | private static function isLineDiffRemoval(string $line): bool {
105 | return (1 === preg_match('/^\-/', $line));
106 | }
107 |
108 | private static function isLineDiffAddition(string $line): bool {
109 | return (1 === preg_match('/^\+/', $line));
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/PhpcsChanged/LintMessages.php:
--------------------------------------------------------------------------------
1 | Per-file execution timing in seconds
17 | */
18 | private $timingData = [];
19 |
20 | final public function __construct(array $messages) {
21 | foreach($messages as $message) {
22 | if (! $message instanceof LintMessage) {
23 | throw new \Exception('Each message in a LintMessages object must be a LintMessage; found ' . var_export($message, true));
24 | }
25 | }
26 | $this->messages = $messages;
27 | }
28 |
29 | /**
30 | * @return static
31 | */
32 | public static function merge(array $messages) {
33 | $merged = self::fromLintMessages(array_merge([], ...array_map(function(self $message) {
34 | return $message->getMessages();
35 | }, $messages)));
36 |
37 | // Merge timing data from all message sets
38 | $mergedTiming = [];
39 | foreach ($messages as $messageSet) {
40 | $mergedTiming = array_merge($mergedTiming, $messageSet->getAllTiming());
41 | }
42 | $merged->setAllTiming($mergedTiming);
43 |
44 | return $merged;
45 | }
46 |
47 | /**
48 | * @return static
49 | */
50 | public static function fromLintMessages(array $messages, ?string $fileName = null) {
51 | return new static(array_map(function(LintMessage $message) use ($fileName) {
52 | if (is_string($fileName) && strlen($fileName) > 0) {
53 | $message->setFile($fileName);
54 | }
55 | return $message;
56 | }, $messages));
57 | }
58 |
59 | /**
60 | * @return LintMessage[]
61 | */
62 | public function getMessages(): array {
63 | return $this->messages;
64 | }
65 |
66 | /**
67 | * @return int[]
68 | */
69 | public function getLineNumbers(): array {
70 | return array_map(function($message) {
71 | return $message->getLineNumber();
72 | }, $this->messages);
73 | }
74 |
75 | /**
76 | * @return static
77 | */
78 | public static function getNewMessages(string $unifiedDiff, self $unmodifiedMessages, self $modifiedMessages) {
79 | $map = DiffLineMap::fromUnifiedDiff($unifiedDiff);
80 | $fileName = DiffLineMap::getFileNameFromDiff($unifiedDiff);
81 | $newMessages = self::fromLintMessages(array_values(array_filter($modifiedMessages->getMessages(), function($newMessage) use ($unmodifiedMessages, $map) {
82 | $lineNumber = $newMessage->getLineNumber();
83 | if (! $lineNumber) {
84 | return true;
85 | }
86 | $unmodifiedLineNumber = $map->getOldLineNumberForLine($lineNumber);
87 | $unmodifiedMessagesContainingUnmodifiedLineNumber = array_values(array_filter($unmodifiedMessages->getMessages(), function($unmodifiedMessage) use ($unmodifiedLineNumber) {
88 | return $unmodifiedMessage->getLineNumber() === $unmodifiedLineNumber;
89 | }));
90 | return ! (count($unmodifiedMessagesContainingUnmodifiedLineNumber) > 0);
91 | })), $fileName);
92 |
93 | // Preserve timing data from the modified messages
94 | $newMessages->setAllTiming($modifiedMessages->getAllTiming());
95 |
96 | return $newMessages;
97 | }
98 |
99 | /**
100 | * Set timing data for a file
101 | *
102 | * @param string $fileName The file name or path
103 | * @param float $duration Duration in seconds
104 | */
105 | public function setTiming(string $fileName, float $duration): void {
106 | $this->timingData[$fileName] = $duration;
107 | }
108 |
109 | /**
110 | * Get timing data for a specific file
111 | *
112 | * @param string $fileName The file name or path
113 | * @return float Duration in seconds, or 0.0 if not set
114 | */
115 | public function getTiming(string $fileName): float {
116 | return $this->timingData[$fileName] ?? 0.0;
117 | }
118 |
119 | /**
120 | * Get all timing data
121 | *
122 | * @return array Array mapping file names to durations in seconds
123 | */
124 | public function getAllTiming(): array {
125 | return $this->timingData;
126 | }
127 |
128 | /**
129 | * Set all timing data at once
130 | *
131 | * @param array $timingData Array mapping file names to durations
132 | */
133 | public function setAllTiming(array $timingData): void {
134 | $this->timingData = $timingData;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/bin/phpcs-changed:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | isFile() && !fileHasValidExtension($file, isset($options['extensions']) && is_string($options['extensions']) ? $options['extensions'] : '')) {
83 | return false;
84 | }
85 | return $iterator->hasChildren() || $file->isFile() ? true : false;
86 | }));
87 | foreach ($iterator as $file) {
88 | if (shouldIgnorePath($file->getPathName(), isset($options['ignore']) && is_string($options['ignore']) ? $options['ignore'] : null)) {
89 | continue;
90 | }
91 | $fileNamesExpanded[] = $file->getPathName();
92 | }
93 | } elseif (!shouldIgnorePath($file, isset($options['ignore']) && is_string($options['ignore']) ? $options['ignore'] : null)) {
94 | $fileNamesExpanded[] = $file;
95 | }
96 | }
97 |
98 | if (isset($options['h']) || isset($options['help'])) {
99 | printHelp();
100 | exit(0);
101 | }
102 |
103 | if (isset($options['version'])) {
104 | printVersion();
105 | }
106 |
107 | if (isset($options['i'])) {
108 | $cliOptions = CliOptions::fromArray($options);
109 | $shell = new UnixShell($cliOptions);
110 | printInstalledCodingStandards($shell);
111 | }
112 |
113 | // --git-branch exists for compatibility, --git-base supports branches
114 | if (isset($options['git-branch'])) {
115 | $options['git-base'] = $options['git-branch'];
116 | unset($options['git-branch']);
117 | }
118 |
119 | // --arc-lint is a shorthand for several other options
120 | if (isset($options['arc-lint'])) {
121 | $options['no-verify-git-file'] = 1;
122 | $options['always-exit-zero'] = 1;
123 | unset($options['arc-lint']);
124 | }
125 |
126 | if (isset($options['phpcs-orig'])) {
127 | $options['phpcs-unmodified'] = $options['phpcs-orig'];
128 | unset($options['phpcs-orig']);
129 | }
130 | if (isset($options['phpcs-new'])) {
131 | $options['phpcs-modified'] = $options['phpcs-new'];
132 | unset($options['phpcs-new']);
133 | }
134 |
135 | $options['files'] = $fileNamesExpanded;
136 |
137 | $debug = getDebug(isset($options['debug']));
138 | run($options, $debug);
139 |
140 | function run(array $rawOptions, callable $debug): void {
141 | $encodedOptions = json_encode($rawOptions);
142 | if ($encodedOptions !== false) {
143 | $debug('Options: ' . $encodedOptions);
144 | }
145 | try {
146 | $options = CliOptions::fromArray($rawOptions);
147 | } catch ( InvalidOptionException $err ) {
148 | printErrorAndExit($err->getMessage());
149 | exit(1);
150 | }
151 | $debug('Running on filenames: ' . implode(', ', $options->files));
152 |
153 | if ($options->mode === Modes::MANUAL) {
154 | $shell = new UnixShell($options);
155 | reportMessagesAndExit(
156 | runManualWorkflow($options->diffFile, $options->phpcsUnmodified, $options->phpcsModified),
157 | $options,
158 | $shell
159 | );
160 | return;
161 | }
162 |
163 | if ($options->mode === Modes::SVN) {
164 | $shell = new UnixShell($options);
165 | reportMessagesAndExit(
166 | runSvnWorkflow($options->files, $options, $shell, new CacheManager(new FileCache(), $debug), $debug),
167 | $options,
168 | $shell
169 | );
170 | return;
171 | }
172 |
173 | if ($options->isGitMode()) {
174 | $shell = new UnixShell($options);
175 | reportMessagesAndExit(
176 | runGitWorkflow($options, $shell, new CacheManager(new FileCache(), $debug), $debug),
177 | $options,
178 | $shell
179 | );
180 | return;
181 | }
182 |
183 | printHelp();
184 | exit(1);
185 | }
186 |
187 |
--------------------------------------------------------------------------------
/tests/DiffLineMapTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('bin/review-stuck-orders.php', $name);
27 | }
28 |
29 | public function testGetLineNumberForSimpleAdd() {
30 | $diff = <<assertNull($map->getOldLineNumberForLine(20));
46 | $this->assertEquals(17, $map->getOldLineNumberForLine(17));
47 | $this->assertEquals(20, $map->getOldLineNumberForLine(21));
48 | $this->assertEquals(21, $map->getOldLineNumberForLine(22));
49 | }
50 |
51 | public function testGetLineNumberForAllRemovalDiff() {
52 | $diff = <<assertEquals(1, $map->getOldLineNumberForLine(1));
82 | $this->assertEquals(3, $map->getOldLineNumberForLine(2));
83 | $this->assertEquals(4, $map->getOldLineNumberForLine(3));
84 | $this->assertEquals(7, $map->getOldLineNumberForLine(6));
85 | $this->assertEquals(12, $map->getOldLineNumberForLine(9));
86 | $this->assertEquals(14, $map->getOldLineNumberForLine(11));
87 | $this->assertEquals(22, $map->getOldLineNumberForLine(17));
88 | }
89 |
90 | public function testGetLineNumberForMixedDiff() {
91 | $diff = <<assertEquals(3, $map->getOldLineNumberForLine(1));
114 | $this->assertNull($map->getOldLineNumberForLine(2));
115 | $this->assertNull($map->getOldLineNumberForLine(3));
116 | $this->assertEquals(5, $map->getOldLineNumberForLine(4));
117 | $this->assertEquals(6, $map->getOldLineNumberForLine(5));
118 | $this->assertEquals(9, $map->getOldLineNumberForLine(8));
119 | $this->assertEquals(10, $map->getOldLineNumberForLine(9));
120 | $this->assertNull($map->getOldLineNumberForLine(11));
121 | $this->assertNull($map->getOldLineNumberForLine(12));
122 | $this->assertNull($map->getOldLineNumberForLine(13));
123 | }
124 |
125 | public function testGetLineNumberForDiffWithOriginalAndNewErrorsOnSameLines() {
126 | $diff = <<assertNull($map->getOldLineNumberForLine(20));
143 | }
144 |
145 | public function testGetLineNumberOutsideOfHunks() {
146 | $diff = <<assertEquals(8, $map->getOldLineNumberForLine(7));
169 | $this->assertEquals(12, $map->getOldLineNumberForLine(14));
170 | $this->assertEquals(13, $map->getOldLineNumberForLine(15));
171 | $this->assertEquals(15, $map->getOldLineNumberForLine(17));
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/PhpcsChanged/CacheManager.php:
--------------------------------------------------------------------------------
1 | >>>
25 | */
26 | private $fileDataByPath = [];
27 |
28 | /**
29 | * @var bool
30 | */
31 | private $hasBeenModified = false;
32 |
33 | /**
34 | * @var CacheInterface
35 | */
36 | private $cache;
37 |
38 | /**
39 | * @var CacheObject
40 | */
41 | private $cacheObject;
42 |
43 | /**
44 | * @var callable
45 | */
46 | private $debug;
47 |
48 | public function __construct(CacheInterface $cache, ?callable $debug = null) {
49 | $this->cache = $cache;
50 | $noopDebug =
51 | /** @param string[] $output */
52 | /** @psalm-suppress UnusedClosureParam, MissingClosureParamType */
53 | function(...$output): void {}; // phpcs:ignore VariableAnalysis
54 | $this->debug = $debug ?? $noopDebug;
55 | }
56 |
57 | public function load(): void {
58 | ($this->debug)("Loading cache...");
59 | $this->cacheObject = $this->cache->load();
60 |
61 | // Don't try to use old cache versions
62 | $version = getVersion();
63 | if (! $this->cacheObject->cacheVersion) {
64 | $this->cacheObject->cacheVersion = $version;
65 | }
66 | if ($this->cacheObject->cacheVersion !== $version) {
67 | ($this->debug)("Cache version has changed ({$this->cacheObject->cacheVersion} -> {$version}). Clearing cache.");
68 | $this->clearCache();
69 | $this->cacheObject->cacheVersion = $version;
70 | }
71 |
72 | // Keep a map of cache data so it's faster to access
73 | foreach($this->cacheObject->entries as $entry) {
74 | $this->addCacheEntry($entry);
75 | }
76 |
77 | $this->hasBeenModified = false;
78 | ($this->debug)("Cache loaded.");
79 | }
80 |
81 | public function save(): void {
82 | if (! $this->hasBeenModified) {
83 | ($this->debug)("Not saving cache. It is unchanged.");
84 | return;
85 | }
86 | ($this->debug)("Saving cache.");
87 |
88 | // Copy cache data map back to object
89 | $this->cacheObject->entries = $this->getEntries();
90 |
91 | $this->cache->save($this->cacheObject);
92 | $this->hasBeenModified = false;
93 | }
94 |
95 | public function getCacheVersion(): string {
96 | return $this->cacheObject->cacheVersion;
97 | }
98 |
99 | /**
100 | * @return CacheEntry[]
101 | */
102 | public function getEntries(): array {
103 | return $this->flattenArray($this->fileDataByPath);
104 | }
105 |
106 | /**
107 | * Flatten an array
108 | *
109 | * From https://stackoverflow.com/questions/1319903/how-to-flatten-a-multidimensional-array
110 | *
111 | * @param array|CacheEntry $array
112 | */
113 | private function flattenArray($array): array {
114 | if (!is_array($array)) {
115 | // nothing to do if it's not an array
116 | return array($array);
117 | }
118 |
119 | $result = array();
120 | foreach ($array as $value) {
121 | // explode the sub-array, and add the parts
122 | $result = array_merge($result, $this->flattenArray($value));
123 | }
124 |
125 | return $result;
126 | }
127 |
128 | public function setCacheVersion(string $cacheVersion): void {
129 | if ($this->cacheObject->cacheVersion === $cacheVersion) {
130 | return;
131 | }
132 | ($this->debug)("Cache version has changed ('{$this->cacheObject->cacheVersion}' -> '{$cacheVersion}'). Clearing cache.");
133 | $this->hasBeenModified = true;
134 | $this->clearCache();
135 | $this->cacheObject->cacheVersion = $cacheVersion;
136 | }
137 |
138 | public function getCacheForFile(string $filePath, string $type, string $hash, string $phpcsStandard, string $warningSeverity, string $errorSeverity): ?string {
139 | $phpcsStandard = $this->getPhpcsStandardCacheKey( $phpcsStandard, $warningSeverity, $errorSeverity );
140 |
141 | $entry = $this->fileDataByPath[$filePath][$type][$hash][$phpcsStandard] ?? null;
142 | if (! $entry) {
143 | ($this->debug)("Cache miss: file '{$filePath}', hash '{$hash}', standard '{$phpcsStandard}'");
144 | return null;
145 | }
146 | return $entry->data;
147 | }
148 |
149 | public function setCacheForFile(string $filePath, string $type, string $hash, string $phpcsStandard, string $warningSeverity, string $errorSeverity, string $data): void {
150 |
151 | $phpcsStandard = $this->getPhpcsStandardCacheKey( $phpcsStandard, $warningSeverity, $errorSeverity );
152 |
153 | $this->hasBeenModified = true;
154 | $entry = new CacheEntry();
155 | $entry->phpcsStandard = $phpcsStandard;
156 | $entry->hash = $hash;
157 | $entry->data = $data;
158 | $entry->path = $filePath;
159 | $entry->type = $type;
160 | $this->addCacheEntry($entry);
161 | }
162 |
163 | public function addCacheEntry(CacheEntry $entry): void {
164 | $this->hasBeenModified = true;
165 | $this->pruneOldEntriesForFile($entry);
166 | if (! array_key_exists($entry->path, $this->fileDataByPath)) {
167 | $this->fileDataByPath[$entry->path] = [];
168 | }
169 | if (! array_key_exists($entry->type, $this->fileDataByPath[$entry->path])) {
170 | $this->fileDataByPath[$entry->path][$entry->type] = [];
171 | }
172 | if (! array_key_exists($entry->hash, $this->fileDataByPath[$entry->path][$entry->type])) {
173 | $this->fileDataByPath[$entry->path][$entry->type][$entry->hash] = [];
174 | }
175 | $this->fileDataByPath[$entry->path][$entry->type][$entry->hash][$entry->phpcsStandard] = $entry;
176 | ($this->debug)("Cache add: file '{$entry->path}', type '{$entry->type}', hash '{$entry->hash}', standard '{$entry->phpcsStandard}'");
177 | }
178 |
179 | public function getPhpcsStandardCacheKey( string $phpcsStandard, string $warningSeverity, string $errorSeverity ): string {
180 | $warningSeverity = '' === $warningSeverity ? self::DEFAULT_SEVERITY : $warningSeverity;
181 | $errorSeverity = '' === $errorSeverity ? self::DEFAULT_SEVERITY : $errorSeverity;
182 | if (self::DEFAULT_SEVERITY !== $warningSeverity || self::DEFAULT_SEVERITY !==$errorSeverity) {
183 | $phpcsStandard .= ':w' . $warningSeverity . 'e' . $errorSeverity;
184 | }
185 | return $phpcsStandard;
186 | }
187 |
188 | private function pruneOldEntriesForFile(CacheEntry $newEntry): void {
189 | foreach ($this->getEntries() as $oldEntry) {
190 | if ($this->shouldEntryBeRemoved($oldEntry, $newEntry)) {
191 | $this->removeCacheEntry($oldEntry);
192 | }
193 | }
194 | }
195 |
196 | private function shouldEntryBeRemoved(CacheEntry $oldEntry, CacheEntry $newEntry): bool {
197 | if ($oldEntry->path === $newEntry->path && $oldEntry->type === $newEntry->type && $oldEntry->phpcsStandard === $newEntry->phpcsStandard) {
198 | return true;
199 | }
200 | return false;
201 | }
202 |
203 | public function removeCacheEntry(CacheEntry $entry): void {
204 | if (isset($this->fileDataByPath[$entry->path][$entry->type][$entry->hash][$entry->phpcsStandard])) {
205 | ($this->debug)("Cache remove: file '{$entry->path}', type '{$entry->type}', hash '{$entry->hash}', standard '{$entry->phpcsStandard}'");
206 | unset($this->fileDataByPath[$entry->path][$entry->type][$entry->hash][$entry->phpcsStandard]);
207 | }
208 | }
209 |
210 | public function clearCache(): void {
211 | ($this->debug)("Cache cleared");
212 | $this->hasBeenModified = true;
213 | $this->fileDataByPath = [];
214 | $this->cacheObject->entries = [];
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/tests/fixtures/old-phpcs-output.json:
--------------------------------------------------------------------------------
1 | {"totals":{"errors":0,"warnings":56,"fixable":0},"files":{"bin/review-stuck-orders.php":{"errors":0,"warnings":56,"messages":[{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Import","severity":5,"type":"WARNING","line":20,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":99,"column":16,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":108,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":111,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":114,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":127,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":135,"column":19,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":232,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":257,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":267,"column":54,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":268,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":278,"column":53,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":279,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":299,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":304,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":314,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":321,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":328,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":329,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":330,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":336,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":344,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":351,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":352,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":358,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":365,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":373,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":381,"column":9,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":384,"column":40,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":411,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":416,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":422,"column":24,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":441,"column":37,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":445,"column":37,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":449,"column":15,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":450,"column":13,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":457,"column":21,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":460,"column":9,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":463,"column":49,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":466,"column":12,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":469,"column":41,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":472,"column":41,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":486,"column":58,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":487,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":530,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":595,"column":61,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":643,"column":58,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":657,"column":8,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":664,"column":17,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":665,"column":13,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":684,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":690,"column":66,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":691,"column":36,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":711,"column":12,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":716,"column":19,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":766,"column":1,"fixable":false}]}}}
2 |
--------------------------------------------------------------------------------
/tests/fixtures/new-phpcs-output.json:
--------------------------------------------------------------------------------
1 | {"totals":{"errors":0,"warnings":57,"fixable":0},"files":{"bin/review-stuck-orders.php":{"errors":0,"warnings":57,"messages":[{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Import","severity":5,"type":"WARNING","line":20,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Import","severity":5,"type":"WARNING","line":21,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":100,"column":16,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":109,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":112,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":115,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":128,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":136,"column":19,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":233,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":258,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":268,"column":54,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":269,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":279,"column":53,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":280,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":300,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":305,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":315,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":322,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":329,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":330,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":331,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":337,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":345,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":352,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":353,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":359,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":366,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":374,"column":4,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":382,"column":9,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":385,"column":40,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":412,"column":5,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":417,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":423,"column":24,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":442,"column":37,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":446,"column":37,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":450,"column":15,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":451,"column":13,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":458,"column":21,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":461,"column":9,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":464,"column":49,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":467,"column":12,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":470,"column":41,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":473,"column":41,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":487,"column":58,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":488,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":531,"column":6,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":596,"column":61,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":644,"column":58,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":658,"column":8,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":665,"column":17,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":666,"column":13,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":685,"column":3,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":691,"column":66,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":692,"column":36,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":712,"column":12,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":717,"column":19,"fixable":false},{"message":"Found problem.","source":"ImportDetection.Imports.RequireImports.Symbol","severity":5,"type":"WARNING","line":767,"column":1,"fixable":false}]}}}
2 |
--------------------------------------------------------------------------------
/tests/CheckstyleReporterTest.php:
--------------------------------------------------------------------------------
1 | 'WARNING',
16 | 'severity' => 5,
17 | 'fixable' => false,
18 | 'column' => 5,
19 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
20 | 'line' => 15,
21 | 'message' => 'Found unused symbol Foo.',
22 | ],
23 | ], 'fileA.php');
24 | $expected = <<
26 |
27 |
28 |
29 |
30 |
31 |
32 | EOF;
33 | $reporter = new CheckstyleReporter();
34 | $result = $reporter->getFormattedMessages($messages, []);
35 | $this->assertEquals($expected, $result);
36 | }
37 |
38 | public function testSingleError() {
39 | $messages = PhpcsMessages::fromArrays([
40 | [
41 | 'type' => 'ERROR',
42 | 'severity' => 5,
43 | 'fixable' => false,
44 | 'column' => 5,
45 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
46 | 'line' => 15,
47 | 'message' => 'Found unused symbol Foo.',
48 | ],
49 | ], 'fileA.php');
50 | $expected = <<
52 |
53 |
54 |
55 |
56 |
57 |
58 | EOF;
59 | $reporter = new CheckstyleReporter();
60 | $result = $reporter->getFormattedMessages($messages, []);
61 | $this->assertEquals($expected, $result);
62 | }
63 |
64 | public function testMultipleWarningsWithLongLineNumber() {
65 | $messages = PhpcsMessages::fromArrays([
66 | [
67 | 'type' => 'WARNING',
68 | 'severity' => 5,
69 | 'fixable' => false,
70 | 'column' => 5,
71 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
72 | 'line' => 133825,
73 | 'message' => 'Found unused symbol Foo.',
74 | ],
75 | [
76 | 'type' => 'WARNING',
77 | 'severity' => 5,
78 | 'fixable' => false,
79 | 'column' => 5,
80 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
81 | 'line' => 15,
82 | 'message' => 'Found unused symbol Bar.',
83 | ],
84 | ], 'fileA.php');
85 | $expected = <<
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | EOF;
95 | $reporter = new CheckstyleReporter();
96 | $result = $reporter->getFormattedMessages($messages, []);
97 | $this->assertEquals($expected, $result);
98 | }
99 |
100 | public function testMultipleWarningsErrorsAndFiles() {
101 | $messagesA = PhpcsMessages::fromArrays([
102 | [
103 | 'type' => 'ERROR',
104 | 'severity' => 5,
105 | 'fixable' => true,
106 | 'column' => 2,
107 | 'source' => 'ImportDetection.Imports.RequireImports.Something',
108 | 'line' => 12,
109 | 'message' => 'Found unused symbol Faa.',
110 | ],
111 | [
112 | 'type' => 'ERROR',
113 | 'severity' => 5,
114 | 'fixable' => false,
115 | 'column' => 5,
116 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
117 | 'line' => 15,
118 | 'message' => 'Found unused symbol Foo.',
119 | ],
120 | [
121 | 'type' => 'WARNING',
122 | 'severity' => 5,
123 | 'fixable' => false,
124 | 'column' => 8,
125 | 'source' => 'ImportDetection.Imports.RequireImports.Boom',
126 | 'line' => 18,
127 | 'message' => 'Found unused symbol Bar.',
128 | ],
129 | ], 'fileA.php');
130 | $messagesB = PhpcsMessages::fromArrays([
131 | [
132 | 'type' => 'WARNING',
133 | 'severity' => 5,
134 | 'fixable' => false,
135 | 'column' => 5,
136 | 'source' => 'ImportDetection.Imports.RequireImports.Zoop',
137 | 'line' => 30,
138 | 'message' => 'Found unused symbol Hi.',
139 | ],
140 | ], 'fileB.php');
141 | $messages = PhpcsMessages::merge([$messagesA, $messagesB]);
142 | $expected = <<
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | EOF;
156 | $reporter = new CheckstyleReporter();
157 | $result = $reporter->getFormattedMessages($messages, ['s' => 1]);
158 | $this->assertEquals($expected, $result);
159 | }
160 |
161 | public function testNoWarnings() {
162 | $messages = PhpcsMessages::fromArrays([]);
163 | $expected = <<
165 |
166 |
167 |
168 | EOF;
169 | $reporter = new CheckstyleReporter();
170 | $result = $reporter->getFormattedMessages($messages, []);
171 | $this->assertEquals($expected, $result);
172 | }
173 |
174 | public function testSingleWarningWithNoFilename() {
175 | $messages = PhpcsMessages::fromArrays([
176 | [
177 | 'type' => 'WARNING',
178 | 'severity' => 5,
179 | 'fixable' => false,
180 | 'column' => 5,
181 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
182 | 'line' => 15,
183 | 'message' => 'Found unused symbol Foo.',
184 | ],
185 | ]);
186 | $expected = <<
188 |
189 |
190 |
191 |
192 |
193 |
194 | EOF;
195 | $reporter = new CheckstyleReporter();
196 | $result = $reporter->getFormattedMessages($messages, []);
197 | $this->assertEquals($expected, $result);
198 | }
199 |
200 | public function testXmlEscaping() {
201 | $messages = PhpcsMessages::fromArrays([
202 | [
203 | 'type' => 'ERROR',
204 | 'severity' => 5,
205 | 'fixable' => false,
206 | 'column' => 5,
207 | 'source' => 'Test.Source<>&"',
208 | 'line' => 15,
209 | 'message' => 'Message with & "quotes".',
210 | ],
211 | ], 'fileA.php');
212 | $expected = <<
214 |
215 |
216 |
217 |
218 |
219 |
220 | EOF;
221 | $reporter = new CheckstyleReporter();
222 | $result = $reporter->getFormattedMessages($messages, []);
223 | $this->assertEquals($expected, $result);
224 | }
225 |
226 | public function testGetExitCodeWithMessages() {
227 | $messages = PhpcsMessages::fromArrays([
228 | [
229 | 'type' => 'WARNING',
230 | 'severity' => 5,
231 | 'fixable' => false,
232 | 'column' => 5,
233 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
234 | 'line' => 15,
235 | 'message' => 'Found unused symbol Foo.',
236 | ],
237 | ], 'fileA.php');
238 | $reporter = new CheckstyleReporter();
239 | $this->assertEquals(1, $reporter->getExitCode($messages));
240 | }
241 |
242 | public function testGetExitCodeWithNoMessages() {
243 | $messages = PhpcsMessages::fromArrays([], 'fileA.php');
244 | $reporter = new CheckstyleReporter();
245 | $this->assertEquals(0, $reporter->getExitCode($messages));
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/tests/JsonReporterTest.php:
--------------------------------------------------------------------------------
1 | 'WARNING',
15 | 'severity' => 5,
16 | 'fixable' => false,
17 | 'column' => 5,
18 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
19 | 'line' => 15,
20 | 'message' => 'Found unused symbol Foo.',
21 | ],
22 | ], 'fileA.php');
23 | $expected = <<getFormattedMessages($messages, []);
28 | $this->assertEquals($expected, $result);
29 | }
30 |
31 | public function testSingleWarningWithShowCodeOption() {
32 | $messages = PhpcsMessages::fromArrays([
33 | [
34 | 'type' => 'WARNING',
35 | 'severity' => 5,
36 | 'fixable' => false,
37 | 'column' => 5,
38 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
39 | 'line' => 15,
40 | 'message' => 'Found unused symbol Foo.',
41 | ],
42 | ], 'fileA.php');
43 | $expected = <<getFormattedMessages($messages, ['s' => 1]);
48 | $this->assertEquals($expected, $result);
49 | }
50 |
51 | public function testSingleWarningWithShowCodeOptionAndNoCode() {
52 | $messages = PhpcsMessages::fromArrays([
53 | [
54 | 'type' => 'WARNING',
55 | 'severity' => 5,
56 | 'fixable' => false,
57 | 'column' => 5,
58 | 'line' => 15,
59 | 'message' => 'Found unused symbol Foo.',
60 | ],
61 | ], 'fileA.php');
62 | $expected = <<getFormattedMessages($messages, ['s' => 1]);
67 | $this->assertEquals($expected, $result);
68 | }
69 |
70 | public function testMultipleWarningsWithLongLineNumber() {
71 | $messages = PhpcsMessages::fromArrays([
72 | [
73 | 'type' => 'WARNING',
74 | 'severity' => 5,
75 | 'fixable' => false,
76 | 'column' => 5,
77 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
78 | 'line' => 133825,
79 | 'message' => 'Found unused symbol Foo.',
80 | ],
81 | [
82 | 'type' => 'WARNING',
83 | 'severity' => 5,
84 | 'fixable' => false,
85 | 'column' => 5,
86 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
87 | 'line' => 15,
88 | 'message' => 'Found unused symbol Bar.',
89 | ],
90 | ], 'fileA.php');
91 | $expected = <<getFormattedMessages($messages, []);
96 | $this->assertEquals($expected, $result);
97 | }
98 |
99 | public function testMultipleWarningsErrorsAndFiles() {
100 | $messagesA = PhpcsMessages::fromArrays([
101 | [
102 | 'type' => 'ERROR',
103 | 'severity' => 5,
104 | 'fixable' => true,
105 | 'column' => 2,
106 | 'source' => 'ImportDetection.Imports.RequireImports.Something',
107 | 'line' => 12,
108 | 'message' => 'Found unused symbol Faa.',
109 | ],
110 | [
111 | 'type' => 'ERROR',
112 | 'severity' => 5,
113 | 'fixable' => false,
114 | 'column' => 5,
115 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
116 | 'line' => 15,
117 | 'message' => 'Found unused symbol Foo.',
118 | ],
119 | [
120 | 'type' => 'WARNING',
121 | 'severity' => 5,
122 | 'fixable' => false,
123 | 'column' => 8,
124 | 'source' => 'ImportDetection.Imports.RequireImports.Boom',
125 | 'line' => 18,
126 | 'message' => 'Found unused symbol Bar.',
127 | ],
128 | [
129 | 'type' => 'WARNING',
130 | 'severity' => 5,
131 | 'fixable' => false,
132 | 'column' => 5,
133 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
134 | 'line' => 22,
135 | 'message' => 'Found unused symbol Foo.',
136 | ],
137 | ], 'fileA.php');
138 | $messagesB = PhpcsMessages::fromArrays([
139 | [
140 | 'type' => 'WARNING',
141 | 'severity' => 5,
142 | 'fixable' => false,
143 | 'column' => 5,
144 | 'source' => 'ImportDetection.Imports.RequireImports.Zoop',
145 | 'line' => 30,
146 | 'message' => 'Found unused symbol Hi.',
147 | ],
148 | ], 'fileB.php');
149 | $messages = PhpcsMessages::merge([$messagesA, $messagesB]);
150 | $expected = <<getFormattedMessages($messages, ['s' => 1]);
155 | $this->assertEquals($expected, $result);
156 | }
157 |
158 | public function testNoWarnings() {
159 | $messages = PhpcsMessages::fromArrays([]);
160 | $expected = <<getFormattedMessages($messages, []);
165 | $this->assertEquals($expected, $result);
166 | }
167 |
168 | public function testSingleWarningWithNoFilename() {
169 | $messages = PhpcsMessages::fromArrays([
170 | [
171 | 'type' => 'WARNING',
172 | 'severity' => 5,
173 | 'fixable' => false,
174 | 'column' => 5,
175 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
176 | 'line' => 15,
177 | 'message' => 'Found unused symbol Foo.',
178 | ],
179 | ]);
180 | $expected = <<getFormattedMessages($messages, []);
185 | $this->assertEquals($expected, $result);
186 | }
187 |
188 | public function testGetExitCodeWithMessages() {
189 | $messages = PhpcsMessages::fromArrays([
190 | [
191 | 'type' => 'WARNING',
192 | 'severity' => 5,
193 | 'fixable' => false,
194 | 'column' => 5,
195 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
196 | 'line' => 15,
197 | 'message' => 'Found unused symbol Foo.',
198 | ],
199 | ], 'fileA.php');
200 | $reporter = new JsonReporter();
201 | $this->assertEquals(1, $reporter->getExitCode($messages));
202 | }
203 |
204 | public function testGetExitCodeWithNoMessages() {
205 | $messages = PhpcsMessages::fromArrays([], 'fileA.php');
206 | $reporter = new JsonReporter();
207 | $this->assertEquals(0, $reporter->getExitCode($messages));
208 | }
209 | }
210 |
211 |
--------------------------------------------------------------------------------
/tests/FullReporterTest.php:
--------------------------------------------------------------------------------
1 | 'WARNING',
15 | 'severity' => 5,
16 | 'fixable' => false,
17 | 'column' => 5,
18 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
19 | 'line' => 15,
20 | 'message' => 'Found unused symbol Foo.',
21 | ],
22 | ], 'fileA.php');
23 | $expected = <<getFormattedMessages($messages, []);
35 | $this->assertEquals($expected, $result);
36 | }
37 |
38 | public function testSingleWarningWithShowCodeOption() {
39 | $messages = PhpcsMessages::fromArrays([
40 | [
41 | 'type' => 'WARNING',
42 | 'severity' => 5,
43 | 'fixable' => false,
44 | 'column' => 5,
45 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
46 | 'line' => 15,
47 | 'message' => 'Found unused symbol Foo.',
48 | ],
49 | ], 'fileA.php');
50 | $expected = <<getFormattedMessages($messages, ['s' => 1]);
62 | $this->assertEquals($expected, $result);
63 | }
64 |
65 | public function testSingleWarningWithShowCodeOptionAndNoCode() {
66 | $messages = PhpcsMessages::fromArrays([
67 | [
68 | 'type' => 'WARNING',
69 | 'severity' => 5,
70 | 'fixable' => false,
71 | 'column' => 5,
72 | 'line' => 15,
73 | 'message' => 'Found unused symbol Foo.',
74 | ],
75 | ], 'fileA.php');
76 | $expected = <<getFormattedMessages($messages, ['s' => 1]);
88 | $this->assertEquals($expected, $result);
89 | }
90 |
91 | public function testMultipleWarningsWithLongLineNumber() {
92 | $messages = PhpcsMessages::fromArrays([
93 | [
94 | 'type' => 'WARNING',
95 | 'severity' => 5,
96 | 'fixable' => false,
97 | 'column' => 5,
98 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
99 | 'line' => 133825,
100 | 'message' => 'Found unused symbol Foo.',
101 | ],
102 | [
103 | 'type' => 'WARNING',
104 | 'severity' => 5,
105 | 'fixable' => false,
106 | 'column' => 5,
107 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
108 | 'line' => 15,
109 | 'message' => 'Found unused symbol Bar.',
110 | ],
111 | ], 'fileA.php');
112 | $expected = <<getFormattedMessages($messages, []);
125 | $this->assertEquals($expected, $result);
126 | }
127 |
128 | public function testMultipleWarningsErrorsAndFiles() {
129 | $messagesA = PhpcsMessages::fromArrays([
130 | [
131 | 'type' => 'ERROR',
132 | 'severity' => 5,
133 | 'fixable' => true,
134 | 'column' => 2,
135 | 'source' => 'ImportDetection.Imports.RequireImports.Something',
136 | 'line' => 12,
137 | 'message' => 'Found unused symbol Faa.',
138 | ],
139 | [
140 | 'type' => 'ERROR',
141 | 'severity' => 5,
142 | 'fixable' => false,
143 | 'column' => 5,
144 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
145 | 'line' => 15,
146 | 'message' => 'Found unused symbol Foo.',
147 | ],
148 | [
149 | 'type' => 'WARNING',
150 | 'severity' => 5,
151 | 'fixable' => false,
152 | 'column' => 8,
153 | 'source' => 'ImportDetection.Imports.RequireImports.Boom',
154 | 'line' => 18,
155 | 'message' => 'Found unused symbol Bar.',
156 | ],
157 | [
158 | 'type' => 'WARNING',
159 | 'severity' => 5,
160 | 'fixable' => false,
161 | 'column' => 5,
162 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
163 | 'line' => 22,
164 | 'message' => 'Found unused symbol Foo.',
165 | ],
166 | ], 'fileA.php');
167 | $messagesB = PhpcsMessages::fromArrays([
168 | [
169 | 'type' => 'WARNING',
170 | 'severity' => 5,
171 | 'fixable' => false,
172 | 'column' => 5,
173 | 'source' => 'ImportDetection.Imports.RequireImports.Zoop',
174 | 'line' => 30,
175 | 'message' => 'Found unused symbol Hi.',
176 | ],
177 | ], 'fileB.php');
178 | $messages = PhpcsMessages::merge([$messagesA, $messagesB]);
179 | $expected = <<getFormattedMessages($messages, ['s' => 1]);
202 | $this->assertEquals($expected, $result);
203 | }
204 |
205 | public function testNoWarnings() {
206 | $messages = PhpcsMessages::fromArrays([]);
207 | $expected = <<getFormattedMessages($messages, []);
212 | $this->assertEquals($expected, $result);
213 | }
214 |
215 | public function testSingleWarningWithNoFilename() {
216 | $messages = PhpcsMessages::fromArrays([
217 | [
218 | 'type' => 'WARNING',
219 | 'severity' => 5,
220 | 'fixable' => false,
221 | 'column' => 5,
222 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
223 | 'line' => 15,
224 | 'message' => 'Found unused symbol Foo.',
225 | ],
226 | ]);
227 | $expected = <<getFormattedMessages($messages, []);
239 | $this->assertEquals($expected, $result);
240 | }
241 |
242 | public function testGetExitCodeWithMessages() {
243 | $messages = PhpcsMessages::fromArrays([
244 | [
245 | 'type' => 'WARNING',
246 | 'severity' => 5,
247 | 'fixable' => false,
248 | 'column' => 5,
249 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
250 | 'line' => 15,
251 | 'message' => 'Found unused symbol Foo.',
252 | ],
253 | ], 'fileA.php');
254 | $reporter = new FullReporter();
255 | $this->assertEquals(1, $reporter->getExitCode($messages));
256 | }
257 |
258 | public function testGetExitCodeWithNoMessages() {
259 | $messages = PhpcsMessages::fromArrays([], 'fileA.php');
260 | $reporter = new FullReporter();
261 | $this->assertEquals(0, $reporter->getExitCode($messages));
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/tests/JunitReporterTest.php:
--------------------------------------------------------------------------------
1 | 'WARNING',
16 | 'severity' => 5,
17 | 'fixable' => false,
18 | 'column' => 5,
19 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
20 | 'line' => 15,
21 | 'message' => 'Found unused symbol Foo.',
22 | ],
23 | ], 'fileA.php');
24 | $expected = <<
26 |
27 |
28 |
29 | Line 15, Column 5: Found unused symbol Foo. (Severity: 5)
30 |
31 |
32 |
33 |
34 | EOF;
35 | $reporter = new JunitReporter();
36 | $result = $reporter->getFormattedMessages($messages, []);
37 | $this->assertEquals($expected, $result);
38 | }
39 |
40 | public function testSingleError() {
41 | $messages = PhpcsMessages::fromArrays([
42 | [
43 | 'type' => 'ERROR',
44 | 'severity' => 5,
45 | 'fixable' => false,
46 | 'column' => 5,
47 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
48 | 'line' => 15,
49 | 'message' => 'Found unused symbol Foo.',
50 | ],
51 | ], 'fileA.php');
52 | $expected = <<
54 |
55 |
56 |
57 | Line 15, Column 5: Found unused symbol Foo. (Severity: 5)
58 |
59 |
60 |
61 |
62 | EOF;
63 | $reporter = new JunitReporter();
64 | $result = $reporter->getFormattedMessages($messages, []);
65 | $this->assertEquals($expected, $result);
66 | }
67 |
68 | public function testMultipleWarningsWithLongLineNumber() {
69 | $messages = PhpcsMessages::fromArrays([
70 | [
71 | 'type' => 'WARNING',
72 | 'severity' => 5,
73 | 'fixable' => false,
74 | 'column' => 5,
75 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
76 | 'line' => 133825,
77 | 'message' => 'Found unused symbol Foo.',
78 | ],
79 | [
80 | 'type' => 'WARNING',
81 | 'severity' => 5,
82 | 'fixable' => false,
83 | 'column' => 5,
84 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
85 | 'line' => 15,
86 | 'message' => 'Found unused symbol Bar.',
87 | ],
88 | ], 'fileA.php');
89 | $expected = <<
91 |
92 |
93 |
94 | Line 133825, Column 5: Found unused symbol Foo. (Severity: 5)
95 |
96 |
97 | Line 15, Column 5: Found unused symbol Bar. (Severity: 5)
98 |
99 |
100 |
101 |
102 | EOF;
103 | $reporter = new JunitReporter();
104 | $result = $reporter->getFormattedMessages($messages, []);
105 | $this->assertEquals($expected, $result);
106 | }
107 |
108 | public function testMultipleWarningsErrorsAndFiles() {
109 | $messagesA = PhpcsMessages::fromArrays([
110 | [
111 | 'type' => 'ERROR',
112 | 'severity' => 5,
113 | 'fixable' => true,
114 | 'column' => 2,
115 | 'source' => 'ImportDetection.Imports.RequireImports.Something',
116 | 'line' => 12,
117 | 'message' => 'Found unused symbol Faa.',
118 | ],
119 | [
120 | 'type' => 'ERROR',
121 | 'severity' => 5,
122 | 'fixable' => false,
123 | 'column' => 5,
124 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
125 | 'line' => 15,
126 | 'message' => 'Found unused symbol Foo.',
127 | ],
128 | [
129 | 'type' => 'WARNING',
130 | 'severity' => 5,
131 | 'fixable' => false,
132 | 'column' => 8,
133 | 'source' => 'ImportDetection.Imports.RequireImports.Boom',
134 | 'line' => 18,
135 | 'message' => 'Found unused symbol Bar.',
136 | ],
137 | [
138 | 'type' => 'WARNING',
139 | 'severity' => 5,
140 | 'fixable' => false,
141 | 'column' => 5,
142 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
143 | 'line' => 22,
144 | 'message' => 'Found unused symbol Foo.',
145 | ],
146 | ], 'fileA.php');
147 | $messagesB = PhpcsMessages::fromArrays([
148 | [
149 | 'type' => 'WARNING',
150 | 'severity' => 5,
151 | 'fixable' => false,
152 | 'column' => 5,
153 | 'source' => 'ImportDetection.Imports.RequireImports.Zoop',
154 | 'line' => 30,
155 | 'message' => 'Found unused symbol Hi.',
156 | ],
157 | ], 'fileB.php');
158 | $messages = PhpcsMessages::merge([$messagesA, $messagesB]);
159 | $expected = <<
161 |
162 |
163 |
164 | Line 12, Column 2: Found unused symbol Faa. (Severity: 5)
165 |
166 |
167 | Line 15, Column 5: Found unused symbol Foo. (Severity: 5)
168 |
169 |
170 | Line 18, Column 8: Found unused symbol Bar. (Severity: 5)
171 |
172 |
173 | Line 22, Column 5: Found unused symbol Foo. (Severity: 5)
174 |
175 |
176 |
177 |
178 | Line 30, Column 5: Found unused symbol Hi. (Severity: 5)
179 |
180 |
181 |
182 |
183 | EOF;
184 | $reporter = new JunitReporter();
185 | $result = $reporter->getFormattedMessages($messages, ['s' => 1]);
186 | $this->assertEquals($expected, $result);
187 | }
188 |
189 | public function testNoWarnings() {
190 | $messages = PhpcsMessages::fromArrays([]);
191 | $expected = <<
193 |
194 |
195 |
196 |
197 |
198 | EOF;
199 | $reporter = new JunitReporter();
200 | $result = $reporter->getFormattedMessages($messages, []);
201 | $this->assertEquals($expected, $result);
202 | }
203 |
204 | public function testSingleWarningWithNoFilename() {
205 | $messages = PhpcsMessages::fromArrays([
206 | [
207 | 'type' => 'WARNING',
208 | 'severity' => 5,
209 | 'fixable' => false,
210 | 'column' => 5,
211 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
212 | 'line' => 15,
213 | 'message' => 'Found unused symbol Foo.',
214 | ],
215 | ]);
216 | $expected = <<
218 |
219 |
220 |
221 | Line 15, Column 5: Found unused symbol Foo. (Severity: 5)
222 |
223 |
224 |
225 |
226 | EOF;
227 | $reporter = new JunitReporter();
228 | $result = $reporter->getFormattedMessages($messages, []);
229 | $this->assertEquals($expected, $result);
230 | }
231 |
232 | public function testXmlEscaping() {
233 | $messages = PhpcsMessages::fromArrays([
234 | [
235 | 'type' => 'ERROR',
236 | 'severity' => 5,
237 | 'fixable' => false,
238 | 'column' => 5,
239 | 'source' => 'Test.Source<>&"',
240 | 'line' => 15,
241 | 'message' => 'Message with & "quotes".',
242 | ],
243 | ], 'fileA.php');
244 | $expected = <<
246 |
247 |
248 |
249 | Line 15, Column 5: Message with <xml> & "quotes". (Severity: 5)
250 |
251 |
252 |
253 |
254 | EOF;
255 | $reporter = new JunitReporter();
256 | $result = $reporter->getFormattedMessages($messages, []);
257 | $this->assertEquals($expected, $result);
258 | }
259 |
260 | public function testGetExitCodeWithMessages() {
261 | $messages = PhpcsMessages::fromArrays([
262 | [
263 | 'type' => 'WARNING',
264 | 'severity' => 5,
265 | 'fixable' => false,
266 | 'column' => 5,
267 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
268 | 'line' => 15,
269 | 'message' => 'Found unused symbol Foo.',
270 | ],
271 | ], 'fileA.php');
272 | $reporter = new JunitReporter();
273 | $this->assertEquals(1, $reporter->getExitCode($messages));
274 | }
275 |
276 | public function testGetExitCodeWithNoMessages() {
277 | $messages = PhpcsMessages::fromArrays([], 'fileA.php');
278 | $reporter = new JunitReporter();
279 | $this->assertEquals(0, $reporter->getExitCode($messages));
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/tests/XmlReporterTest.php:
--------------------------------------------------------------------------------
1 | 'WARNING',
18 | 'severity' => 5,
19 | 'fixable' => false,
20 | 'column' => 5,
21 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
22 | 'line' => 15,
23 | 'message' => 'Found unused symbol Foo.',
24 | ],
25 | ], 'fileA.php');
26 | $expected = <<
28 |
29 |
30 | Found unused symbol Foo.
31 |
32 |
33 |
34 | EOF;
35 | $options = new CliOptions();
36 | $shell = new TestShell($options, []);
37 | $shell->registerExecutable('phpcs');
38 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
39 | $reporter = new TestXmlReporter($options, $shell);
40 | $result = $reporter->getFormattedMessages($messages, []);
41 | $this->assertEquals($expected, $result);
42 | }
43 |
44 | public function testSingleWarningWithShowCodeOption() {
45 | $messages = PhpcsMessages::fromArrays([
46 | [
47 | 'type' => 'WARNING',
48 | 'severity' => 5,
49 | 'fixable' => false,
50 | 'column' => 5,
51 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
52 | 'line' => 15,
53 | 'message' => 'Found unused symbol Foo.',
54 | ],
55 | ], 'fileA.php');
56 | $expected = <<
58 |
59 |
60 | Found unused symbol Foo.
61 |
62 |
63 |
64 | EOF;
65 | $options = new CliOptions();
66 | $shell = new TestShell($options, []);
67 | $shell->registerExecutable('phpcs');
68 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
69 | $reporter = new TestXmlReporter($options, $shell);
70 | $result = $reporter->getFormattedMessages($messages, ['s' => 1]);
71 | $this->assertEquals($expected, $result);
72 | }
73 |
74 | public function testSingleWarningWithShowCodeOptionAndNoCode() {
75 | $messages = PhpcsMessages::fromArrays([
76 | [
77 | 'type' => 'WARNING',
78 | 'severity' => 5,
79 | 'fixable' => false,
80 | 'column' => 5,
81 | 'line' => 15,
82 | 'message' => 'Found unused symbol Foo.',
83 | ],
84 | ], 'fileA.php');
85 | $expected = <<
87 |
88 |
89 | Found unused symbol Foo.
90 |
91 |
92 |
93 | EOF;
94 | $options = new CliOptions();
95 | $shell = new TestShell($options, []);
96 | $shell->registerExecutable('phpcs');
97 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
98 | $reporter = new TestXmlReporter($options, $shell);
99 | $result = $reporter->getFormattedMessages($messages, ['s' => 1]);
100 | $this->assertEquals($expected, $result);
101 | }
102 |
103 | public function testMultipleWarningsWithLongLineNumber() {
104 | $messages = PhpcsMessages::fromArrays([
105 | [
106 | 'type' => 'WARNING',
107 | 'severity' => 5,
108 | 'fixable' => false,
109 | 'column' => 5,
110 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
111 | 'line' => 133825,
112 | 'message' => 'Found unused symbol Foo.',
113 | ],
114 | [
115 | 'type' => 'WARNING',
116 | 'severity' => 5,
117 | 'fixable' => false,
118 | 'column' => 5,
119 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
120 | 'line' => 15,
121 | 'message' => 'Found unused symbol Bar.',
122 | ],
123 | ], 'fileA.php');
124 | $expected = <<
126 |
127 |
128 | Found unused symbol Foo.
129 | Found unused symbol Bar.
130 |
131 |
132 |
133 | EOF;
134 | $options = new CliOptions();
135 | $shell = new TestShell($options, []);
136 | $shell->registerExecutable('phpcs');
137 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
138 | $reporter = new TestXmlReporter($options, $shell);
139 | $result = $reporter->getFormattedMessages($messages, []);
140 | $this->assertEquals($expected, $result);
141 | }
142 |
143 | public function testMultipleWarningsErrorsAndFiles() {
144 | $messagesA = PhpcsMessages::fromArrays([
145 | [
146 | 'type' => 'ERROR',
147 | 'severity' => 5,
148 | 'fixable' => true,
149 | 'column' => 2,
150 | 'source' => 'ImportDetection.Imports.RequireImports.Something',
151 | 'line' => 12,
152 | 'message' => 'Found unused symbol Faa.',
153 | ],
154 | [
155 | 'type' => 'ERROR',
156 | 'severity' => 5,
157 | 'fixable' => false,
158 | 'column' => 5,
159 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
160 | 'line' => 15,
161 | 'message' => 'Found unused symbol Foo.',
162 | ],
163 | [
164 | 'type' => 'WARNING',
165 | 'severity' => 5,
166 | 'fixable' => false,
167 | 'column' => 8,
168 | 'source' => 'ImportDetection.Imports.RequireImports.Boom',
169 | 'line' => 18,
170 | 'message' => 'Found unused symbol Bar.',
171 | ],
172 | [
173 | 'type' => 'WARNING',
174 | 'severity' => 5,
175 | 'fixable' => false,
176 | 'column' => 5,
177 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
178 | 'line' => 22,
179 | 'message' => 'Found unused symbol Foo.',
180 | ],
181 | ], 'fileA.php');
182 | $messagesB = PhpcsMessages::fromArrays([
183 | [
184 | 'type' => 'WARNING',
185 | 'severity' => 5,
186 | 'fixable' => false,
187 | 'column' => 5,
188 | 'source' => 'ImportDetection.Imports.RequireImports.Zoop',
189 | 'line' => 30,
190 | 'message' => 'Found unused symbol Hi.',
191 | ],
192 | ], 'fileB.php');
193 | $messages = PhpcsMessages::merge([$messagesA, $messagesB]);
194 | $expected = <<
196 |
197 |
198 | Found unused symbol Faa.
199 | Found unused symbol Foo.
200 | Found unused symbol Bar.
201 | Found unused symbol Foo.
202 |
203 |
204 | Found unused symbol Hi.
205 |
206 |
207 |
208 | EOF;
209 | $options = new CliOptions();
210 | $shell = new TestShell($options, []);
211 | $shell->registerExecutable('phpcs');
212 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
213 | $reporter = new TestXmlReporter($options, $shell);
214 | $result = $reporter->getFormattedMessages($messages, ['s' => 1]);
215 | $this->assertEquals($expected, $result);
216 | }
217 |
218 | public function testNoWarnings() {
219 | $messages = PhpcsMessages::fromArrays([]);
220 | $expected = <<
222 |
223 |
224 |
225 |
226 |
227 | EOF;
228 | $options = new CliOptions();
229 | $shell = new TestShell($options, []);
230 | $shell->registerExecutable('phpcs');
231 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
232 | $reporter = new TestXmlReporter($options, $shell);
233 | $result = $reporter->getFormattedMessages($messages, []);
234 | $this->assertEquals($expected, $result);
235 | }
236 |
237 | public function testSingleWarningWithNoFilename() {
238 | $messages = PhpcsMessages::fromArrays([
239 | [
240 | 'type' => 'WARNING',
241 | 'severity' => 5,
242 | 'fixable' => false,
243 | 'column' => 5,
244 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
245 | 'line' => 15,
246 | 'message' => 'Found unused symbol Foo.',
247 | ],
248 | ]);
249 | $expected = <<
251 |
252 |
253 | Found unused symbol Foo.
254 |
255 |
256 |
257 | EOF;
258 | $options = new CliOptions();
259 | $shell = new TestShell($options, []);
260 | $shell->registerExecutable('phpcs');
261 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
262 | $reporter = new TestXmlReporter($options, $shell);
263 | $result = $reporter->getFormattedMessages($messages, []);
264 | $this->assertEquals($expected, $result);
265 | }
266 |
267 | public function testGetExitCodeWithMessages() {
268 | $messages = PhpcsMessages::fromArrays([
269 | [
270 | 'type' => 'WARNING',
271 | 'severity' => 5,
272 | 'fixable' => false,
273 | 'column' => 5,
274 | 'source' => 'ImportDetection.Imports.RequireImports.Import',
275 | 'line' => 15,
276 | 'message' => 'Found unused symbol Foo.',
277 | ],
278 | ], 'fileA.php');
279 | $options = new CliOptions();
280 | $shell = new TestShell($options, []);
281 | $shell->registerExecutable('phpcs');
282 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
283 | $reporter = new TestXmlReporter($options, $shell);
284 | $this->assertEquals(1, $reporter->getExitCode($messages));
285 | }
286 |
287 | public function testGetExitCodeWithNoMessages() {
288 | $messages = PhpcsMessages::fromArrays([], 'fileA.php');
289 | $options = new CliOptions();
290 | $shell = new TestShell($options, []);
291 | $shell->registerExecutable('phpcs');
292 | $shell->registerCommand('phpcs --version', 'PHP_CodeSniffer version 1.2.3 (stable) by Squiz (http://www.squiz.net)');
293 | $reporter = new TestXmlReporter($options, $shell);
294 | $this->assertEquals(0, $reporter->getExitCode($messages));
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Run [phpcs](https://github.com/squizlabs/PHP_CodeSniffer) on files and only report new warnings/errors compared to the previous version.
2 |
3 | This is both a PHP library that can be used manually as well as a CLI script that you can just run on your files.
4 |
5 | ## What is this for?
6 |
7 | Let's say that you need to add a feature to a large legacy file which has many phpcs errors. If you try to run phpcs on that file, there is so much noise it's impossible to notice any errors which you may have added yourself.
8 |
9 | Using this script you can get phpcs output which applies only to the changes you have made and ignores the unchanged errors.
10 |
11 | ## Installation
12 |
13 | ```
14 | composer global require sirbrillig/phpcs-changed
15 | ```
16 |
17 | ## CLI Usage
18 |
19 | 👩💻👩💻👩💻
20 |
21 | To make this work, you need to be able to provide data about the previous version of your code. `phpcs-changed` can get this data itself if you use svn or git, or you can provide it manually.
22 |
23 | Here's an example using `phpcs-changed` with the `--svn` option:
24 |
25 | ```
26 | phpcs-changed --svn file.php
27 | ```
28 |
29 | If you wanted to use svn and phpcs manually, this produces the same output:
30 |
31 | ```
32 | svn diff file.php > file.php.diff
33 | svn cat file.php | phpcs --report=json -q > file.php.orig.phpcs
34 | cat file.php | phpcs --report=json -q > file.php.phpcs
35 | phpcs-changed --diff file.php.diff --phpcs-unmodified file.php.orig.phpcs --phpcs-modified file.php.phpcs
36 | ```
37 |
38 | Both will output something like:
39 |
40 | ```
41 | FILE: file.php
42 | -----------------------------------------------------------------------------------------------
43 | FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
44 | -----------------------------------------------------------------------------------------------
45 | 76 | WARNING | Variable $foobar is undefined.
46 | -----------------------------------------------------------------------------------------------
47 | ```
48 |
49 | Or, with `--report json`:
50 |
51 | ```json
52 | {
53 | "totals": {
54 | "errors": 0,
55 | "warnings": 1,
56 | "fixable": 0
57 | },
58 | "files": {
59 | "file.php": {
60 | "errors": 0,
61 | "warnings": 1,
62 | "messages": [
63 | {
64 | "line": 76,
65 | "message": "Variable $foobar is undefined.",
66 | "source": "VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable",
67 | "severity": 5,
68 | "fixable": false,
69 | "type": "WARNING",
70 | "column": 8
71 | }
72 | ]
73 | }
74 | }
75 | }
76 | ```
77 |
78 | If the file was versioned by git, we can do the same with the `--git` option:
79 |
80 | ```
81 | phpcs-changed --git --git-unstaged file.php
82 | ```
83 |
84 | When using `--git`, you should also specify `--git-staged`, `--git-unstaged`, or `--git-base`.
85 |
86 | `--git-staged` compares the currently staged changes (as the modified version of the files) to the current HEAD (as the unmodified version of the files). This is the default.
87 |
88 | `--git-unstaged` compares the current (unstaged) working copy changes (as the modified version of the files) to the either the currently staged changes, or if there are none, the current HEAD (as the unmodified version of the files).
89 |
90 | `--git-base`, followed by a git object, compares the current HEAD (as the modified version of the files) to the specified [git object](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) (as the unmodified version of the file) which can be a branch name, a commit, or some other valid git object.
91 |
92 | ```
93 | git checkout add-new-feature
94 | phpcs-changed --git --git-base master file.php
95 | ```
96 |
97 | ### CLI Options
98 |
99 | More than one file can be specified after a version control option, including globs and directories. If any file is a directory, phpcs-changed will scan the directory for all files ending in `.php` and process them. For example: `phpcs-changed --git src/lib test/**/*.php` will operate on all the php files in the `src/lib/` and `test/` directories.
100 |
101 | You can use `--ignore` to ignore any directory, file, or paths matching provided pattern(s). For example.: `--ignore=bin/*,vendor/*` would ignore any files in bin directory, as well as in vendor.
102 |
103 | You can use `--report` to customize the output type. `full` (the default) is human-readable, `json` prints a JSON object as shown above, and 'xml' can be used by IDEs. These match the phpcs reporters of the same names. `junit` can also be used for [JUnit XML](https://github.com/testmoapp/junitxml) which can be helpful for test runners. `checkstyle` will output Checkstyle format.
104 |
105 | You can use `--standard` to specify a specific phpcs standard to run. This matches the phpcs option of the same name.
106 |
107 | You can use `--extensions` to specify a list of valid file extensions that phpcs should check. These should be separated by commas. This matches the phpcs option of the same name.
108 |
109 | You can also use the `-s` option to Always show sniff codes after each error in the full reporter. This matches the phpcs option of the same name.
110 |
111 | The `--error-severity` and `--warning-severity` options can be used for instructing the `phpcs` command on what error and warning severity to report. Those values are being passed through to `phpcs` itself. Consult `phpcs` documentation for severity settings.
112 |
113 | The `--cache` option will enable caching of phpcs output and can significantly improve performance for slow phpcs standards or when running with high frequency. There are actually two caches: one for the phpcs scan of the unmodified version of the file and one for the phpcs scan of the modified version. The unmodified version phpcs output cache is invalidated when the version control revision changes or when the phpcs standard changes. The modified version phpcs output cache is invalidated when the file hash changes or when the phpcs standard changes.
114 |
115 | The `--no-cache` option will disable the cache if it's been enabled. (This may also be useful in the future if caching is made the default.)
116 |
117 | The `--clear-cache` option will clear the cache before running. This works with or without caching enabled.
118 |
119 | The `--always-exit-zero` option will make sure the run will always exit with `0` return code, no matter if there are lint issues or not. When not set, `1` is returned in case there are some lint issues, `0` if no lint issues were found. The flag makes the phpcs-changed working with other scripts which could detect `1` as failure in the script run (eg.: arcanist).
120 |
121 | The `--no-verify-git-file` option will prevent checking to see if a file is tracked by git during the git workflow. This can save a little time if you can guarantee this otherwise.
122 |
123 | The `--no-cache-git-root` option will prevent caching the check used by the git workflow to determine the git root within a single execution. This is probably only useful for automated tests.
124 |
125 | The `--arc-lint` option can be used when the phpcs-changed is run via arcanist, as it skips some checks, which are performed by arcanist itself. It leads to better performance when used with arcanist. (Equivalent to `--no-verify-git-file --always-exit-zero`.)
126 |
127 | The `--svn-path`, `--git-path`, `--cat-path`, and `--phpcs-path` options can be used to specify the paths to the executables of the same names. If these options are not set, the program will try to use the `SVN`, `GIT`, `CAT`, and `PHPCS` env variables. If those are also not set, the program will default to `svn`, `git`, `cat`, and `phpcs`, respectively, assuming that each command will be in the system's `PATH`.
128 |
129 | For phpcs, if the path is not overridden, and a `phpcs` executable exists under the `vendor/bin` directory where this command is run, that executable will be used instead of relying on the PATH. You can disable this feature with the `--no-vendor-phpcs` option.
130 |
131 | The `--debug` option will show every step taken by the script.
132 |
133 | ## PHP Library
134 |
135 | 🐘🐘🐘
136 |
137 | ### getNewPhpcsMessagesFromFiles
138 |
139 | This library exposes a function `PhpcsMessages\getNewPhpcsMessagesFromFiles()` which takes three arguments:
140 |
141 | - A file path containing the full unified diff of a single file.
142 | - A file path containing the messages resulting from running phpcs on the file before your recent changes.
143 | - A file path containing the messages resulting from running phpcs on the file after your recent changes.
144 |
145 | It will return an instance of `PhpcsMessages` which is a filtered list of the third argument above where every line that was present in the second argument has been removed.
146 |
147 | `PhpcsMessages` represents the output of running phpcs.
148 |
149 | To read the phpcs JSON output from an instance of `PhpcsMessages`, you can use the `toPhpcsJson()` method. For example:
150 |
151 | ```php
152 | use function PhpcsChanged\getNewPhpcsMessagesFromFiles;
153 |
154 | $changedMessages = getNewPhpcsMessagesFromFiles(
155 | $unifiedDiffFileName,
156 | $oldFilePhpcsOutputFileName,
157 | $newFilePhpcsOutputFileName
158 | );
159 |
160 | echo $changedMessages->toPhpcsJson();
161 | ```
162 |
163 | This will output something like:
164 |
165 | ```json
166 | {
167 | "totals": {
168 | "errors": 0,
169 | "warnings": 1,
170 | "fixable": 0
171 | },
172 | "files": {
173 | "file.php": {
174 | "errors": 0,
175 | "warnings": 1,
176 | "messages": [
177 | {
178 | "line": 20,
179 | "type": "WARNING",
180 | "severity": 5,
181 | "fixable": false,
182 | "column": 5,
183 | "source": "ImportDetection.Imports.RequireImports.Import",
184 | "message": "Found unused symbol Foobar."
185 | }
186 | ]
187 | }
188 | }
189 | }
190 | ```
191 |
192 | ### getNewPhpcsMessages
193 |
194 | If the previous function is not sufficient, this library exposes a lower-level function `PhpcsMessages\getNewPhpcsMessages()` which takes three arguments:
195 |
196 | - (string) The full unified diff of a single file.
197 | - (PhpcsMessages) The messages resulting from running phpcs on the file before your recent changes.
198 | - (PhpcsMessages) The messages resulting from running phpcs on the file after your recent changes.
199 |
200 | It will return an instance of `PhpcsMessages` which is a filtered list of the third argument above where every line that was present in the second argument has been removed.
201 |
202 | You can create an instance of `PhpcsMessages` from real phpcs JSON output by using `PhpcsMessages::fromPhpcsJson()`. The following example produces the same output as the previous one:
203 |
204 | ```php
205 | use function PhpcsChanged\getNewPhpcsMessages;
206 | use PhpcsChanged\PhpcsMessages;
207 |
208 | $changedMessages = getNewPhpcsMessages(
209 | $unifiedDiff,
210 | PhpcsMessages::fromPhpcsJson($oldFilePhpcsOutput),
211 | PhpcsMessages::fromPhpcsJson($newFilePhpcsOutput)
212 | );
213 |
214 | echo $changedMessages->toPhpcsJson();
215 | ```
216 |
217 | ### Multiple files
218 |
219 | You can combine the results of `getNewPhpcsMessages` or `getNewPhpcsMessagesFromFiles` by using `PhpcsChanged\PhpcsMessages::merge()` which takes an array of `PhpcsMessages` instances and merges them into one instance. For example:
220 |
221 | ```php
222 | use function PhpcsChanged\getNewPhpcsMessages;
223 | use function PhpcsChanged\getNewPhpcsMessagesFromFiles;
224 | use PhpcsChanged\PhpcsMessages;
225 |
226 | $changedMessagesA = getNewPhpcsMessages(
227 | $unifiedDiffA,
228 | PhpcsMessages::fromPhpcsJson($oldFilePhpcsOutputA),
229 | PhpcsMessages::fromPhpcsJson($newFilePhpcsOutputA)
230 | );
231 | $changedMessagesB = getNewPhpcsMessagesFromFiles(
232 | $unifiedDiffFileNameB,
233 | $oldFilePhpcsOutputFileNameB,
234 | $newFilePhpcsOutputFileNameB
235 | );
236 |
237 | $changedMessages = PhpcsMessages::merge([$changedMessagesA, $changedMessagesB]);
238 |
239 | echo $changedMessages->toPhpcsJson();
240 | ```
241 |
242 | ## Running Tests
243 |
244 | Run the following commands in this directory to run the built-in test suite:
245 |
246 | ```
247 | composer install
248 | composer test
249 | ```
250 |
251 | You can also run linting and static analysis:
252 |
253 | ```
254 | composer lint
255 | composer static-analysis
256 | ```
257 |
258 | ## Debugging
259 |
260 | If something isn't working the way you expect, use the `--debug` option. This will show a considerable amount of output. Pay particular attention to the CLI commands run by the script. You can run these commands manually to try to better understand the issue.
261 |
262 | ## Inspiration
263 |
264 | This was inspired by the amazing work in https://github.com/Automattic/phpcs-diff
265 |
--------------------------------------------------------------------------------
/PhpcsChanged/CliOptions.php:
--------------------------------------------------------------------------------
1 | files = $options['files'];
176 | }
177 | if (array_key_exists('no-vendor-phpcs', $options)) {
178 | $cliOptions->noVendorPhpcs = true;
179 | }
180 | if (array_key_exists('phpcs-path', $options)) {
181 | $cliOptions->phpcsPath = $options['phpcs-path'];
182 | }
183 | if (array_key_exists('git-path', $options)) {
184 | $cliOptions->gitPath = $options['git-path'];
185 | }
186 | if (array_key_exists('cat-path', $options)) {
187 | $cliOptions->catPath = $options['cat-path'];
188 | }
189 | if (array_key_exists('svn-path', $options)) {
190 | $cliOptions->svnPath = $options['svn-path'];
191 | }
192 | if (array_key_exists('svn', $options)) {
193 | $cliOptions->mode = Modes::SVN;
194 | }
195 | if (array_key_exists('git', $options)) {
196 | $cliOptions->mode = Modes::GIT_STAGED;
197 | }
198 | if (array_key_exists('git-unstaged', $options)) {
199 | $cliOptions->mode = Modes::GIT_UNSTAGED;
200 | }
201 | if (array_key_exists('git-staged', $options)) {
202 | $cliOptions->mode = Modes::GIT_STAGED;
203 | }
204 | if (array_key_exists('git-base', $options)) {
205 | $cliOptions->mode = Modes::GIT_BASE;
206 | $cliOptions->gitBase = $options['git-base'];
207 | }
208 | if (array_key_exists('report', $options)) {
209 | $cliOptions->reporter = $options['report'];
210 | }
211 | if (array_key_exists('debug', $options)) {
212 | $cliOptions->debug = true;
213 | }
214 | if (array_key_exists('clear-cache', $options)) {
215 | $cliOptions->clearCache = true;
216 | }
217 | if (array_key_exists('cache', $options)) {
218 | $cliOptions->useCache = true;
219 | }
220 | if (array_key_exists('no-cache', $options)) {
221 | $cliOptions->useCache = false;
222 | }
223 | if (array_key_exists('diff', $options)) {
224 | $cliOptions->mode = Modes::MANUAL;
225 | $cliOptions->diffFile = $options['diff'];
226 | }
227 | if (array_key_exists('phpcs-unmodified', $options)) {
228 | $cliOptions->mode = Modes::MANUAL;
229 | $cliOptions->phpcsUnmodified = $options['phpcs-unmodified'];
230 | }
231 | if (array_key_exists('phpcs-modified', $options)) {
232 | $cliOptions->mode = Modes::MANUAL;
233 | $cliOptions->phpcsModified = $options['phpcs-modified'];
234 | }
235 | if (array_key_exists('s', $options)) {
236 | $cliOptions->showMessageCodes = true;
237 | }
238 | if (array_key_exists('standard', $options)) {
239 | $cliOptions->phpcsStandard = $options['standard'];
240 | }
241 | if (array_key_exists('extensions', $options)) {
242 | $cliOptions->phpcsExtensions = $options['extensions'];
243 | }
244 | if (array_key_exists('always-exit-zero', $options)) {
245 | $cliOptions->alwaysExitZero = true;
246 | }
247 | if (array_key_exists('no-cache-git-root', $options)) {
248 | $cliOptions->noCacheGitRoot = true;
249 | }
250 | if (array_key_exists('no-verify-git-file', $options)) {
251 | $cliOptions->noVerifyGitFile = true;
252 | }
253 | if (array_key_exists('warning-severity', $options)) {
254 | $cliOptions->warningSeverity = $options['warning-severity'];
255 | }
256 | if (array_key_exists('error-severity', $options)) {
257 | $cliOptions->errorSeverity = $options['error-severity'];
258 | }
259 | if (array_key_exists('i', $options)) {
260 | $cliOptions->mode = Modes::INFO_ONLY;
261 | }
262 | $cliOptions->validate();
263 | return $cliOptions;
264 | }
265 |
266 | public function toArray(): array {
267 | $options = [];
268 | $options['report'] = $this->reporter;
269 | $options['files'] = $this->files;
270 | if (boolval($this->phpcsStandard)) {
271 | $options['standard'] = $this->phpcsStandard;
272 | }
273 | if (boolval($this->phpcsExtensions)) {
274 | $options['extensions'] = $this->phpcsExtensions;
275 | }
276 | if (boolval($this->noVendorPhpcs)) {
277 | $options['no-vendor-phpcs'] = true;
278 | }
279 | if (boolval($this->phpcsPath)) {
280 | $options['phpcs-path'] = $this->phpcsPath;
281 | }
282 | if (boolval($this->gitPath)) {
283 | $options['git-path'] = $this->gitPath;
284 | }
285 | if (boolval($this->catPath)) {
286 | $options['cat-path'] = $this->catPath;
287 | }
288 | if (boolval($this->svnPath)) {
289 | $options['svn-path'] = $this->svnPath;
290 | }
291 | if (boolval($this->debug)) {
292 | $options['debug'] = true;
293 | }
294 | if (boolval($this->showMessageCodes)) {
295 | $options['s'] = true;
296 | }
297 | if ($this->mode === Modes::SVN) {
298 | $options['svn'] = true;
299 | }
300 | if ($this->mode === Modes::GIT_STAGED) {
301 | $options['git'] = true;
302 | $options['git-staged'] = true;
303 | }
304 | if ($this->mode === Modes::GIT_UNSTAGED) {
305 | $options['git'] = true;
306 | $options['git-unstaged'] = true;
307 | }
308 | if ($this->mode === Modes::GIT_BASE) {
309 | $options['git'] = true;
310 | $options['git-base'] = $this->gitBase;
311 | }
312 | if (boolval($this->useCache)) {
313 | $options['cache'] = true;
314 | }
315 | if (! boolval($this->useCache)) {
316 | $options['no-cache'] = true;
317 | }
318 | if (boolval($this->clearCache)) {
319 | $options['clear-cache'] = true;
320 | }
321 | if ($this->mode === Modes::MANUAL) {
322 | $options['diff'] = $this->diffFile;
323 | $options['phpcs-unmodified'] = $this->phpcsUnmodified;
324 | $options['phpcs-modified'] = $this->phpcsModified;
325 | }
326 | if (boolval($this->alwaysExitZero)) {
327 | $options['always-exit-zero'] = true;
328 | }
329 | if (boolval($this->noCacheGitRoot)) {
330 | $options['no-cache-git-root'] = true;
331 | }
332 | if (boolval($this->noVerifyGitFile)) {
333 | $options['no-verify-git-file'] = true;
334 | }
335 | // Note that both warningSeverity and errorSeverity can be the string '0'
336 | // which is falsy in PHP but is a valid value here so we must be careful
337 | // when testing for it.
338 | if (is_string($this->warningSeverity) && strlen($this->warningSeverity) > 0) {
339 | $options['warning-severity'] = $this->warningSeverity;
340 | }
341 | if (is_string($this->errorSeverity) && strlen($this->errorSeverity) > 0) {
342 | $options['error-severity'] = $this->errorSeverity;
343 | }
344 | return $options;
345 | }
346 |
347 | public function isGitMode(): bool {
348 | $gitModes = [Modes::GIT_BASE, Modes::GIT_UNSTAGED, Modes::GIT_STAGED];
349 | return in_array($this->mode, $gitModes, true);
350 | }
351 |
352 | public function getExecutablePath(string $executableName): string {
353 | switch ($executableName) {
354 | case 'phpcs':
355 | if (is_string($this->phpcsPath) && strlen($this->phpcsPath) > 0) {
356 | return $this->phpcsPath;
357 | }
358 | $env = getenv('PHPCS');
359 | if (is_string($env) && strlen($env) > 0) {
360 | return $env;
361 | }
362 | return 'phpcs';
363 | case 'git':
364 | if (is_string($this->gitPath) && strlen($this->gitPath) > 0) {
365 | return $this->gitPath;
366 | }
367 | $env = getenv('GIT');
368 | if (is_string($env) && strlen($env) > 0) {
369 | return $env;
370 | }
371 | return 'git';
372 | case 'cat':
373 | if (is_string($this->catPath) && strlen($this->catPath) > 0) {
374 | return $this->catPath;
375 | }
376 | $env = getenv('CAT');
377 | if (is_string($env) && strlen($env) > 0) {
378 | return $env;
379 | }
380 | return 'cat';
381 | case 'svn':
382 | if (is_string($this->svnPath) && strlen($this->svnPath) > 0) {
383 | return $this->svnPath;
384 | }
385 | $env = getenv('SVN');
386 | if (is_string($env) && strlen($env) > 0) {
387 | return $env;
388 | }
389 | return 'svn';
390 | default:
391 | throw new \Exception("No executable found called '{$executableName}'.");
392 | }
393 | }
394 |
395 | public function validate(): void {
396 | if (! boolval($this->mode)) {
397 | throw new InvalidOptionException('You must use either automatic or manual mode.');
398 | }
399 | if ($this->mode === Modes::MANUAL) {
400 | if ( ! boolval($this->diffFile) || ! boolval($this->phpcsUnmodified) || ! boolval($this->phpcsModified)) {
401 | throw new InvalidOptionException('Manual mode requires a diff, the unmodified file phpcs output, and the modified file phpcs output.');
402 | }
403 | }
404 | if ($this->mode === Modes::GIT_BASE && ! boolval($this->gitBase)) {
405 | throw new InvalidOptionException('git-base mode requires a git object.');
406 | }
407 | if ($this->isGitMode() && ! boolval($this->files)) {
408 | throw new InvalidOptionException('You must supply at least one file or directory to run in git mode.');
409 | }
410 | if ($this->mode === Modes::SVN && ! boolval($this->files)) {
411 | throw new InvalidOptionException('You must supply at least one file or directory to run in svn mode.');
412 | }
413 | }
414 | }
415 |
--------------------------------------------------------------------------------
/tests/PhpcsChangedTest.php:
--------------------------------------------------------------------------------
1 | 20 ],
28 | [ 'line' => 99 ],
29 | [ 'line' => 108 ],
30 | [ 'line' => 111 ],
31 | [ 'line' => 114 ],
32 | ];
33 | $newFilePhpcs = [
34 | [ 'line' => 20 ],
35 | [ 'line' => 21 ],
36 | [ 'line' => 100 ],
37 | [ 'line' => 109 ],
38 | [ 'line' => 112 ],
39 | [ 'line' => 115 ],
40 | ];
41 | $actual = getNewPhpcsMessages($diff, PhpcsMessages::fromArrays($oldFilePhpcs), PhpcsMessages::fromArrays($newFilePhpcs));
42 | $expected = PhpcsMessages::fromArrays([
43 | [ 'line' => 20 ],
44 | ]);
45 | $this->assertEquals($expected->getLineNumbers(), $actual->getLineNumbers());
46 | }
47 |
48 | public function testGetNewPhpcsMessagesWithNewFile() {
49 | $diff = << 3 ],
62 | ];
63 | $actual = getNewPhpcsMessages($diff, PhpcsMessages::fromArrays($oldFilePhpcs), PhpcsMessages::fromArrays($newFilePhpcs));
64 | $expected = PhpcsMessages::fromArrays([
65 | [ 'line' => 3 ],
66 | ]);
67 | $this->assertEquals($expected->getLineNumbers(), $actual->getLineNumbers());
68 | }
69 |
70 | public function testGetNewPhpcsMessagesWithChangedLine() {
71 | $diff = << 20 ],
88 | ];
89 | $newFilePhpcs = [
90 | [ 'line' => 20 ],
91 | ];
92 | $actual = getNewPhpcsMessages($diff, PhpcsMessages::fromArrays($oldFilePhpcs), PhpcsMessages::fromArrays($newFilePhpcs));
93 | $expected = PhpcsMessages::fromArrays([
94 | [ 'line' => 20 ],
95 | ]);
96 | $this->assertEquals($expected->getLineNumbers(), $actual->getLineNumbers());
97 | }
98 |
99 | public function testGetNewPhpcsMessagesHasFileName() {
100 | $diff = << 20 ],
116 | [ 'line' => 99 ],
117 | [ 'line' => 108 ],
118 | [ 'line' => 111 ],
119 | [ 'line' => 114 ],
120 | ];
121 | $newFilePhpcs = [
122 | [ 'line' => 20 ],
123 | [ 'line' => 21 ],
124 | [ 'line' => 100 ],
125 | [ 'line' => 109 ],
126 | [ 'line' => 112 ],
127 | [ 'line' => 115 ],
128 | ];
129 | $actual = getNewPhpcsMessages(
130 | $diff,
131 | PhpcsMessages::fromArrays($oldFilePhpcs),
132 | PhpcsMessages::fromArrays($newFilePhpcs)
133 | );
134 | $this->assertEquals('bin/review-stuck-orders.php', $actual->getMessages()[0]->getFile());
135 | }
136 |
137 | public function testGetNewPhpcsMessagesFromFiles() {
138 | $actual = getNewPhpcsMessagesFromFiles(
139 | 'tests/fixtures/review-stuck-orders.diff',
140 | 'tests/fixtures/old-phpcs-output.json',
141 | 'tests/fixtures/new-phpcs-output.json'
142 | );
143 | $expected = PhpcsMessages::fromArrays([
144 | [ 'line' => 20 ],
145 | ]);
146 | $this->assertEquals($expected->getLineNumbers(), $actual->getLineNumbers());
147 | }
148 |
149 | public function testGetNewPhpcsMessagesFromFilesHasFileName() {
150 | $actual = getNewPhpcsMessagesFromFiles(
151 | 'tests/fixtures/review-stuck-orders.diff',
152 | 'tests/fixtures/old-phpcs-output.json',
153 | 'tests/fixtures/new-phpcs-output.json'
154 | );
155 | $this->assertEquals('bin/review-stuck-orders.php', $actual->getMessages()[0]->getFile());
156 | }
157 |
158 |
159 | public function testGetNewPhpcsMessagesWithPhpcsJson() {
160 | $diff = <<toPhpcsJson();
177 | $expected = '{"totals":{"errors":0,"warnings":1,"fixable":0},"files":{"bin/review-stuck-orders.php":{"errors":0,"warnings":1,"messages":[{"line":20,"type":"WARNING","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."}]}}}';
178 | $this->assertEquals($expected, $actual);
179 | }
180 |
181 | public function testGetNewPhpcsMessagesWithPhpcsJsonAndNewFile() {
182 | $diff = <<toPhpcsJson();
195 | $expected = '{"totals":{"errors":0,"warnings":2,"fixable":0},"files":{"foo.php":{"errors":0,"warnings":2,"messages":[{"line":20,"type":"WARNING","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."},{"line":21,"type":"WARNING","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."}]}}}';
196 | $this->assertEquals($expected, $actual);
197 | }
198 |
199 | public function testGetNewPhpcsMessagesWithPhpcsJsonHasFileNameIfProvided() {
200 | $diff = <<toPhpcsJson();
218 | $expected = '{"totals":{"errors":0,"warnings":1,"fixable":0},"files":{"bin/review-stuck-orders.php":{"errors":0,"warnings":1,"messages":[{"line":20,"type":"WARNING","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."}]}}}';
219 | $this->assertEquals($expected, $actual);
220 | }
221 |
222 | public function testGetNewPhpcsMessagesWithPhpcsJsonAndFilename() {
223 | $diff = <<toPhpcsJson();
240 | $expected = '{"totals":{"errors":0,"warnings":1,"fixable":0},"files":{"bin/review-stuck-orders.php":{"errors":0,"warnings":1,"messages":[{"line":20,"type":"WARNING","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."}]}}}';
241 | $this->assertEquals($expected, $actual);
242 | }
243 |
244 | public function testGetNewPhpcsMessagesWithPhpcsJsonAndErrors() {
245 | $diff = <<toPhpcsJson();
262 | $expected = '{"totals":{"errors":1,"warnings":0,"fixable":0},"files":{"bin/review-stuck-orders.php":{"errors":1,"warnings":0,"messages":[{"line":20,"type":"ERROR","severity":5,"fixable":false,"column":5,"source":"ImportDetection.Imports.RequireImports.Import","message":"Found unused symbol Emergent."}]}}}';
263 | $this->assertEquals($expected, $actual);
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/PhpcsChanged/UnixShell.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | private $fullPaths = [];
27 |
28 | /**
29 | * The output of `svn info` for each svn file keyed by filename.
30 | *
31 | * @var Array
32 | */
33 | private $svnInfo = [];
34 |
35 | public function __construct(CliOptions $options) {
36 | $this->options = $options;
37 | }
38 |
39 | #[\Override]
40 | public function clearCaches(): void {
41 | $this->fullPaths = [];
42 | $this->svnInfo = [];
43 | }
44 |
45 | #[\Override]
46 | public function validateShellIsReady(): void {
47 | if ($this->options->mode === Modes::MANUAL) {
48 | $phpcs = $this->getPhpcsExecutable();
49 | $this->validateExecutableExists('phpcs', $phpcs);
50 | }
51 |
52 | if ($this->options->mode === Modes::SVN) {
53 | $cat = $this->options->getExecutablePath('cat');
54 | $svn = $this->options->getExecutablePath('svn');
55 | $this->validateExecutableExists('svn', $svn);
56 | $this->validateExecutableExists('cat', $cat);
57 | $phpcs = $this->getPhpcsExecutable();
58 | $this->validateExecutableExists('phpcs', $phpcs);
59 | }
60 |
61 | if ($this->options->isGitMode()) {
62 | $git = $this->options->getExecutablePath('git');
63 | $this->validateExecutableExists('git', $git);
64 | $phpcs = $this->getPhpcsExecutable();
65 | $this->validateExecutableExists('phpcs', $phpcs);
66 | }
67 | }
68 |
69 | protected function validateExecutableExists(string $name, string $command): void {
70 | exec(sprintf("type %s > /dev/null 2>&1", escapeshellarg($command)), $ignore, $returnVal);
71 | if ($returnVal != 0) {
72 | throw new \Exception("Cannot find executable for {$name}, currently set to '{$command}'.");
73 | }
74 | }
75 |
76 | private function getPhpcsExecutable(): string {
77 | if (boolval($this->options->phpcsPath) || boolval(getenv('PHPCS'))) {
78 | return $this->options->getExecutablePath('phpcs');
79 | }
80 | if (! $this->options->noVendorPhpcs && $this->doesPhpcsExistInVendor()) {
81 | return $this->getVendorPhpcsPath();
82 | }
83 | return 'phpcs';
84 | }
85 |
86 | private function doesPhpcsExistInVendor(): bool {
87 | try {
88 | $this->validateExecutableExists('phpcs', $this->getVendorPhpcsPath());
89 | } catch (\Exception $err) {
90 | return false;
91 | }
92 | return true;
93 | }
94 |
95 | private function getVendorPhpcsPath(): string {
96 | return 'vendor/bin/phpcs';
97 | }
98 |
99 | protected function executeCommand(string $command, ?int &$return_val = null): string {
100 | $output = [];
101 | exec($command, $output, $return_val);
102 | return implode(PHP_EOL, $output) . PHP_EOL;
103 | }
104 |
105 | #[\Override]
106 | public function getPhpcsStandards(): string {
107 | $phpcs = $this->getPhpcsExecutable();
108 | $installedCodingStandardsPhpcsOutputCommand = "{$phpcs} -i";
109 | return $this->executeCommand($installedCodingStandardsPhpcsOutputCommand);
110 | }
111 |
112 | private function doesFileExistInGitBase(string $fileName): bool {
113 | $debug = getDebug($this->options->debug);
114 | $git = $this->options->getExecutablePath('git');
115 | $gitStatusCommand = "{$git} cat-file -e " . escapeshellarg($this->options->gitBase) . ':' . escapeshellarg($this->getFullGitPathToFile($fileName)) . ' 2>/dev/null';
116 | $debug('checking status of file with command:', $gitStatusCommand);
117 | /** @var int */
118 | $return_val = 1;
119 | $gitStatusOutput = $this->executeCommand($gitStatusCommand, $return_val);
120 | $debug('status command output:', $gitStatusOutput);
121 | $debug('status command return val:', $return_val);
122 | return 0 !== $return_val;
123 | }
124 |
125 | private function getGitStatusForFile(string $fileName): string {
126 | $debug = getDebug($this->options->debug);
127 | $git = $this->options->getExecutablePath('git');
128 | $gitStatusCommand = "{$git} status --porcelain " . escapeshellarg($fileName);
129 | $debug('checking git status of file with command:', $gitStatusCommand);
130 | $gitStatusOutput = $this->executeCommand($gitStatusCommand);
131 | $debug('git status output:', $gitStatusOutput);
132 | return $gitStatusOutput;
133 | }
134 |
135 | private function isFileStagedForAdding(string $fileName): bool {
136 | $gitStatusOutput = $this->getGitStatusForFile($fileName);
137 | // The git status will be empty for tracked, unchanged files.
138 | if (! $gitStatusOutput || false === strpos($gitStatusOutput, $fileName)) {
139 | return false;
140 | }
141 | if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') {
142 | throw new ShellException("File does not appear to be tracked by git: '{$fileName}'");
143 | }
144 | return isset($gitStatusOutput[0]) && $gitStatusOutput[0] === 'A';
145 | }
146 |
147 | #[\Override]
148 | public function doesUnmodifiedFileExistInGit(string $fileName): bool {
149 | if ($this->options->mode === Modes::GIT_BASE) {
150 | return $this->doesFileExistInGitBase($fileName);
151 | }
152 | return $this->isFileStagedForAdding($fileName);
153 | }
154 |
155 | private function getFullGitPathToFile(string $fileName): string {
156 | // Return cache if set.
157 | if (array_key_exists($fileName, $this->fullPaths)) {
158 | return $this->fullPaths[$fileName];
159 | }
160 |
161 | $debug = getDebug($this->options->debug);
162 | $git = $this->options->getExecutablePath('git');
163 |
164 | // Verify that the file exists in git before we try to get its full path.
165 | // There's never a case where we'd be scanning a modified file that is not
166 | // tracked by git (a new file must be staged because otherwise we wouldn't
167 | // know it exists).
168 | if (! $this->options->noVerifyGitFile) {
169 | $gitStatusOutput = $this->getGitStatusForFile($fileName);
170 | // The git status will be empty for tracked, unchanged files.
171 | if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') {
172 | throw new ShellException("File does not appear to be tracked by git: '{$fileName}'");
173 | }
174 | }
175 |
176 | $command = "{$git} ls-files --full-name " . escapeshellarg($fileName);
177 | $debug('getting full path to file with command:', $command);
178 | $fullPath = trim($this->executeCommand($command));
179 |
180 | // This will not change so we can cache it.
181 | $this->fullPaths[$fileName] = $fullPath;
182 | return $fullPath;
183 | }
184 |
185 | private function getModifiedFileContentsCommand(string $fileName): string {
186 | $git = $this->options->getExecutablePath('git');
187 | $cat = $this->options->getExecutablePath('cat');
188 | $fullPath = $this->getFullGitPathToFile($fileName);
189 | if ($this->options->mode === Modes::GIT_BASE) {
190 | // for git-base mode, we get the contents of the file from the HEAD version of the file in the current branch
191 | return "{$git} show HEAD:" . escapeshellarg($fullPath);
192 | }
193 | if ($this->options->mode === Modes::GIT_UNSTAGED) {
194 | // for git-unstaged mode, we get the contents of the file from the current working copy
195 | return "{$cat} " . escapeshellarg($fileName);
196 | }
197 | // default mode is git-staged, so we get the contents from the staged version of the file
198 | return "{$git} show :0:" . escapeshellarg($fullPath);
199 | }
200 |
201 | private function getUnmodifiedFileContentsCommand(string $fileName): string {
202 | $git = $this->options->getExecutablePath('git');
203 | if ($this->options->mode === Modes::GIT_BASE) {
204 | $rev = escapeshellarg($this->options->gitBase);
205 | } else if ($this->options->mode === Modes::GIT_UNSTAGED) {
206 | $rev = ':0'; // :0 in this case means "staged version or HEAD if there is no staged version"
207 | } else {
208 | // git-staged is the default
209 | $rev = 'HEAD';
210 | }
211 | $fullPath = $this->getFullGitPathToFile($fileName);
212 | return "{$git} show {$rev}:" . escapeshellarg($fullPath);
213 | }
214 |
215 | #[\Override]
216 | public function getGitHashOfModifiedFile(string $fileName): string {
217 | $debug = getDebug($this->options->debug);
218 | $git = $this->options->getExecutablePath('git');
219 | $fileContentsCommand = $this->getModifiedFileContentsCommand($fileName);
220 | $command = "{$fileContentsCommand} | {$git} hash-object --stdin";
221 | $debug('running modified file git hash command:', $command);
222 | $hash = $this->executeCommand($command);
223 | if (! $hash) {
224 | throw new ShellException("Cannot get modified file hash for file '{$fileName}'");
225 | }
226 | $debug('modified file git hash command output:', $hash);
227 | return $hash;
228 | }
229 |
230 | #[\Override]
231 | public function getGitHashOfUnmodifiedFile(string $fileName): string {
232 | $debug = getDebug($this->options->debug);
233 | $git = $this->options->getExecutablePath('git');
234 | $fileContentsCommand = $this->getUnmodifiedFileContentsCommand($fileName);
235 | $command = "{$fileContentsCommand} | {$git} hash-object --stdin";
236 | $debug('running unmodified file git hash command:', $command);
237 | $hash = $this->executeCommand($command);
238 | if (! $hash) {
239 | throw new ShellException("Cannot get unmodified file hash for file '{$fileName}'");
240 | }
241 | $debug('unmodified file git hash command output:', $hash);
242 | return $hash;
243 | }
244 |
245 | private function getPhpcsStandardOption(): string {
246 | $phpcsStandard = $this->options->phpcsStandard ?? '';
247 | $phpcsStandardOption = strlen($phpcsStandard) > 0 ? ' --standard=' . escapeshellarg($phpcsStandard) : '';
248 | $warningSeverity = $this->options->warningSeverity ?? '';
249 | $phpcsStandardOption .= strlen($warningSeverity) > 0 ? ' --warning-severity=' . escapeshellarg($warningSeverity) : '';
250 | $errorSeverity = $this->options->errorSeverity ?? '';
251 | $phpcsStandardOption .= strlen($errorSeverity) > 0 ? ' --error-severity=' . escapeshellarg($errorSeverity) : '';
252 | return $phpcsStandardOption;
253 | }
254 |
255 | private function getPhpcsExtensionsOption(): string {
256 | $phpcsExtensions = $this->options->phpcsExtensions ?? '';
257 | $phpcsExtensionsOption = strlen($phpcsExtensions) > 0 ? ' --extensions=' . escapeshellarg($phpcsExtensions) : '';
258 | return $phpcsExtensionsOption;
259 | }
260 |
261 | #[\Override]
262 | public function getPhpcsOutputOfModifiedGitFile(string $fileName): string {
263 | $debug = getDebug($this->options->debug);
264 | $fileContentsCommand = $this->getModifiedFileContentsCommand($fileName);
265 | $modifiedFilePhpcsOutputCommand = "{$fileContentsCommand} | " . $this->getPhpcsCommand($fileName);
266 | $debug('running modified file phpcs command:', $modifiedFilePhpcsOutputCommand);
267 | $modifiedFilePhpcsOutput = $this->executeCommand($modifiedFilePhpcsOutputCommand);
268 | return $this->processPhpcsOutput($fileName, 'modified', $modifiedFilePhpcsOutput);
269 | }
270 |
271 | #[\Override]
272 | public function getPhpcsOutputOfUnmodifiedGitFile(string $fileName): string {
273 | $debug = getDebug($this->options->debug);
274 | $unmodifiedFileContentsCommand = $this->getUnmodifiedFileContentsCommand($fileName);
275 | $unmodifiedFilePhpcsOutputCommand = "{$unmodifiedFileContentsCommand} | " . $this->getPhpcsCommand($fileName);
276 | $debug('running unmodified file phpcs command:', $unmodifiedFilePhpcsOutputCommand);
277 | $unmodifiedFilePhpcsOutput = $this->executeCommand($unmodifiedFilePhpcsOutputCommand);
278 | return $this->processPhpcsOutput($fileName, 'unmodified', $unmodifiedFilePhpcsOutput);
279 | }
280 |
281 | #[\Override]
282 | public function getGitUnifiedDiff(string $fileName): string {
283 | $debug = getDebug($this->options->debug);
284 | $git = $this->options->getExecutablePath('git');
285 | $objectOption = $this->options->mode === Modes::GIT_BASE ? ' ' . escapeshellarg($this->options->gitBase) . '...' : '';
286 | $stagedOption = ! boolval($objectOption) && $this->options->mode !== Modes::GIT_UNSTAGED ? ' --staged' : '';
287 | $unifiedDiffCommand = "{$git} diff{$stagedOption}{$objectOption} --no-prefix " . escapeshellarg($fileName);
288 | $debug('running diff command:', $unifiedDiffCommand);
289 | $unifiedDiff = $this->executeCommand($unifiedDiffCommand);
290 | if (! $unifiedDiff) {
291 | throw new NoChangesException("Cannot get git diff for file '{$fileName}'; skipping");
292 | }
293 | $debug('diff command output:', $unifiedDiff);
294 | return $unifiedDiff;
295 | }
296 |
297 | #[\Override]
298 | public function getGitMergeBase(): string {
299 | if ($this->options->mode !== Modes::GIT_BASE) {
300 | return '';
301 | }
302 | $debug = getDebug($this->options->debug);
303 | $git = $this->options->getExecutablePath('git');
304 | $mergeBaseCommand = "{$git} merge-base " . escapeshellarg($this->options->gitBase) . ' HEAD';
305 | $debug('running merge-base command:', $mergeBaseCommand);
306 | $mergeBase = $this->executeCommand($mergeBaseCommand);
307 | if (! $mergeBase) {
308 | $debug('merge-base command produced no output');
309 | return $this->options->gitBase;
310 | }
311 | $debug('merge-base command output:', $mergeBase);
312 | return trim($mergeBase);
313 | }
314 |
315 | #[\Override]
316 | public function getPhpcsOutputOfModifiedSvnFile(string $fileName): string {
317 | $debug = getDebug($this->options->debug);
318 | $cat = $this->options->getExecutablePath('cat');
319 | $modifiedFilePhpcsOutputCommand = "{$cat} " . escapeshellarg($fileName) . ' | ' . $this->getPhpcsCommand($fileName);
320 | $debug('running modified file phpcs command:', $modifiedFilePhpcsOutputCommand);
321 | $modifiedFilePhpcsOutput = $this->executeCommand($modifiedFilePhpcsOutputCommand);
322 | return $this->processPhpcsOutput($fileName, 'modified', $modifiedFilePhpcsOutput);
323 | }
324 |
325 | #[\Override]
326 | public function getPhpcsOutputOfUnmodifiedSvnFile(string $fileName): string {
327 | $debug = getDebug($this->options->debug);
328 | $svn = $this->options->getExecutablePath('svn');
329 | $unmodifiedFilePhpcsOutputCommand = "{$svn} cat " . escapeshellarg($fileName) . " | " . $this->getPhpcsCommand($fileName);
330 | $debug('running unmodified file phpcs command:', $unmodifiedFilePhpcsOutputCommand);
331 | $unmodifiedFilePhpcsOutput = $this->executeCommand($unmodifiedFilePhpcsOutputCommand);
332 | return $this->processPhpcsOutput($fileName, 'unmodified', $unmodifiedFilePhpcsOutput);
333 | }
334 |
335 | private function getPhpcsCommand(string $fileName): string {
336 | $phpcs = $this->getPhpcsExecutable();
337 | return "{$phpcs} --report=json -q" . $this->getPhpcsStandardOption() . $this->getPhpcsExtensionsOption() . ' --stdin-path=' . escapeshellarg($fileName) . ' -';
338 | }
339 |
340 | private function processPhpcsOutput(string $fileName, string $modifiedOrUnmodified, string $phpcsOutput): string {
341 | $debug = getDebug($this->options->debug);
342 | if (! $phpcsOutput) {
343 | throw new ShellException("Cannot get {$modifiedOrUnmodified} file phpcs output for file '{$fileName}'");
344 | }
345 | $debug("{$modifiedOrUnmodified} file phpcs command output:", $phpcsOutput);
346 | if (false !== strpos($phpcsOutput, 'You must supply at least one file or directory to process')) {
347 | $debug("phpcs output implies {$modifiedOrUnmodified} file is empty");
348 | return '';
349 | }
350 | return $phpcsOutput;
351 | }
352 |
353 | #[\Override]
354 | public function doesUnmodifiedFileExistInSvn(string $fileName): bool {
355 | $svnFileInfo = $this->getSvnFileInfo($fileName);
356 | return (false !== strpos($svnFileInfo, 'Schedule: add'));
357 | }
358 |
359 | #[\Override]
360 | public function getSvnRevisionId(string $fileName): string {
361 | $svnFileInfo = $this->getSvnFileInfo($fileName);
362 | preg_match('/\bLast Changed Rev:\s([^\n]+)/', $svnFileInfo, $matches);
363 | // New files will not have a revision
364 | return $matches[1] ?? '';
365 | }
366 |
367 | private function getSvnFileInfo(string $fileName): string {
368 | // Return cache if set.
369 | if (array_key_exists($fileName, $this->svnInfo)) {
370 | return $this->svnInfo[$fileName];
371 | }
372 | $debug = getDebug($this->options->debug);
373 | $svn = $this->options->getExecutablePath('svn');
374 | $svnStatusCommand = "{$svn} info " . escapeshellarg($fileName);
375 | $debug('checking svn status of file with command:', $svnStatusCommand);
376 | $svnStatusOutput = $this->executeCommand($svnStatusCommand);
377 | $debug('svn status output:', $svnStatusOutput);
378 | if (! $svnStatusOutput || false === strpos($svnStatusOutput, 'Schedule:')) {
379 | throw new ShellException("Cannot get svn info for file '{$fileName}'");
380 | }
381 | // This will not change within a run so we can cache it.
382 | $this->svnInfo[$fileName] = $svnStatusOutput;
383 | return $svnStatusOutput;
384 | }
385 |
386 | #[\Override]
387 | public function getSvnUnifiedDiff(string $fileName): string {
388 | $debug = getDebug($this->options->debug);
389 | $svn = $this->options->getExecutablePath('svn');
390 | $unifiedDiffCommand = "{$svn} diff " . escapeshellarg($fileName);
391 | $debug('running diff command:', $unifiedDiffCommand);
392 | $unifiedDiff = $this->executeCommand($unifiedDiffCommand);
393 | if (! $unifiedDiff) {
394 | throw new NoChangesException("Cannot get svn diff for file '{$fileName}'; skipping");
395 | }
396 | $debug('diff command output:', $unifiedDiff);
397 | return $unifiedDiff;
398 | }
399 |
400 | #[\Override]
401 | public function isReadable(string $fileName): bool {
402 | return is_readable($fileName);
403 | }
404 |
405 | #[\Override]
406 | public function getFileHash(string $fileName): string {
407 | $result = md5_file($fileName);
408 | if ($result === false) {
409 | throw new \Exception("Cannot get hash for file '{$fileName}'.");
410 | }
411 | return $result;
412 | }
413 |
414 | #[\Override]
415 | public function exitWithCode(int $code): void {
416 | exit($code);
417 | }
418 |
419 | #[\Override]
420 | public function printError(string $message): void {
421 | printError($message);
422 | }
423 |
424 | #[\Override]
425 | public function getFileNameFromPath(string $path): string {
426 | $parts = explode('/', $path);
427 | return end($parts);
428 | }
429 |
430 | #[\Override]
431 | public function getPhpcsVersion(): string {
432 | $phpcs = $this->getPhpcsExecutable();
433 |
434 | $versionPhpcsOutputCommand = "{$phpcs} --version";
435 | $versionPhpcsOutput = $this->executeCommand($versionPhpcsOutputCommand);
436 | if (! $versionPhpcsOutput) {
437 | throw new ShellException("Cannot get phpcs version");
438 | }
439 |
440 | $matched = preg_match('/version\\s([0-9.]+)/uim', $versionPhpcsOutput, $matches);
441 | if (
442 | $matched === false
443 | || count($matches) < 2
444 | || strlen($matches[1]) < 1
445 | ) {
446 | throw new ShellException("Cannot parse phpcs version output");
447 | }
448 |
449 | return $matches[1];
450 | }
451 | }
452 |
--------------------------------------------------------------------------------