Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/csqa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ jobs:
coverage: none
tools: cs2pr

# Using PHPCS `master` as an early detection system for bugs upstream.
# Using PHPCS `3.x-dev` as an early detection system for bugs upstream.
- name: 'Composer: adjust dependencies'
run: composer require --no-update squizlabs/php_codesniffer:"dev-master"
run: composer require --no-update squizlabs/php_codesniffer:"3.x-dev"

# Install dependencies and handle caching in one go.
# @link https://github.com/marketplace/actions/install-composer-dependencies
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,24 @@ jobs:
# - PHP 8.0 needs PHPCS 3.5.7+ to run without errors.
# - PHP 8.1 needs PHPCS 3.6.1+ to run without errors.
php: ['7.1', '7.2', '7.3']
phpcs_version: ['3.5.6', 'dev-master']
phpcs_version: ['3.5.6', '3.x-dev']

include:
# Make the matrix complete without duplicating builds run in code coverage.
- php: '8.1'
phpcs_version: '3.6.1'

- php: '8.0'
phpcs_version: 'dev-master'
phpcs_version: '3.x-dev'
- php: '8.0'
phpcs_version: '3.5.7'

- php: '7.4'
phpcs_version: 'dev-master'
phpcs_version: '3.x-dev'

# Experimental builds.
- php: '8.2' # Nightly.
phpcs_version: 'dev-master'
phpcs_version: '3.x-dev'

name: "Test: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }}"

