contrib: add parallel-lint 1.3.1

Avoid pulling in composer.  Looks easy enough to manually load classes.
This commit is contained in:
Franco Fichtner 2021-10-05 07:57:35 +02:00
parent cff444c9df
commit 062d51889e
22 changed files with 3191 additions and 0 deletions

View File

@ -0,0 +1,111 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
[Unreleased]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/compare/v1.3.0...HEAD
## [1.3.1] - 2021-08-13
### Added
- Extend by the Code Climate output format [#50] from [@lukas9393].
### Fixed
- PHP 8.1: silence the deprecation notices about missing return types [#64] from [@jrfnl].
### Internal
- Reformat changelog to use reflinks in changelog entries [#58] from [@glensc].
[1.3.1]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/compare/v1.3.0...1.3.1
[#50]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/50
[#58]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/58
[#64]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/64
## [1.3.0] - 2021-04-07
### Added
- Allow for multi-part file extensions to be passed using -e (like `-e php,php.dist`) from [@jrfnl].
- Added syntax error callback [#30] from [@arxeiss].
- Ignore PHP startup errors [#34] from [@jrfnl].
- Restore php 5.3 support [#51] from [@glensc].
### Fixed
- Determine skip lint process failure by status code instead of stderr content [#48] from [@jankonas].
### Changed
- Improve wording in the readme [#52] from [@glensc].
### Internal
- Normalized composer.json from [@OndraM].
- Updated PHPCS dependency from [@jrfnl].
- Cleaned coding style from [@jrfnl].
- Provide one true way to run the test suite [#37] from [@mfn].
- Travis: add build against PHP 8.0 and fix failing test [#41] from [@jrfnl].
- GitHub Actions for testing, and automatic phar creation [#46] from [@roelofr].
- Add .github folder to .gitattributes export-ignore [#54] from [@glensc].
- Suggest to curl composer install via HTTPS [#53] from [@reedy].
- GH Actions: allow for manually triggering a workflow [#55] from [@jrfnl].
- GH Actions: fix phar creation [#55] from [@jrfnl].
- GH Actions: run the tests against all supported PHP versions [#55] from [@jrfnl].
- GH Actions: report CS violations in the PR [#55] from [@jrfnl].
[1.3.0]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/compare/v1.2.0...v1.3.0
[#30]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/30
[#34]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/34
[#37]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/37
[#41]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/41
[#46]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/46
[#48]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/48
[#51]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/51
[#52]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/52
[#53]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/53
[#54]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/54
[#55]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/pull/55
## [1.2.0] - 2020-04-04
### Added
- Added changelog.
### Fixed
- Fixed vendor location for running from other folder from [@Erkens].
### Internal
- Added a .gitattributes file from [@jrfnl], thanks for issue to [@ondrejmirtes].
- Fixed incorrect unit tests from [@jrfnl].
- Fixed minor grammatical errors from [@jrfnl].
- Added Travis: test against nightly (= PHP 8) from [@jrfnl].
- Travis: removed sudo from [@jrfnl].
- Added info about installing like not a dependency.
- Cleaned readme - new organization from previous package.
- Added checklist for new version from [@szepeviktor].
[1.2.0]: https://github.com/php-parallel-lint/PHP-Parallel-Lint/compare/v1.1.0...v1.2.0
[@Erkens]: https://github.com/Erkens
[@OndraM]: https://github.com/OndraM
[@arxeiss]: https://github.com/arxeiss
[@glensc]: https://github.com/glensc
[@jankonas]: https://github.com/jankonas
[@jrfnl]: https://github.com/jrfnl
[@mfn]: https://github.com/mfn
[@ondrejmirtes]: https://github.com/ondrejmirtes
[@reedy]: https://github.com/reedy
[@roelofr]: https://github.com/roelofr
[@szepeviktor]: https://github.com/szepeviktor
[@lukas9393]: https://github.com/lukas9393

View File

@ -0,0 +1,26 @@
Copyright (c) 2012, Jakub Onderka
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.

View File

@ -0,0 +1,112 @@
# PHP Parallel Lint
[![Downloads this Month](https://img.shields.io/packagist/dm/php-parallel-lint/php-parallel-lint.svg)](https://packagist.org/packages/php-parallel-lint/php-parallel-lint)
[![Build Status](https://github.com/php-parallel-lint/PHP-Parallel-Lint/actions/workflows/test.yml/badge.svg)](https://github.com/php-parallel-lint/PHP-Parallel-Lint/actions/workflows/test.yml)
[![License](https://poser.pugx.org/php-parallel-lint/php-parallel-lint/license.svg)](https://packagist.org/packages/php-parallel-lint/php-parallel-lint)
This application checks syntax of PHP files in parallel.
It can output in plain text, colored text, json and checksyntax formats.
Additionally `blame` can be used to show commits that introduced the breakage.
Running parallel jobs in PHP is inspired by Nette framework tests.
The application is officially supported for use with PHP 5.3 to 8.0.
## Table of contents
1. [Installation](#installation)
2. [Example output](#example-output)
3. [History](#history)
4. [Command line options](#command-line-options)
5. [Recommended excludes for Symfony framework](#recommended-excludes-for-symfony-framework)
6. [Create Phar package](#create-phar-package)
7. [How to upgrade](#how-to-upgrade)
## Installation
Install with `composer` as development dependency:
composer require --dev php-parallel-lint/php-parallel-lint
Alternatively you can install as a standalone `composer` project:
composer create-project php-parallel-lint/php-parallel-lint /path/to/folder/php-parallel-lint
/path/to/folder/php-parallel-lint/parallel-lint # running tool
For colored output, install the suggested package `php-parallel-lint/php-console-highlighter`:
composer require --dev php-parallel-lint/php-console-highlighter
## Example output
![Example use of tool with error](/tests/examples/example-images/use-error.png?raw=true "Example use of tool with error")
## History
This project was originally created by [@JakubOnderka] and released as
[jakub-onderka/php-parallel-lint].
Since then, Jakub has moved on to other interests and as of January 2020, the
second most active maintainer [@grogy] has taken over maintenance of the project
and given the project - and related dependencies - a new home in the PHP
Parallel Lint organisation.
It is strongly recommended for existing users of the (unmaintained)
[jakub-onderka/php-parallel-lint] package to switch their dependency to
[php-parallel-lint/php-parallel-lint], see [How to upgrade](#how-to-upgrade) below.
[php-parallel-lint/php-parallel-lint]: https://github.com/php-parallel-lint/PHP-Parallel-Lint
[grogy/php-parallel-lint]: https://github.com/grogy/PHP-Parallel-Lint
[jakub-onderka/php-parallel-lint]: https://github.com/JakubOnderka/PHP-Parallel-Lint
[@JakubOnderka]: https://github.com/JakubOnderka
[@grogy]: https://github.com/grogy
## Command line options
- `-p <php>` Specify PHP-CGI executable to run (default: 'php').
- `-s, --short` Set short_open_tag to On (default: Off).
- `-a, --asp` Set asp_tags to On (default: Off).
- `-e <ext>` Check only files with selected extensions separated by comma. (default: php,php3,php4,php5,phtml,phpt)
- `--exclude` Exclude a file or directory. If you want exclude multiple items, use multiple exclude parameters.
- `-j <num>` Run <num> jobs in parallel (default: 10).
- `--colors` Force enable colors in console output.
- `--no-colors` Disable colors in console output.
- `--no-progress` Disable progress in console output.
- `--checkstyle` Output results as Checkstyle XML.
- `--json` Output results as JSON string (requires PHP 5.4).
- `--gitlab` Output results for the GitLab Code Quality widget (requires PHP 5.4), see more in [Code Quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) documentation.
- `--blame` Try to show git blame for row with error.
- `--git <git>` Path to Git executable to show blame message (default: 'git').
- `--stdin` Load files and folder to test from standard input.
- `--ignore-fails` Ignore failed tests.
- `--syntax-error-callback` File with syntax error callback for ability to modify error, see more in [example](doc/syntax-error-callback.md)
- `-h, --help` Print this help.
- `-V, --version` Display this application version.
## Recommended excludes for Symfony framework
To run from the command line:
vendor/bin/parallel-lint --exclude app --exclude vendor .
## Create Phar package
PHP Parallel Lint supports [Box app](https://box-project.github.io/box2/) for creating Phar package. First, install box app:
curl -LSs https://box-project.github.io/box2/installer.php | php
then run the build command in parallel lint folder, which creates `parallel-lint.phar` file.
box build
## How to upgrade
Are you using `jakub-onderka/php-parallel-lint` package? You can switch to `php-parallel-lint/php-parallel-lint` using:
composer remove --dev jakub-onderka/php-parallel-lint
composer require --dev php-parallel-lint/php-parallel-lint

View File

@ -0,0 +1,19 @@
<?php
$stdin = fopen('php://stdin', 'r');
$input = stream_get_contents($stdin);
fclose($stdin);
foreach (explode(PHP_EOL, $input) as $file) {
$skip = false;
$f = @fopen($file, 'r');
if ($f) {
$firstLine = fgets($f);
@fclose($f);
if (preg_match('~<?php\\s*\\/\\/\s*lint\s*([^\d\s]+)\s*([^\s]+)\s*~i', $firstLine, $m)) {
$skip = version_compare(PHP_VERSION, $m[2], $m[1]) === false;
}
}
echo $file . ';' . ($skip ? '1' : '0') . PHP_EOL;
}

View File

@ -0,0 +1,48 @@
#!/usr/bin/env php
<?php
/*
Copyright (c) 2014, Jakub Onderka
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.
*/
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300) {
fwrite(STDERR, "PHP Parallel Lint requires PHP 5.3.0 or newer." . PHP_EOL);
exit(254);
}
require_once __DIR__ . '/src/Process/Process.php';
require_once __DIR__ . '/src/Process/PhpProcess.php';
foreach (glob(__DIR__ . '/src/*/*.php') as $file) {
require_once $file;
}
foreach (glob(__DIR__ . '/src/*.php') as $file) {
require_once $file;
}
$app = new JakubOnderka\PhpParallelLint\Application();
exit($app->run());

View File

@ -0,0 +1,125 @@
<?php
namespace JakubOnderka\PhpParallelLint;
class Application
{
const VERSION = '1.3.1';
// Return codes
const SUCCESS = 0,
WITH_ERRORS = 1,
FAILED = 254; // Error code 255 is reserved for PHP itself
/**
* Run the application
* @return int Return code
*/
public function run()
{
if (in_array('proc_open', explode(',', ini_get('disable_functions')))) {
echo "Function 'proc_open' is required, but it is disabled by the 'disable_functions' ini setting.", PHP_EOL;
return self::FAILED;
}
if (in_array('-h', $_SERVER['argv']) || in_array('--help', $_SERVER['argv'])) {
$this->showUsage();
return self::SUCCESS;
}
if (in_array('-V', $_SERVER['argv']) || in_array('--version', $_SERVER['argv'])) {
$this->showVersion();
return self::SUCCESS;
}
try {
$settings = Settings::parseArguments($_SERVER['argv']);
if ($settings->stdin) {
$settings->addPaths(Settings::getPathsFromStdIn());
}
if (empty($settings->paths)) {
$this->showUsage();
return self::FAILED;
}
$manager = new Manager;
$result = $manager->run($settings);
if ($settings->ignoreFails) {
return $result->hasSyntaxError() ? self::WITH_ERRORS : self::SUCCESS;
} else {
return $result->hasError() ? self::WITH_ERRORS : self::SUCCESS;
}
} catch (InvalidArgumentException $e) {
echo "Invalid option {$e->getArgument()}", PHP_EOL, PHP_EOL;
$this->showOptions();
return self::FAILED;
} catch (Exception $e) {
if (isset($settings) && $settings->format === Settings::FORMAT_JSON) {
echo json_encode($e);
} else {
echo $e->getMessage(), PHP_EOL;
}
return self::FAILED;
} catch (\Exception $e) {
echo $e->getMessage(), PHP_EOL;
return self::FAILED;
}
}
/**
* Outputs the options
*/
private function showOptions()
{
echo <<<HELP
Options:
-p <php> Specify PHP-CGI executable to run (default: 'php').
-s, --short Set short_open_tag to On (default: Off).
-a, -asp Set asp_tags to On (default: Off).
-e <ext> Check only files with selected extensions separated by comma.
(default: php,php3,php4,php5,phtml,phpt)
--exclude Exclude a file or directory. If you want exclude multiple items,
use multiple exclude parameters.
-j <num> Run <num> jobs in parallel (default: 10).
--colors Enable colors in console output. (disables auto detection of color support)
--no-colors Disable colors in console output.
--no-progress Disable progress in console output.
--json Output results as JSON string.
--gitlab Output results for the GitLab Code Quality Widget.
--checkstyle Output results as Checkstyle XML.
--blame Try to show git blame for row with error.
--git <git> Path to Git executable to show blame message (default: 'git').
--stdin Load files and folder to test from standard input.
--ignore-fails Ignore failed tests.
--syntax-error-callback File with syntax error callback for ability to modify error
-h, --help Print this help.
-V, --version Display this application version
HELP;
}
/**
* Outputs the current version
*/
private function showVersion()
{
echo 'PHP Parallel Lint version ' . self::VERSION.PHP_EOL;
}
/**
* Shows usage
*/
private function showUsage()
{
$this->showVersion();
echo <<<USAGE
-------------------------------
Usage:
parallel-lint [sa] [-p php] [-e ext] [-j num] [--exclude dir] [files or directories]
USAGE;
$this->showOptions();
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace JakubOnderka\PhpParallelLint\Contracts;
use JakubOnderka\PhpParallelLint\SyntaxError;
interface SyntaxErrorCallback
{
/**
* @param SyntaxError $error
* @return SyntaxError
*/
public function errorFound(SyntaxError $error);
}

View File

@ -0,0 +1,226 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use ReturnTypeWillChange;
class Error implements \JsonSerializable
{
/** @var string */
protected $filePath;
/** @var string */
protected $message;
/**
* @param string $filePath
* @param string $message
*/
public function __construct($filePath, $message)
{
$this->filePath = $filePath;
$this->message = rtrim($message);
}
/**
* @return string
*/
public function getMessage()
{
return $this->message;
}
/**
* @return string
*/
public function getFilePath()
{
return $this->filePath;
}
/**
* @return string
*/
public function getShortFilePath()
{
$cwd = getcwd();
if ($cwd === '/') {
// For root directory in unix, do not modify path
return $this->filePath;
}
return preg_replace('/' . preg_quote($cwd, '/') . '/', '', $this->filePath, 1);
}
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
*/
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'type' => 'error',
'file' => $this->getFilePath(),
'message' => $this->getMessage(),
);
}
}
class Blame implements \JsonSerializable
{
public $name;
public $email;
/** @var \DateTime */
public $datetime;
public $commitHash;
public $summary;
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
*/
#[ReturnTypeWillChange]
function jsonSerialize()
{
return array(
'name' => $this->name,
'email' => $this->email,
'datetime' => $this->datetime,
'commitHash' => $this->commitHash,
'summary' => $this->summary,
);
}
}
class SyntaxError extends Error
{
/** @var Blame */
private $blame;
/**
* @return int|null
*/
public function getLine()
{
preg_match('~on line ([0-9]+)$~', $this->message, $matches);
if ($matches && isset($matches[1])) {
$onLine = (int) $matches[1];
return $onLine;
}
return null;
}
/**
* @param bool $translateTokens
* @return mixed|string
*/
public function getNormalizedMessage($translateTokens = false)
{
$message = preg_replace('~^(Parse|Fatal) error: (syntax error, )?~', '', $this->message);
$message = preg_replace('~ in ' . preg_quote(basename($this->filePath)) . ' on line [0-9]+$~', '', $message);
$message = ucfirst($message);
if ($translateTokens) {
$message = $this->translateTokens($message);
}
return $message;
}
/**
* @param Blame $blame
*/
public function setBlame(Blame $blame)
{
$this->blame = $blame;
}
/**
* @return Blame
*/
public function getBlame()
{
return $this->blame;
}
/**
* @param string $message
* @return string
*/
protected function translateTokens($message)
{
static $translateTokens = array(
'T_FILE' => '__FILE__',
'T_FUNC_C' => '__FUNCTION__',
'T_HALT_COMPILER' => '__halt_compiler()',
'T_INC' => '++',
'T_IS_EQUAL' => '==',
'T_IS_GREATER_OR_EQUAL' => '>=',
'T_IS_IDENTICAL' => '===',
'T_IS_NOT_IDENTICAL' => '!==',
'T_IS_SMALLER_OR_EQUAL' => '<=',
'T_LINE' => '__LINE__',
'T_METHOD_C' => '__METHOD__',
'T_MINUS_EQUAL' => '-=',
'T_MOD_EQUAL' => '%=',
'T_MUL_EQUAL' => '*=',
'T_NS_C' => '__NAMESPACE__',
'T_NS_SEPARATOR' => '\\',
'T_OBJECT_OPERATOR' => '->',
'T_OR_EQUAL' => '|=',
'T_PAAMAYIM_NEKUDOTAYIM' => '::',
'T_PLUS_EQUAL' => '+=',
'T_SL' => '<<',
'T_SL_EQUAL' => '<<=',
'T_SR' => '>>',
'T_SR_EQUAL' => '>>=',
'T_START_HEREDOC' => '<<<',
'T_XOR_EQUAL' => '^=',
'T_ECHO' => 'echo'
);
return preg_replace_callback('~T_([A-Z_]*)~', function ($matches) use ($translateTokens) {
list($tokenName) = $matches;
if (isset($translateTokens[$tokenName])) {
$operator = $translateTokens[$tokenName];
return "$operator ($tokenName)";
}
return $tokenName;
}, $message);
}
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
*/
public function jsonSerialize()
{
return array(
'type' => 'syntaxError',
'file' => $this->getFilePath(),
'line' => $this->getLine(),
'message' => $this->getMessage(),
'normalizeMessage' => $this->getNormalizedMessage(),
'blame' => $this->blame,
);
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use JakubOnderka\PhpConsoleColor\ConsoleColor;
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
class ErrorFormatter
{
/** @var string */
private $useColors;
/** @var bool */
private $forceColors;
/** @var bool */
private $translateTokens;
public function __construct($useColors = Settings::AUTODETECT, $translateTokens = false, $forceColors = false)
{
$this->useColors = $useColors;
$this->forceColors = $forceColors;
$this->translateTokens = $translateTokens;
}
/**
* @param Error $error
* @return string
*/
public function format(Error $error)
{
if ($error instanceof SyntaxError) {
return $this->formatSyntaxErrorMessage($error);
} else {
if ($error->getMessage()) {
return $error->getMessage();
} else {
return "Unknown error for file '{$error->getFilePath()}'.";
}
}
}
/**
* @param SyntaxError $error
* @param bool $withCodeSnipped
* @return string
*/
public function formatSyntaxErrorMessage(SyntaxError $error, $withCodeSnipped = true)
{
$string = "Parse error: {$error->getShortFilePath()}";
if ($error->getLine()) {
$onLine = $error->getLine();
$string .= ":$onLine" . PHP_EOL;
if ($withCodeSnipped) {
if ($this->useColors !== Settings::DISABLED) {
$string .= $this->getColoredCodeSnippet($error->getFilePath(), $onLine);
} else {
$string .= $this->getCodeSnippet($error->getFilePath(), $onLine);
}
}
}
$string .= $error->getNormalizedMessage($this->translateTokens);
if ($error->getBlame()) {
$blame = $error->getBlame();
$shortCommitHash = substr($blame->commitHash, 0, 8);
$dateTime = $blame->datetime->format('c');
$string .= PHP_EOL . "Blame {$blame->name} <{$blame->email}>, commit '$shortCommitHash' from $dateTime";
}
return $string;
}
/**
* @param string $filePath
* @param int $lineNumber
* @param int $linesBefore
* @param int $linesAfter
* @return string
*/
protected function getCodeSnippet($filePath, $lineNumber, $linesBefore = 2, $linesAfter = 2)
{
$lines = file($filePath);
$offset = $lineNumber - $linesBefore - 1;
$offset = max($offset, 0);
$length = $linesAfter + $linesBefore + 1;
$lines = array_slice($lines, $offset, $length, $preserveKeys = true);
end($lines);
$lineStrlen = strlen(key($lines) + 1);
$snippet = '';
foreach ($lines as $i => $line) {
$snippet .= ($lineNumber === $i + 1 ? ' > ' : ' ');
$snippet .= str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| ' . rtrim($line) . PHP_EOL;
}
return $snippet;
}
/**
* @param string $filePath
* @param int $lineNumber
* @param int $linesBefore
* @param int $linesAfter
* @return string
*/
protected function getColoredCodeSnippet($filePath, $lineNumber, $linesBefore = 2, $linesAfter = 2)
{
if (
!class_exists('\JakubOnderka\PhpConsoleHighlighter\Highlighter') ||
!class_exists('\JakubOnderka\PhpConsoleColor\ConsoleColor')
) {
return $this->getCodeSnippet($filePath, $lineNumber, $linesBefore, $linesAfter);
}
$colors = new ConsoleColor();
$colors->setForceStyle($this->forceColors);
$highlighter = new Highlighter($colors);
$fileContent = file_get_contents($filePath);
return $highlighter->getCodeSnippet($fileContent, $lineNumber, $linesBefore, $linesAfter);
}
}

View File

@ -0,0 +1,293 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use JakubOnderka\PhpParallelLint\Contracts\SyntaxErrorCallback;
use JakubOnderka\PhpParallelLint\Process\GitBlameProcess;
use JakubOnderka\PhpParallelLint\Process\PhpExecutable;
use ReturnTypeWillChange;
class Manager
{
/** @var Output */
protected $output;
/**
* @param null|Settings $settings
* @return Result
* @throws Exception
* @throws \Exception
*/
public function run(Settings $settings = null)
{
$settings = $settings ?: new Settings;
$output = $this->output ?: $this->getDefaultOutput($settings);
$phpExecutable = PhpExecutable::getPhpExecutable($settings->phpExecutable);
$olderThanPhp54 = $phpExecutable->getVersionId() < 50400; // From PHP version 5.4 are tokens translated by default
$translateTokens = $phpExecutable->isIsHhvmType() || $olderThanPhp54;
$output->writeHeader($phpExecutable->getVersionId(), $settings->parallelJobs, $phpExecutable->getHhvmVersion());
$files = $this->getFilesFromPaths($settings->paths, $settings->extensions, $settings->excluded);
if (empty($files)) {
throw new Exception('No file found to check.');
}
$output->setTotalFileCount(count($files));
$parallelLint = new ParallelLint($phpExecutable, $settings->parallelJobs);
$parallelLint->setAspTagsEnabled($settings->aspTags);
$parallelLint->setShortTagEnabled($settings->shortTag);
$parallelLint->setShowDeprecated($settings->showDeprecated);
$parallelLint->setSyntaxErrorCallback($this->createSyntaxErrorCallback($settings));
$parallelLint->setProcessCallback(function ($status, $file) use ($output) {
if ($status === ParallelLint::STATUS_OK) {
$output->ok();
} else if ($status === ParallelLint::STATUS_SKIP) {
$output->skip();
} else if ($status === ParallelLint::STATUS_ERROR) {
$output->error();
} else {
$output->fail();
}
});
$result = $parallelLint->lint($files);
if ($settings->blame) {
$this->gitBlame($result, $settings);
}
$output->writeResult($result, new ErrorFormatter($settings->colors, $translateTokens), $settings->ignoreFails);
return $result;
}
/**
* @param Output $output
*/
public function setOutput(Output $output)
{
$this->output = $output;
}
/**
* @param Settings $settings
* @return Output
*/
protected function getDefaultOutput(Settings $settings)
{
$writer = new ConsoleWriter;
switch ($settings->format) {
case Settings::FORMAT_JSON:
return new JsonOutput($writer);
case Settings::FORMAT_GITLAB:
return new GitLabOutput($writer);
case Settings::FORMAT_CHECKSTYLE:
return new CheckstyleOutput($writer);
}
if ($settings->colors === Settings::DISABLED) {
$output = new TextOutput($writer);
} else {
$output = new TextOutputColored($writer, $settings->colors);
}
$output->showProgress = $settings->showProgress;
return $output;
}
/**
* @param Result $result
* @param Settings $settings
* @throws Exception
*/
protected function gitBlame(Result $result, Settings $settings)
{
if (!GitBlameProcess::gitExists($settings->gitExecutable)) {
return;
}
foreach ($result->getErrors() as $error) {
if ($error instanceof SyntaxError) {
$process = new GitBlameProcess($settings->gitExecutable, $error->getFilePath(), $error->getLine());
$process->waitForFinish();
if ($process->isSuccess()) {
$blame = new Blame;
$blame->name = $process->getAuthor();
$blame->email = $process->getAuthorEmail();
$blame->datetime = $process->getAuthorTime();
$blame->commitHash = $process->getCommitHash();
$blame->summary = $process->getSummary();
$error->setBlame($blame);
}
}
}
}
/**
* @param array $paths
* @param array $extensions
* @param array $excluded
* @return array
* @throws NotExistsPathException
*/
protected function getFilesFromPaths(array $paths, array $extensions, array $excluded = array())
{
$extensions = array_map('preg_quote', $extensions, array_fill(0, count($extensions), '`'));
$regex = '`\.(?:' . implode('|', $extensions) . ')$`iD';
$files = array();
foreach ($paths as $path) {
if (is_file($path)) {
$files[] = $path;
} else if (is_dir($path)) {
$iterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
if (!empty($excluded)) {
$iterator = new RecursiveDirectoryFilterIterator($iterator, $excluded);
}
$iterator = new \RecursiveIteratorIterator(
$iterator,
\RecursiveIteratorIterator::LEAVES_ONLY,
\RecursiveIteratorIterator::CATCH_GET_CHILD
);
$iterator = new \RegexIterator($iterator, $regex);
/** @var \SplFileInfo[] $iterator */
foreach ($iterator as $directoryFile) {
$files[] = (string) $directoryFile;
}
} else {
throw new NotExistsPathException($path);
}
}
$files = array_unique($files);
return $files;
}
protected function createSyntaxErrorCallback(Settings $settings)
{
if ($settings->syntaxErrorCallbackFile === null) {
return null;
}
$fullFilePath = realpath($settings->syntaxErrorCallbackFile);
if ($fullFilePath === false) {
throw new NotExistsPathException($settings->syntaxErrorCallbackFile);
}
require_once $fullFilePath;
$expectedClassName = basename($fullFilePath, '.php');
if (!class_exists($expectedClassName)) {
throw new NotExistsClassException($expectedClassName, $settings->syntaxErrorCallbackFile);
}
$callbackInstance = new $expectedClassName;
if (!($callbackInstance instanceof SyntaxErrorCallback)) {
throw new NotImplementCallbackException($expectedClassName);
}
return $callbackInstance;
}
}
class RecursiveDirectoryFilterIterator extends \RecursiveFilterIterator
{
/** @var \RecursiveDirectoryIterator */
private $iterator;
/** @var array */
private $excluded = array();
/**
* @param \RecursiveDirectoryIterator $iterator
* @param array $excluded
*/
public function __construct(\RecursiveDirectoryIterator $iterator, array $excluded)
{
parent::__construct($iterator);
$this->iterator = $iterator;
$this->excluded = array_map(array($this, 'getPathname'), $excluded);
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Check whether the current element of the iterator is acceptable
*
* @link http://php.net/manual/en/filteriterator.accept.php
* @return bool true if the current element is acceptable, otherwise false.
*/
#[ReturnTypeWillChange]
public function accept()
{
$current = $this->current()->getPathname();
$current = $this->normalizeDirectorySeparator($current);
if ('.' . DIRECTORY_SEPARATOR !== $current[0] . $current[1]) {
$current = '.' . DIRECTORY_SEPARATOR . $current;
}
return !in_array($current, $this->excluded);
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Check whether the inner iterator's current element has children
*
* @link http://php.net/manual/en/recursivefilteriterator.haschildren.php
* @return bool true if the inner iterator has children, otherwise false
*/
#[ReturnTypeWillChange]
public function hasChildren()
{
return $this->iterator->hasChildren();
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Return the inner iterator's children contained in a RecursiveFilterIterator
*
* @link http://php.net/manual/en/recursivefilteriterator.getchildren.php
* @return \RecursiveFilterIterator containing the inner iterator's children.
*/
#[ReturnTypeWillChange]
public function getChildren()
{
return new self($this->iterator->getChildren(), $this->excluded);
}
/**
* @param string $file
* @return string
*/
private function getPathname($file)
{
$file = $this->normalizeDirectorySeparator($file);
if ('.' . DIRECTORY_SEPARATOR !== $file[0] . $file[1]) {
$file = '.' . DIRECTORY_SEPARATOR . $file;
}
$directoryFile = new \SplFileInfo($file);
return $directoryFile->getPathname();
}
/**
* @param string $file
* @return string
*/
private function normalizeDirectorySeparator($file)
{
return str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $file);
}
}

View File

@ -0,0 +1,588 @@
<?php
namespace JakubOnderka\PhpParallelLint;
interface Output
{
public function __construct(IWriter $writer);
public function ok();
public function skip();
public function error();
public function fail();
public function setTotalFileCount($count);
public function writeHeader($phpVersion, $parallelJobs, $hhvmVersion = null);
public function writeResult(Result $result, ErrorFormatter $errorFormatter, $ignoreFails);
}
class JsonOutput implements Output
{
/** @var IWriter */
protected $writer;
/** @var int */
protected $phpVersion;
/** @var int */
protected $parallelJobs;
/** @var string */
protected $hhvmVersion;
/**
* @param IWriter $writer
*/
public function __construct(IWriter $writer)
{
$this->writer = $writer;
}
public function ok()
{
}
public function skip()
{
}
public function error()
{
}
public function fail()
{
}
public function setTotalFileCount($count)
{
}
public function writeHeader($phpVersion, $parallelJobs, $hhvmVersion = null)
{
$this->phpVersion = $phpVersion;
$this->parallelJobs = $parallelJobs;
$this->hhvmVersion = $hhvmVersion;
}
public function writeResult(Result $result, ErrorFormatter $errorFormatter, $ignoreFails)
{
echo json_encode(array(
'phpVersion' => $this->phpVersion,
'hhvmVersion' => $this->hhvmVersion,
'parallelJobs' => $this->parallelJobs,
'results' => $result,
));
}
}
class GitLabOutput implements Output
{
/** @var IWriter */
protected $writer;
/**
* @param IWriter $writer
*/
public function __construct(IWriter $writer)
{
$this->writer = $writer;
}
public function ok()
{
}
public function skip()
{
}
public function error()
{
}
public function fail()
{
}
public function setTotalFileCount($count)
{
}
public function writeHeader($phpVersion, $parallelJobs, $hhvmVersion = null)
{
}
public function writeResult(Result $result, ErrorFormatter $errorFormatter, $ignoreFails)
{
$errors = array();
foreach ($result->getErrors() as $error) {
$message = $error->getMessage();
$line = 1;
if ($error instanceof SyntaxError) {
$line = $error->getLine();
}
$filePath = $error->getFilePath();
$result = array(
'type' => 'issue',
'check_name' => 'Parse error',
'description' => $message,
'categories' => 'Style',
'fingerprint' => md5($filePath . $message . $line),
'severity' => 'minor',
'location' => array(
'path' => $filePath,
'lines' => array(
'begin' => $line,
),
),
);
array_push($errors, $result);
}
$string = json_encode($errors) . PHP_EOL;
$this->writer->write($string);
}
}
class TextOutput implements Output
{
const TYPE_DEFAULT = 'default',
TYPE_SKIP = 'skip',
TYPE_ERROR = 'error',
TYPE_FAIL = 'fail',
TYPE_OK = 'ok';
/** @var int */
public $filesPerLine = 60;
/** @var bool */
public $showProgress = true;
/** @var int */
protected $checkedFiles;
/** @var int */
protected $totalFileCount;
/** @var IWriter */
protected $writer;
/**
* @param IWriter $writer
*/
public function __construct(IWriter $writer)
{
$this->writer = $writer;
}
public function ok()
{
$this->writeMark(self::TYPE_OK);
}
public function skip()
{
$this->writeMark(self::TYPE_SKIP);
}
public function error()
{
$this->writeMark(self::TYPE_ERROR);
}
public function fail()
{
$this->writeMark(self::TYPE_FAIL);
}
/**
* @param string $string
* @param string $type
*/
public function write($string, $type = self::TYPE_DEFAULT)
{
$this->writer->write($string);
}
/**
* @param string|null $line
* @param string $type
*/
public function writeLine($line = null, $type = self::TYPE_DEFAULT)
{
$this->write($line, $type);
$this->writeNewLine();
}
/**
* @param int $count
*/
public function writeNewLine($count = 1)
{
$this->write(str_repeat(PHP_EOL, $count));
}
/**
* @param int $count
*/
public function setTotalFileCount($count)
{
$this->totalFileCount = $count;
}
/**
* @param int $phpVersion
* @param int $parallelJobs
* @param string $hhvmVersion
*/
public function writeHeader($phpVersion, $parallelJobs, $hhvmVersion = null)
{
$this->write("PHP {$this->phpVersionIdToString($phpVersion)} | ");
if ($hhvmVersion) {
$this->write("HHVM $hhvmVersion | ");
}
if ($parallelJobs === 1) {
$this->writeLine("1 job");
} else {
$this->writeLine("{$parallelJobs} parallel jobs");
}
}
/**
* @param Result $result
* @param ErrorFormatter $errorFormatter
* @param bool $ignoreFails
*/
public function writeResult(Result $result, ErrorFormatter $errorFormatter, $ignoreFails)
{
if ($this->showProgress) {
if ($this->checkedFiles % $this->filesPerLine !== 0) {
$rest = $this->filesPerLine - ($this->checkedFiles % $this->filesPerLine);
$this->write(str_repeat(' ', $rest));
$this->writePercent();
}
$this->writeNewLine(2);
}
$testTime = round($result->getTestTime(), 1);
$message = "Checked {$result->getCheckedFilesCount()} files in $testTime ";
$message .= $testTime == 1 ? 'second' : 'seconds';
if ($result->getSkippedFilesCount() > 0) {
$message .= ", skipped {$result->getSkippedFilesCount()} ";
$message .= ($result->getSkippedFilesCount() === 1 ? 'file' : 'files');
}
$this->writeLine($message);
if (!$result->hasSyntaxError()) {
$message = "No syntax error found";
} else {
$message = "Syntax error found in {$result->getFilesWithSyntaxErrorCount()} ";
$message .= ($result->getFilesWithSyntaxErrorCount() === 1 ? 'file' : 'files');
}
if ($result->hasFilesWithFail()) {
$message .= ", failed to check {$result->getFilesWithFailCount()} ";
$message .= ($result->getFilesWithFailCount() === 1 ? 'file' : 'files');
if ($ignoreFails) {
$message .= ' (ignored)';
}
}
$hasError = $ignoreFails ? $result->hasSyntaxError() : $result->hasError();
$this->writeLine($message, $hasError ? self::TYPE_ERROR : self::TYPE_OK);
if ($result->hasError()) {
$this->writeNewLine();
foreach ($result->getErrors() as $error) {
$this->writeLine(str_repeat('-', 60));
$this->writeLine($errorFormatter->format($error));
}
}
}
protected function writeMark($type)
{
++$this->checkedFiles;
if ($this->showProgress) {
if ($type === self::TYPE_OK) {
$this->writer->write('.');
} else if ($type === self::TYPE_SKIP) {
$this->write('S', self::TYPE_SKIP);
} else if ($type === self::TYPE_ERROR) {
$this->write('X', self::TYPE_ERROR);
} else if ($type === self::TYPE_FAIL) {
$this->writer->write('-');
}
if ($this->checkedFiles % $this->filesPerLine === 0) {
$this->writePercent();
}
}
}
protected function writePercent()
{
$percent = floor($this->checkedFiles / $this->totalFileCount * 100);
$current = $this->stringWidth($this->checkedFiles, strlen($this->totalFileCount));
$this->writeLine(" $current/$this->totalFileCount ($percent %)");
}
/**
* @param string $input
* @param int $width
* @return string
*/
protected function stringWidth($input, $width = 3)
{
$multiplier = $width - strlen($input);
return str_repeat(' ', $multiplier > 0 ? $multiplier : 0) . $input;
}
/**
* @param int $phpVersionId
* @return string
*/
protected function phpVersionIdToString($phpVersionId)
{
$releaseVersion = (int) substr($phpVersionId, -2, 2);
$minorVersion = (int) substr($phpVersionId, -4, 2);
$majorVersion = (int) substr($phpVersionId, 0, strlen($phpVersionId) - 4);
return "$majorVersion.$minorVersion.$releaseVersion";
}
}
class CheckstyleOutput implements Output
{
private $writer;
public function __construct(IWriter $writer)
{
$this->writer = $writer;
}
public function ok()
{
}
public function skip()
{
}
public function error()
{
}
public function fail()
{
}
public function setTotalFileCount($count)
{
}
public function writeHeader($phpVersion, $parallelJobs, $hhvmVersion = null)
{
$this->writer->write('<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL);
}
public function writeResult(Result $result, ErrorFormatter $errorFormatter, $ignoreFails)
{
$this->writer->write('<checkstyle>' . PHP_EOL);
$errors = array();
foreach ($result->getErrors() as $error) {
$message = $error->getMessage();
if ($error instanceof SyntaxError) {
$line = $error->getLine();
$source = "Syntax Error";
} else {
$line = 1;
$source = "Linter Error";
}
$errors[$error->getShortFilePath()][] = array(
'message' => $message,
'line' => $line,
'source' => $source
);
}
foreach ($errors as $file => $fileErrors) {
$this->writer->write(sprintf(' <file name="%s">', $file) . PHP_EOL);
foreach ($fileErrors as $fileError) {
$this->writer->write(
sprintf(
' <error line="%d" severity="ERROR" message="%s" source="%s" />',
$fileError['line'],
$fileError['message'],
$fileError['source']
) .
PHP_EOL
);
}
$this->writer->write(' </file>' . PHP_EOL);
}
$this->writer->write('</checkstyle>' . PHP_EOL);
}
}
class TextOutputColored extends TextOutput
{
/** @var \JakubOnderka\PhpConsoleColor\ConsoleColor */
private $colors;
public function __construct(IWriter $writer, $colors = Settings::AUTODETECT)
{
parent::__construct($writer);
if (class_exists('\JakubOnderka\PhpConsoleColor\ConsoleColor')) {
$this->colors = new \JakubOnderka\PhpConsoleColor\ConsoleColor();
$this->colors->setForceStyle($colors === Settings::FORCED);
}
}
/**
* @param string $string
* @param string $type
* @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
*/
public function write($string, $type = self::TYPE_DEFAULT)
{
if (!$this->colors instanceof \JakubOnderka\PhpConsoleColor\ConsoleColor) {
parent::write($string, $type);
} else {
switch ($type) {
case self::TYPE_OK:
parent::write($this->colors->apply('bg_green', $string));
break;
case self::TYPE_SKIP:
parent::write($this->colors->apply('bg_yellow', $string));
break;
case self::TYPE_ERROR:
parent::write($this->colors->apply('bg_red', $string));
break;
default:
parent::write($string);
}
}
}
}
interface IWriter
{
/**
* @param string $string
*/
public function write($string);
}
class NullWriter implements IWriter
{
/**
* @param string $string
*/
public function write($string)
{
}
}
class ConsoleWriter implements IWriter
{
/**
* @param string $string
*/
public function write($string)
{
echo $string;
}
}
class FileWriter implements IWriter
{
/** @var string */
protected $logFile;
/** @var string */
protected $buffer;
public function __construct($logFile)
{
$this->logFile = $logFile;
}
public function write($string)
{
$this->buffer .= $string;
}
public function __destruct()
{
file_put_contents($this->logFile, $this->buffer);
}
}
class MultipleWriter implements IWriter
{
/** @var IWriter[] */
protected $writers;
/**
* @param IWriter[] $writers
*/
public function __construct(array $writers)
{
foreach ($writers as $writer) {
$this->addWriter($writer);
}
}
/**
* @param IWriter $writer
*/
public function addWriter(IWriter $writer)
{
$this->writers[] = $writer;
}
/**
* @param $string
*/
public function write($string)
{
foreach ($this->writers as $writer) {
$writer->write($string);
}
}
}

View File

@ -0,0 +1,286 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use JakubOnderka\PhpParallelLint\Contracts\SyntaxErrorCallback;
use JakubOnderka\PhpParallelLint\Process\LintProcess;
use JakubOnderka\PhpParallelLint\Process\PhpExecutable;
use JakubOnderka\PhpParallelLint\Process\SkipLintProcess;
class ParallelLint
{
const STATUS_OK = 'ok',
STATUS_SKIP = 'skip',
STATUS_FAIL = 'fail',
STATUS_ERROR = 'error';
/** @var int */
private $parallelJobs;
/** @var PhpExecutable */
private $phpExecutable;
/** @var bool */
private $aspTagsEnabled = false;
/** @var bool */
private $shortTagEnabled = false;
/** @var callable */
private $processCallback;
/** @var bool */
private $showDeprecated = false;
/** @var SyntaxErrorCallback|null */
private $syntaxErrorCallback = null;
public function __construct(PhpExecutable $phpExecutable, $parallelJobs = 10)
{
$this->phpExecutable = $phpExecutable;
$this->parallelJobs = $parallelJobs;
}
/**
* @param array $files
* @return Result
* @throws \Exception
*/
public function lint(array $files)
{
$startTime = microtime(true);
$skipLintProcess = new SkipLintProcess($this->phpExecutable, $files);
$processCallback = is_callable($this->processCallback) ? $this->processCallback : function () {
};
/**
* @var LintProcess[] $running
* @var LintProcess[] $waiting
*/
$errors = $running = $waiting = array();
$skippedFiles = $checkedFiles = array();
while ($files || $running) {
for ($i = count($running); $files && $i < $this->parallelJobs; $i++) {
$file = array_shift($files);
if ($skipLintProcess->isSkipped($file) === true) {
$skippedFiles[] = $file;
$processCallback(self::STATUS_SKIP, $file);
} else {
$running[$file] = new LintProcess(
$this->phpExecutable,
$file,
$this->aspTagsEnabled,
$this->shortTagEnabled,
$this->showDeprecated
);
}
}
$skipLintProcess->getChunk();
usleep(100);
foreach ($running as $file => $process) {
if ($process->isFinished()) {
unset($running[$file]);
$skipStatus = $skipLintProcess->isSkipped($file);
if ($skipStatus === null) {
$waiting[$file] = $process;
} else if ($skipStatus === true) {
$skippedFiles[] = $file;
$processCallback(self::STATUS_SKIP, $file);
} else if ($process->containsError()) {
$checkedFiles[] = $file;
$errors[] = $this->triggerSyntaxErrorCallback(new SyntaxError($file, $process->getSyntaxError()));
$processCallback(self::STATUS_ERROR, $file);
} else if ($process->isSuccess()) {
$checkedFiles[] = $file;
$processCallback(self::STATUS_OK, $file);
} else {
$errors[] = new Error($file, $process->getOutput());
$processCallback(self::STATUS_FAIL, $file);
}
}
}
}
if (!empty($waiting)) {
$skipLintProcess->waitForFinish();
if ($skipLintProcess->isFail()) {
$message = "Error in skip-linting.php process\nError output: {$skipLintProcess->getErrorOutput()}";
throw new \Exception($message);
}
foreach ($waiting as $file => $process) {
$skipStatus = $skipLintProcess->isSkipped($file);
if ($skipStatus === null) {
throw new \Exception("File $file has empty skip status. Please contact the author of PHP Parallel Lint.");
} else if ($skipStatus === true) {
$skippedFiles[] = $file;
$processCallback(self::STATUS_SKIP, $file);
} else if ($process->isSuccess()) {
$checkedFiles[] = $file;
$processCallback(self::STATUS_OK, $file);
} else if ($process->containsError()) {
$checkedFiles[] = $file;
$errors[] = $this->triggerSyntaxErrorCallback(new SyntaxError($file, $process->getSyntaxError()));
$processCallback(self::STATUS_ERROR, $file);
} else {
$errors[] = new Error($file, $process->getOutput());
$processCallback(self::STATUS_FAIL, $file);
}
}
}
$testTime = microtime(true) - $startTime;
return new Result($errors, $checkedFiles, $skippedFiles, $testTime);
}
/**
* @return int
*/
public function getParallelJobs()
{
return $this->parallelJobs;
}
/**
* @param int $parallelJobs
* @return ParallelLint
*/
public function setParallelJobs($parallelJobs)
{
$this->parallelJobs = $parallelJobs;
return $this;
}
/**
* @return string
*/
public function getPhpExecutable()
{
return $this->phpExecutable;
}
/**
* @param string $phpExecutable
* @return ParallelLint
*/
public function setPhpExecutable($phpExecutable)
{
$this->phpExecutable = $phpExecutable;
return $this;
}
/**
* @return callable
*/
public function getProcessCallback()
{
return $this->processCallback;
}
/**
* @param callable $processCallback
* @return ParallelLint
*/
public function setProcessCallback($processCallback)
{
$this->processCallback = $processCallback;
return $this;
}
/**
* @return boolean
*/
public function isAspTagsEnabled()
{
return $this->aspTagsEnabled;
}
/**
* @param boolean $aspTagsEnabled
* @return ParallelLint
*/
public function setAspTagsEnabled($aspTagsEnabled)
{
$this->aspTagsEnabled = $aspTagsEnabled;
return $this;
}
/**
* @return boolean
*/
public function isShortTagEnabled()
{
return $this->shortTagEnabled;
}
/**
* @param boolean $shortTagEnabled
* @return ParallelLint
*/
public function setShortTagEnabled($shortTagEnabled)
{
$this->shortTagEnabled = $shortTagEnabled;
return $this;
}
/**
* @return boolean
*/
public function isShowDeprecated()
{
return $this->showDeprecated;
}
/**
* @param $showDeprecated
* @return ParallelLint
*/
public function setShowDeprecated($showDeprecated)
{
$this->showDeprecated = $showDeprecated;
return $this;
}
public function triggerSyntaxErrorCallback($syntaxError)
{
if ($this->syntaxErrorCallback === null) {
return $syntaxError;
}
return $this->syntaxErrorCallback->errorFound($syntaxError);
}
/**
* @param SyntaxErrorCallback|null $syntaxErrorCallback
* @return ParallelLint
*/
public function setSyntaxErrorCallback($syntaxErrorCallback)
{
$this->syntaxErrorCallback = $syntaxErrorCallback;
return $this;
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace JakubOnderka\PhpParallelLint\Process;
use JakubOnderka\PhpParallelLint\RunTimeException;
class GitBlameProcess extends Process
{
/**
* @param string $gitExecutable
* @param string $file
* @param int $line
* @throws RunTimeException
*/
public function __construct($gitExecutable, $file, $line)
{
$arguments = array('blame', '-p', '-L', "$line,+1", $file);
parent::__construct($gitExecutable, $arguments);
}
/**
* @return bool
* @throws RunTimeException
*/
public function isSuccess()
{
return $this->getStatusCode() === 0;
}
/**
* @return string
* @throws RunTimeException
*/
public function getAuthor()
{
if (!$this->isSuccess()) {
throw new RunTimeException("Author can only be retrieved for successful process output.");
}
$output = $this->getOutput();
preg_match('~^author (.*)~m', $output, $matches);
return $matches[1];
}
/**
* @return string
* @throws RunTimeException
*/
public function getAuthorEmail()
{
if (!$this->isSuccess()) {
throw new RunTimeException("Author e-mail can only be retrieved for successful process output.");
}
$output = $this->getOutput();
preg_match('~^author-mail <(.*)>~m', $output, $matches);
return $matches[1];
}
/**
* @return \DateTime
* @throws RunTimeException
*/
public function getAuthorTime()
{
if (!$this->isSuccess()) {
throw new RunTimeException("Author time can only be retrieved for successful process output.");
}
$output = $this->getOutput();
preg_match('~^author-time (.*)~m', $output, $matches);
$time = $matches[1];
preg_match('~^author-tz (.*)~m', $output, $matches);
$zone = $matches[1];
return $this->getDateTime($time, $zone);
}
/**
* @return string
* @throws RunTimeException
*/
public function getCommitHash()
{
if (!$this->isSuccess()) {
throw new RunTimeException("Commit hash can only be retrieved for successful process output.");
}
return substr($this->getOutput(), 0, strpos($this->getOutput(), ' '));
}
/**
* @return string
* @throws RunTimeException
*/
public function getSummary()
{
if (!$this->isSuccess()) {
throw new RunTimeException("Commit summary can only be retrieved for successful process output.");
}
$output = $this->getOutput();
preg_match('~^summary (.*)~m', $output, $matches);
return $matches[1];
}
/**
* @param string $gitExecutable
* @return bool
* @throws RunTimeException
*/
public static function gitExists($gitExecutable)
{
$process = new Process($gitExecutable, array('--version'));
$process->waitForFinish();
return $process->getStatusCode() === 0;
}
/**
* This harakiri method is required to correct support time zone in PHP 5.4
*
* @param int $time
* @param string $zone
* @return \DateTime
* @throws \Exception
*/
protected function getDateTime($time, $zone)
{
$utcTimeZone = new \DateTimeZone('UTC');
$datetime = \DateTime::createFromFormat('U', $time, $utcTimeZone);
$way = substr($zone, 0, 1);
$hours = (int) substr($zone, 1, 2);
$minutes = (int) substr($zone, 3, 2);
$interval = new \DateInterval("PT{$hours}H{$minutes}M");
if ($way === '+') {
$datetime->add($interval);
} else {
$datetime->sub($interval);
}
return new \DateTime($datetime->format('Y-m-d\TH:i:s') . $zone, $utcTimeZone);
}
}

View File

@ -0,0 +1,137 @@
<?php
namespace JakubOnderka\PhpParallelLint\Process;
use JakubOnderka\PhpParallelLint\RunTimeException;
class LintProcess extends PhpProcess
{
const FATAL_ERROR = 'Fatal error';
const PARSE_ERROR = 'Parse error';
const DEPRECATED_ERROR = 'Deprecated:';
/**
* @var bool
*/
private $showDeprecatedErrors;
/**
* @param PhpExecutable $phpExecutable
* @param string $fileToCheck Path to file to check
* @param bool $aspTags
* @param bool $shortTag
* @param bool $deprecated
* @throws RunTimeException
*/
public function __construct(PhpExecutable $phpExecutable, $fileToCheck, $aspTags = false, $shortTag = false, $deprecated = false)
{
if (empty($fileToCheck)) {
throw new \InvalidArgumentException("File to check must be set.");
}
$parameters = array(
'-d asp_tags=' . ($aspTags ? 'On' : 'Off'),
'-d short_open_tag=' . ($shortTag ? 'On' : 'Off'),
'-d error_reporting=E_ALL',
'-n',
'-l',
$fileToCheck,
);
$this->showDeprecatedErrors = $deprecated;
parent::__construct($phpExecutable, $parameters);
}
/**
* @return bool
* @throws
*/
public function containsError()
{
return $this->containsParserError($this->getOutput()) ||
$this->containsFatalError($this->getOutput()) ||
$this->containsDeprecatedError($this->getOutput());
}
/**
* @return string
* @throws RunTimeException
*/
public function getSyntaxError()
{
if ($this->containsError()) {
// Look for fatal errors first
foreach (explode("\n", $this->getOutput()) as $line) {
if ($this->containsFatalError($line)) {
return $line;
}
}
// Look for parser errors second
foreach (explode("\n", $this->getOutput()) as $line) {
if ($this->containsParserError($line)) {
return $line;
}
}
// Look for deprecated errors third
foreach (explode("\n", $this->getOutput()) as $line) {
if ($this->containsDeprecatedError($line)) {
return $line;
}
}
throw new RunTimeException("The output '{$this->getOutput()}' does not contain Parse or Syntax errors");
}
return false;
}
/**
* @return bool
* @throws RunTimeException
*/
public function isFail()
{
return defined('PHP_WINDOWS_VERSION_MAJOR') ? $this->getStatusCode() === 1 : parent::isFail();
}
/**
* @return bool
* @throws RunTimeException
*/
public function isSuccess()
{
return $this->getStatusCode() === 0;
}
/**
* @param string $string
* @return bool
*/
private function containsParserError($string)
{
return strpos($string, self::PARSE_ERROR) !== false;
}
/**
* @param string $string
* @return bool
*/
private function containsFatalError($string)
{
return strpos($string, self::FATAL_ERROR) !== false;
}
/**
* @param string $string
* @return bool
*/
private function containsDeprecatedError($string)
{
if ($this->showDeprecatedErrors === false) {
return false;
}
return strpos($string, self::DEPRECATED_ERROR) !== false;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace JakubOnderka\PhpParallelLint\Process;
use JakubOnderka\PhpParallelLint\RunTimeException;
class PhpExecutable
{
/** @var string */
private $path;
/**
* Version as PHP_VERSION_ID constant
* @var int
*/
private $versionId;
/** @var string */
private $hhvmVersion;
/** @var bool */
private $isHhvmType;
/**
* @param string $path
* @param int $versionId
* @param string $hhvmVersion
* @param bool $isHhvmType
*/
public function __construct($path, $versionId, $hhvmVersion, $isHhvmType)
{
$this->path = $path;
$this->versionId = $versionId;
$this->hhvmVersion = $hhvmVersion;
$this->isHhvmType = $isHhvmType;
}
/**
* @return string
*/
public function getHhvmVersion()
{
return $this->hhvmVersion;
}
/**
* @return boolean
*/
public function isIsHhvmType()
{
return $this->isHhvmType;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return int
*/
public function getVersionId()
{
return $this->versionId;
}
/**
* @param string $phpExecutable
* @return PhpExecutable
* @throws \Exception
*/
public static function getPhpExecutable($phpExecutable)
{
$codeToExecute = <<<PHP
echo 'PHP;', PHP_VERSION_ID, ';', defined('HPHP_VERSION') ? HPHP_VERSION : null;
PHP;
$process = new Process($phpExecutable, array('-n', '-r', $codeToExecute));
$process->waitForFinish();
try {
if ($process->getStatusCode() !== 0 && $process->getStatusCode() !== 255) {
throw new RunTimeException("Unable to execute '{$phpExecutable}'.");
}
return self::getPhpExecutableFromOutput($phpExecutable, $process->getOutput());
} catch (RunTimeException $e) {
// Try HHVM type
$process = new Process($phpExecutable, array('--php', '-r', $codeToExecute));
$process->waitForFinish();
if ($process->getStatusCode() !== 0 && $process->getStatusCode() !== 255) {
throw new RunTimeException("Unable to execute '{$phpExecutable}'.");
}
return self::getPhpExecutableFromOutput($phpExecutable, $process->getOutput(), $isHhvmType = true);
}
}
/**
* @param string $phpExecutable
* @param string $output
* @param bool $isHhvmType
* @return PhpExecutable
* @throws RunTimeException
*/
private static function getPhpExecutableFromOutput($phpExecutable, $output, $isHhvmType = false)
{
$parts = explode(';', $output);
if ($parts[0] !== 'PHP' || !preg_match('~([0-9]+)~', $parts[1], $matches)) {
throw new RunTimeException("'{$phpExecutable}' is not valid PHP binary.");
}
$hhvmVersion = isset($parts[2]) ? $parts[2] : false;
return new PhpExecutable(
$phpExecutable,
intval($matches[1]),
$hhvmVersion,
$isHhvmType
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace JakubOnderka\PhpParallelLint\Process;
class PhpProcess extends Process
{
/**
* @param PhpExecutable $phpExecutable
* @param array $parameters
* @param string|null $stdIn
* @throws \JakubOnderka\PhpParallelLint\RunTimeException
*/
public function __construct(PhpExecutable $phpExecutable, array $parameters = array(), $stdIn = null)
{
$constructedParameters = $this->constructParameters($parameters, $phpExecutable->isIsHhvmType());
parent::__construct($phpExecutable->getPath(), $constructedParameters, $stdIn);
}
/**
* @param array $parameters
* @param bool $isHhvm
* @return array
*/
private function constructParameters(array $parameters, $isHhvm)
{
// Always ignore PHP startup errors ("Unable to load library...") in sub-processes.
array_unshift($parameters, '-d display_startup_errors=0');
if ($isHhvm) {
array_unshift($parameters, '-php');
}
return $parameters;
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace JakubOnderka\PhpParallelLint\Process;
use JakubOnderka\PhpParallelLint\RunTimeException;
class Process
{
const STDIN = 0,
STDOUT = 1,
STDERR = 2;
const READ = 'r',
WRITE = 'w';
/** @var resource */
protected $process;
/** @var resource */
protected $stdout;
/** @var resource */
protected $stderr;
/** @var string */
private $output;
/** @var string */
private $errorOutput;
/** @var int */
private $statusCode;
/**
* @param string $executable
* @param string[] $arguments
* @param string $stdInInput
* @throws RunTimeException
*/
public function __construct($executable, array $arguments = array(), $stdInInput = null)
{
$descriptors = array(
self::STDIN => array('pipe', self::READ),
self::STDOUT => array('pipe', self::WRITE),
self::STDERR => array('pipe', self::WRITE),
);
$cmdLine = $executable . ' ' . implode(' ', array_map('escapeshellarg', $arguments));
$this->process = proc_open($cmdLine, $descriptors, $pipes, null, null, array('bypass_shell' => true));
if ($this->process === false || $this->process === null) {
throw new RunTimeException("Cannot create new process $cmdLine");
}
list($stdin, $this->stdout, $this->stderr) = $pipes;
if ($stdInInput) {
fwrite($stdin, $stdInInput);
}
fclose($stdin);
}
/**
* @return bool
*/
public function isFinished()
{
if ($this->statusCode !== null) {
return true;
}
$status = proc_get_status($this->process);
if ($status['running']) {
return false;
} else if ($this->statusCode === null) {
$this->statusCode = (int) $status['exitcode'];
}
// Process outputs
$this->output = stream_get_contents($this->stdout);
fclose($this->stdout);
$this->errorOutput = stream_get_contents($this->stderr);
fclose($this->stderr);
$statusCode = proc_close($this->process);
if ($this->statusCode === null) {
$this->statusCode = $statusCode;
}
$this->process = null;
return true;
}
public function waitForFinish()
{
while (!$this->isFinished()) {
usleep(100);
}
}
/**
* @return string
* @throws RunTimeException
*/
public function getOutput()
{
if (!$this->isFinished()) {
throw new RunTimeException("Cannot get output for running process");
}
return $this->output;
}
/**
* @return string
* @throws RunTimeException
*/
public function getErrorOutput()
{
if (!$this->isFinished()) {
throw new RunTimeException("Cannot get error output for running process");
}
return $this->errorOutput;
}
/**
* @return int
* @throws RunTimeException
*/
public function getStatusCode()
{
if (!$this->isFinished()) {
throw new RunTimeException("Cannot get status code for running process");
}
return $this->statusCode;
}
/**
* @return bool
* @throws RunTimeException
*/
public function isFail()
{
return $this->getStatusCode() === 1;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace JakubOnderka\PhpParallelLint\Process;
use JakubOnderka\PhpParallelLint\RunTimeException;
class SkipLintProcess extends PhpProcess
{
/** @var array */
private $skipped = array();
/** @var bool */
private $done = false;
/** @var string */
private $endLastChunk = '';
/**
* @param PhpExecutable $phpExecutable
* @param array $filesToCheck
* @throws RunTimeException
*/
public function __construct(PhpExecutable $phpExecutable, array $filesToCheck)
{
$scriptPath = __DIR__ . '/../../bin/skip-linting.php';
$script = file_get_contents($scriptPath);
if (!$script) {
throw new RunTimeException("skip-linting.php script not found in '$scriptPath'.");
}
$script = str_replace('<?php', '', $script);
$parameters = array('-d', 'display_errors=stderr', '-r', $script);
parent::__construct($phpExecutable, $parameters, implode(PHP_EOL, $filesToCheck));
}
/**
* @throws RunTimeException
*/
public function getChunk()
{
if (!$this->isFinished()) {
$this->processLines(fread($this->stdout, 8192));
}
}
/**
* @return bool
* @throws \JakubOnderka\PhpParallelLint\RunTimeException
*/
public function isFinished()
{
$isFinished = parent::isFinished();
if ($isFinished && !$this->done) {
$this->done = true;
$output = $this->getOutput();
$this->processLines($output);
}
return $isFinished;
}
/**
* @param string $file
* @return bool|null
*/
public function isSkipped($file)
{
if (isset($this->skipped[$file])) {
return $this->skipped[$file];
}
return null;
}
/**
* @param string $content
*/
private function processLines($content)
{
if (!empty($content)) {
$lines = explode(PHP_EOL, $this->endLastChunk . $content);
$this->endLastChunk = array_pop($lines);
foreach ($lines as $line) {
$parts = explode(';', $line);
list($file, $status) = $parts;
$this->skipped[$file] = $status === '1' ? true : false;
}
}
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use ReturnTypeWillChange;
class Result implements \JsonSerializable
{
/** @var Error[] */
private $errors;
/** @var array */
private $checkedFiles;
/** @var array */
private $skippedFiles;
/** @var float */
private $testTime;
/**
* @param Error[] $errors
* @param array $checkedFiles
* @param array $skippedFiles
* @param float $testTime
*/
public function __construct(array $errors, array $checkedFiles, array $skippedFiles, $testTime)
{
$this->errors = $errors;
$this->checkedFiles = $checkedFiles;
$this->skippedFiles = $skippedFiles;
$this->testTime = $testTime;
}
/**
* @return Error[]
*/
public function getErrors()
{
return $this->errors;
}
/**
* @return bool
*/
public function hasError()
{
return !empty($this->errors);
}
/**
* @return array
*/
public function getFilesWithFail()
{
$filesWithFail = array();
foreach ($this->errors as $error) {
if (!$error instanceof SyntaxError) {
$filesWithFail[] = $error->getFilePath();
}
}
return $filesWithFail;
}
/**
* @return int
*/
public function getFilesWithFailCount()
{
return count($this->getFilesWithFail());
}
/**
* @return bool
*/
public function hasFilesWithFail()
{
return $this->getFilesWithFailCount() !== 0;
}
/**
* @return array
*/
public function getCheckedFiles()
{
return $this->checkedFiles;
}
/**
* @return int
*/
public function getCheckedFilesCount()
{
return count($this->checkedFiles);
}
/**
* @return array
*/
public function getSkippedFiles()
{
return $this->skippedFiles;
}
/**
* @return int
*/
public function getSkippedFilesCount()
{
return count($this->skippedFiles);
}
/**
* @return array
*/
public function getFilesWithSyntaxError()
{
$filesWithSyntaxError = array();
foreach ($this->errors as $error) {
if ($error instanceof SyntaxError) {
$filesWithSyntaxError[] = $error->getFilePath();
}
}
return $filesWithSyntaxError;
}
/**
* @return int
*/
public function getFilesWithSyntaxErrorCount()
{
return count($this->getFilesWithSyntaxError());
}
/**
* @return bool
*/
public function hasSyntaxError()
{
return $this->getFilesWithSyntaxErrorCount() !== 0;
}
/**
* @return float
*/
public function getTestTime()
{
return $this->testTime;
}
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
*/
#[ReturnTypeWillChange]
function jsonSerialize()
{
return array(
'checkedFiles' => $this->getCheckedFiles(),
'filesWithSyntaxError' => $this->getFilesWithSyntaxError(),
'skippedFiles' => $this->getSkippedFiles(),
'errors' => $this->getErrors(),
);
}
}

View File

@ -0,0 +1,247 @@
<?php
namespace JakubOnderka\PhpParallelLint;
class Settings
{
/**
* constants for enum settings
*/
const FORCED = 'FORCED';
const DISABLED = 'DISABLED';
const AUTODETECT = 'AUTODETECT';
const FORMAT_TEXT = 'text';
const FORMAT_JSON = 'json';
const FORMAT_GITLAB = 'gitlab';
const FORMAT_CHECKSTYLE = 'checkstyle';
/**
* Path to PHP executable
* @var string
*/
public $phpExecutable = 'php';
/**
* Check code inside PHP opening short tag <? or <?= in PHP 5.3
* @var bool
*/
public $shortTag = false;
/**
* Check PHP code inside ASP-style <% %> tags.
* @var bool
*/
public $aspTags = false;
/**
* Number of jobs running in same time
* @var int
*/
public $parallelJobs = 10;
/**
* If path contains directory, only file with these extensions are checked
* @var array
*/
public $extensions = array('php', 'phtml', 'php3', 'php4', 'php5', 'phpt');
/**
* Array of file or directories to check
* @var array
*/
public $paths = array();
/**
* Don't check files or directories
* @var array
*/
public $excluded = array();
/**
* Mode for color detection. Possible values: self::FORCED, self::DISABLED and self::AUTODETECT
* @var string
*/
public $colors = self::AUTODETECT;
/**
* Show progress in text output
* @var bool
*/
public $showProgress = true;
/**
* Output format (see FORMAT_* constants)
* @var string
*/
public $format = self::FORMAT_TEXT;
/**
* Read files and folder to tests from standard input (blocking)
* @var bool
*/
public $stdin = false;
/**
* Try to show git blame for row with error
* @var bool
*/
public $blame = false;
/**
* Path to git executable for blame
* @var string
*/
public $gitExecutable = 'git';
/**
* @var bool
*/
public $ignoreFails = false;
/**
* @var bool
*/
public $showDeprecated = false;
/**
* Path to a file with syntax error callback
* @var string|null
*/
public $syntaxErrorCallbackFile = null;
/**
* @param array $paths
*/
public function addPaths(array $paths)
{
$this->paths = array_merge($this->paths, $paths);
}
/**
* @param array $arguments
* @return Settings
* @throws InvalidArgumentException
*/
public static function parseArguments(array $arguments)
{
$arguments = new ArrayIterator(array_slice($arguments, 1));
$settings = new self;
// Use the currently invoked php as the default if possible
if (defined('PHP_BINARY')) {
$settings->phpExecutable = PHP_BINARY;
}
foreach ($arguments as $argument) {
if ($argument[0] !== '-') {
$settings->paths[] = $argument;
} else {
switch ($argument) {
case '-p':
$settings->phpExecutable = $arguments->getNext();
break;
case '-s':
case '--short':
$settings->shortTag = true;
break;
case '-a':
case '--asp':
$settings->aspTags = true;
break;
case '--exclude':
$settings->excluded[] = $arguments->getNext();
break;
case '-e':
$settings->extensions = array_map('trim', explode(',', $arguments->getNext()));
break;
case '-j':
$settings->parallelJobs = max((int) $arguments->getNext(), 1);
break;
case '--colors':
$settings->colors = self::FORCED;
break;
case '--no-colors':
$settings->colors = self::DISABLED;
break;
case '--no-progress':
$settings->showProgress = false;
break;
case '--json':
$settings->format = self::FORMAT_JSON;
break;
case '--gitlab':
$settings->format = self::FORMAT_GITLAB;
break;
case '--checkstyle':
$settings->format = self::FORMAT_CHECKSTYLE;
break;
case '--git':
$settings->gitExecutable = $arguments->getNext();
break;
case '--stdin':
$settings->stdin = true;
break;
case '--blame':
$settings->blame = true;
break;
case '--ignore-fails':
$settings->ignoreFails = true;
break;
case '--show-deprecated':
$settings->showDeprecated = true;
break;
case '--syntax-error-callback':
$settings->syntaxErrorCallbackFile = $arguments->getNext();
break;
default:
throw new InvalidArgumentException($argument);
}
}
}
return $settings;
}
/**
* @return array
*/
public static function getPathsFromStdIn()
{
$content = stream_get_contents(STDIN);
if (empty($content)) {
return array();
}
$lines = explode("\n", rtrim($content));
return array_map('rtrim', $lines);
}
}
class ArrayIterator extends \ArrayIterator
{
public function getNext()
{
$this->next();
return $this->current();
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use ReturnTypeWillChange;
class Exception extends \Exception implements \JsonSerializable
{
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'type' => get_class($this),
'message' => $this->getMessage(),
'code' => $this->getCode(),
);
}
}
class RunTimeException extends Exception
{
}
class InvalidArgumentException extends Exception
{
protected $argument;
public function __construct($argument)
{
$this->argument = $argument;
$this->message = "Invalid argument $argument";
}
public function getArgument()
{
return $this->argument;
}
}
class NotExistsPathException extends Exception
{
protected $path;
public function __construct($path)
{
$this->path = $path;
$this->message = "Path '$path' not found";
}
public function getPath()
{
return $this->path;
}
}
class NotExistsClassException extends Exception
{
protected $className;
protected $fileName;
public function __construct($className, $fileName)
{
$this->className = $className;
$this->fileName = $fileName;
$this->message = "Class with name '$className' does not exists in file '$fileName'";
}
public function getClassName()
{
return $this->className;
}
public function getFileName()
{
return $this->fileName;
}
}
class NotImplementCallbackException extends Exception
{
protected $className;
public function __construct($className)
{
$this->className = $className;
$this->message = "Class '$className' does not implement SyntaxErrorCallback interface.";
}
public function getClassName()
{
return $this->className;
}
}

View File

@ -0,0 +1,15 @@
<?php
/**
* Polyfill for PHP < 5.4
*/
if (!interface_exists('JsonSerializable', false)) {
interface JsonSerializable
{
/**
* @param void
* @return mixed
*/
function jsonSerialize();
}
}