Expand All @@ -62,7 +62,7 @@ jobs:
run: |
# On stable PHPCS versions, allow for PHP deprecation notices.
# Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore.
if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then
if [ "${{ matrix.phpcs_version }}" != "3.x-dev" ]; then
echo '::set-output name=PHP_INI::error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On, zend.assertions=1'
else
echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On, zend.assertions=1'
Expand Down
1 change: 1 addition & 0 deletions PhpcsChanged/CacheEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class CacheEntry implements \JsonSerializable {
*/
public $data;

#[\Override]
public function jsonSerialize(): array {
return [
'path' => $this->path,
Expand Down
1 change: 1 addition & 0 deletions PhpcsChanged/CacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(CacheInterface $cache, ?callable $debug = null) {
$this->cache = $cache;
$noopDebug =
/** @param string[] $output */
/** @psalm-suppress UnusedClosureParam, MissingClosureParamType */
function(...$output): void {}; // phpcs:ignore VariableAnalysis
$this->debug = $debug ?? $noopDebug;
}
Expand Down
23 changes: 22 additions & 1 deletion PhpcsChanged/Cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpcsChanged\Reporter;
use PhpcsChanged\JsonReporter;
use PhpcsChanged\FullReporter;
use PhpcsChanged\JunitReporter;
use PhpcsChanged\PhpcsMessages;
use PhpcsChanged\ShellException;
use PhpcsChanged\ShellOperator;
Expand Down Expand Up @@ -143,7 +144,7 @@ function printHelp(): void {
printTwoColumns([
'--standard <STANDARD>' => 'The phpcs standard to use.',
'--extensions <EXTENSIONS>' => 'A comma separated list of extensions to check.',
'--report <REPORTER>' => 'The phpcs reporter to use. One of "full" (default), "json", or "xml".',
'--report <REPORTER>' => 'The phpcs reporter to use. One of "full" (default), "json", "xml", or "junit".',
'-s' => 'Show sniff codes for each error when the reporter is "full".',
'--ignore <PATTERNS>' => 'A comma separated list of patterns to ignore files and directories.',
'--warning-severity' => 'The phpcs warning severity to report. See phpcs documentation for usage.',
Expand Down Expand Up @@ -191,6 +192,8 @@ function getReporter(string $reportType, CliOptions $options, ShellOperator $she
return new JsonReporter();
case 'xml':
return new XmlReporter($options, $shell);
case 'junit':
return new JunitReporter();
}
printErrorAndExit("Unknown Reporter '{$reportType}'");
throw new \Exception("Unknown Reporter '{$reportType}'"); // Just in case we don't exit for some reason.
Expand Down Expand Up @@ -252,14 +255,18 @@ function runSvnWorkflowForFile(string $svnFile, CliOptions $options, ShellOperat
$modifiedFilePhpcsOutput = $cache->getCacheForFile($svnFile, 'new', $modifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '');
$debug(($modifiedFilePhpcsOutput ? 'Using' : 'Not using') . " cache for modified file '{$svnFile}' at hash '{$modifiedFileHash}', and standard '{$phpcsStandard}'");
}
$modifiedFileTiming = 0.0;
if (! $modifiedFilePhpcsOutput) {
$modifiedFileStartTime = microtime(true);
$modifiedFilePhpcsOutput = $shell->getPhpcsOutputOfModifiedSvnFile($svnFile);
$modifiedFileTiming = microtime(true) - $modifiedFileStartTime;
if (isCachingEnabled($options->toArray())) {
$cache->setCacheForFile($svnFile, 'new', $modifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '', $modifiedFilePhpcsOutput);
}
}

$modifiedFilePhpcsMessages = PhpcsMessages::fromPhpcsJson($modifiedFilePhpcsOutput, $fileName);
$modifiedFilePhpcsMessages->setTiming($fileName, $modifiedFileTiming);
$hasNewPhpcsMessages = count($modifiedFilePhpcsMessages->getMessages()) > 0;

if (! $hasNewPhpcsMessages) {
Expand All @@ -279,12 +286,17 @@ function runSvnWorkflowForFile(string $svnFile, CliOptions $options, ShellOperat
$unmodifiedFilePhpcsOutput = $cache->getCacheForFile($svnFile, 'old', $revisionId, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '');
$debug(($unmodifiedFilePhpcsOutput ? 'Using' : 'Not using') . " cache for unmodified file '{$svnFile}' at revision '{$revisionId}', and standard '{$phpcsStandard}'");
}
$unmodifiedFileTiming = 0.0;
if (! $unmodifiedFilePhpcsOutput) {
$unmodifiedFileStartTime = microtime(true);
$unmodifiedFilePhpcsOutput = $shell->getPhpcsOutputOfUnmodifiedSvnFile($svnFile);
$unmodifiedFileTiming = microtime(true) - $unmodifiedFileStartTime;
if (isCachingEnabled($options->toArray())) {
$cache->setCacheForFile($svnFile, 'old', $revisionId, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '', $unmodifiedFilePhpcsOutput);
}
}
// Add timing for the unmodified scan (accumulated with modified scan time)
$modifiedFileTiming += $unmodifiedFileTiming;
}
} catch( NoChangesException $err ) {
$debug($err->getMessage());
Expand Down Expand Up @@ -348,14 +360,18 @@ function runGitWorkflowForFile(string $gitFile, CliOptions $options, ShellOperat
$modifiedFilePhpcsOutput = $cache->getCacheForFile($gitFile, 'new', $modifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '');
$debug(($modifiedFilePhpcsOutput ? 'Using' : 'Not using') . " cache for modified file '{$gitFile}' at hash '{$modifiedFileHash}', and standard '{$phpcsStandard}'");
}
$modifiedFileTiming = 0.0;
if (! $modifiedFilePhpcsOutput) {
$modifiedFileStartTime = microtime(true);
$modifiedFilePhpcsOutput = $shell->getPhpcsOutputOfModifiedGitFile($gitFile);
$modifiedFileTiming = microtime(true) - $modifiedFileStartTime;
if (isCachingEnabled($options->toArray())) {
$cache->setCacheForFile($gitFile, 'new', $modifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '', $modifiedFilePhpcsOutput);
}
}

$modifiedFilePhpcsMessages = PhpcsMessages::fromPhpcsJson($modifiedFilePhpcsOutput, $gitFile);
$modifiedFilePhpcsMessages->setTiming($gitFile, $modifiedFileTiming);
$hasNewPhpcsMessages = count($modifiedFilePhpcsMessages->getMessages()) > 0;

$unifiedDiff = '';
Expand All @@ -378,12 +394,17 @@ function runGitWorkflowForFile(string $gitFile, CliOptions $options, ShellOperat
$unmodifiedFilePhpcsOutput = $cache->getCacheForFile($gitFile, 'old', $unmodifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '');
$debug(($unmodifiedFilePhpcsOutput ? 'Using' : 'Not using') . " cache for unmodified file '{$gitFile}' at hash '{$unmodifiedFileHash}', and standard '{$phpcsStandard}'");
}
$unmodifiedFileTiming = 0.0;
if (! $unmodifiedFilePhpcsOutput) {
$unmodifiedFileStartTime = microtime(true);
$unmodifiedFilePhpcsOutput = $shell->getPhpcsOutputOfUnmodifiedGitFile($gitFile);
$unmodifiedFileTiming = microtime(true) - $unmodifiedFileStartTime;
if (isCachingEnabled($options->toArray())) {
$cache->setCacheForFile($gitFile, 'old', $unmodifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '', $unmodifiedFilePhpcsOutput);
}
}
// Add timing for the unmodified scan (accumulated with modified scan time)
$modifiedFileTiming += $unmodifiedFileTiming;
}
} catch( NoChangesException $err ) {
$debug($err->getMessage());
Expand Down
8 changes: 7 additions & 1 deletion PhpcsChanged/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class FileCache implements CacheInterface {
*/
public $cacheFilePath = DEFAULT_CACHE_FILE;

#[\Override]
public function load(): CacheObject {
if (! file_exists($this->cacheFilePath)) {
return new CacheObject();
Expand All @@ -39,12 +40,17 @@ public function load(): CacheObject {
return $cacheObject;
}

#[\Override]
public function save(CacheObject $cacheObject): void {
$data = [
'cacheVersion' => $cacheObject->cacheVersion,
'entries' => $cacheObject->entries,
];
$result = file_put_contents($this->cacheFilePath, json_encode($data));
$encodedData = json_encode($data);
if ($encodedData === false) {
throw new \Exception('Failed to write cache file; encoding failed');
}
$result = file_put_contents($this->cacheFilePath, $encodedData);
if ($result === false) {
throw new \Exception('Failed to write cache file');
}
Expand Down
2 changes: 2 additions & 0 deletions PhpcsChanged/FullReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use function PhpcsChanged\getLongestString;

class FullReporter implements Reporter {
#[\Override]
public function getFormattedMessages(PhpcsMessages $messages, array $options): string {
$files = array_unique(array_map(function(LintMessage $message): string {
return $message->getFile() ?? 'STDIN';
Expand Down Expand Up @@ -68,6 +69,7 @@ private function getFormattedMessagesForFile(array $messages, string $file, arra
EOF;
}

#[\Override]
public function getExitCode(PhpcsMessages $messages): int {
return (count($messages->getMessages()) > 0) ? 1 : 0;
}
Expand Down
7 changes: 3 additions & 4 deletions PhpcsChanged/JsonReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PhpcsChanged\LintMessage;

class JsonReporter implements Reporter {
#[\Override]
public function getFormattedMessages(PhpcsMessages $messages, array $options): string { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$files = array_unique(array_map(function(LintMessage $message): string {
return $message->getFile() ?? 'STDIN';
Expand All @@ -30,9 +31,6 @@ public function getFormattedMessages(PhpcsMessages $messages, array $options): s
$warnings = array_values(array_filter($messages->getMessages(), function($message) {
return $message->getType() === 'WARNING';
}));
$messages = array_map(function($message) {
return PhpcsMessagesHelpers::messageToPhpcsArray($message);
}, $messages->getMessages());
$dataForJson = [
'totals' => [
'errors' => count($errors),
Expand All @@ -42,7 +40,7 @@ public function getFormattedMessages(PhpcsMessages $messages, array $options): s
'files' => array_merge([], ...$outputByFile),
];
$output = json_encode($dataForJson, JSON_UNESCAPED_SLASHES);
if (! boolval($output)) {
if ($output === false) {
throw new \Exception('Failed to JSON-encode result messages');
}
return $output;
Expand All @@ -68,6 +66,7 @@ private function getFormattedMessagesForFile(array $messages, string $file): arr
return $dataForJson;
}

#[\Override]
public function getExitCode(PhpcsMessages $messages): int {
return (count($messages->getMessages()) > 0) ? 1 : 0;
}
Expand Down
95 changes: 95 additions & 0 deletions PhpcsChanged/JunitReporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);

namespace PhpcsChanged;

use PhpcsChanged\Reporter;
use PhpcsChanged\PhpcsMessages;
use PhpcsChanged\LintMessage;

class JunitReporter implements Reporter {
#[\Override]
public function getFormattedMessages(PhpcsMessages $messages, array $options): string { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$files = array_unique(array_map(function(LintMessage $message): string {
return $message->getFile() ?? 'STDIN';
}, $messages->getMessages()));
if (count($files) === 0) {
$files = ['STDIN'];
}

$totalTests = count($messages->getMessages());
$totalFailures = count(array_filter($messages->getMessages(), function(LintMessage $message): bool {
return $message->getType() === 'WARNING';
}));
$totalErrors = count(array_filter($messages->getMessages(), function(LintMessage $message): bool {
return $message->getType() === 'ERROR';
}));

// Calculate total time from all files
$totalTime = array_sum($messages->getAllTiming());

$outputByFile = array_reduce($files, function(string $output, string $file) use ($messages): string {
$messagesForFile = array_values(array_filter($messages->getMessages(), static function(LintMessage $message) use ($file): bool {
return ($message->getFile() ?? 'STDIN') === $file;
}));
$output .= $this->getFormattedMessagesForFile($messagesForFile, $file, $messages);
return $output;
}, '');

$output = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$output .= sprintf("<testsuites tests=\"%d\" failures=\"%d\" errors=\"%d\" time=\"%.3f\">\n", $totalTests, $totalFailures, $totalErrors, $totalTime);
$output .= $outputByFile;
$output .= "</testsuites>\n";

return $output;
}

private function getFormattedMessagesForFile(array $messages, string $file, PhpcsMessages $allMessages): string {
$testCount = count($messages);
$errorCount = count(array_values(array_filter($messages, function(LintMessage $message) {
return $message->getType() === 'ERROR';
})));
$failureCount = count(array_values(array_filter($messages, function(LintMessage $message) {
return $message->getType() === 'WARNING';
})));

// Get timing for this specific file
$fileTime = $allMessages->getTiming($file);

$xmlOutputForFile = sprintf("\t<testsuite name=\"%s\" tests=\"%d\" failures=\"%d\" errors=\"%d\" time=\"%.3f\">\n",
$file, $testCount, $failureCount, $errorCount, $fileTime);
$xmlOutputForFile .= array_reduce($messages, function(string $output, LintMessage $message): string {
$line = $message->getLineNumber();
$column = $message->getColumn();
$source = $this->escapeXml($message->getSource());
$messageText = $this->escapeXml($message->getMessage());
$type = $message->getType();
$severity = $message->getSeverity();

// Create a unique test case name using line:column and source
$testCaseName = "line {$line}, column {$column}";
$output .= "\t\t<testcase name=\"{$testCaseName}\" classname=\"{$source}\" time=\"0\">\n";

if ($type === 'ERROR') {
$output .= "\t\t\t<error type=\"{$source}\" message=\"{$messageText}\">Line {$line}, Column {$column}: {$messageText} (Severity: {$severity})</error>\n";
} else {
$output .= "\t\t\t<failure type=\"{$source}\" message=\"{$messageText}\">Line {$line}, Column {$column}: {$messageText} (Severity: {$severity})</failure>\n";
}

$output .= "\t\t</testcase>\n";
return $output;
}, '');
$xmlOutputForFile .= "\t</testsuite>\n";

return $xmlOutputForFile;
}

private function escapeXml(string $string): string {
return htmlspecialchars($string, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}

#[\Override]
public function getExitCode(PhpcsMessages $messages): int {
return (count($messages->getMessages()) > 0) ? 1 : 0;
}
}
15 changes: 15 additions & 0 deletions PhpcsChanged/LintMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,24 @@
namespace PhpcsChanged;

class LintMessage {
/**
* @var int Line number where the message occurs
*/
private $line;

/**
* @var string|null File path where the message occurs
*/
private $file;

/**
* @var string Message type (e.g., 'ERROR', 'WARNING')
*/
private $type;

/**
* @var array<string, mixed> Additional message properties (message, source, column, severity, fixable, etc.)
*/
private $otherProperties;

public function __construct(int $line, ?string $file, string $type, array $otherProperties) {
Expand Down
Loading
Loading