基础代码

This commit is contained in:
2021-02-26 22:23:13 +08:00
parent 7884df52f0
commit a719feebba
2585 changed files with 328263 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# 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]
## [1.2.0] - 2020-04-04
### Added
- Added changelog.
### Fixed
- Fixed vendor location for running from other folder from [@Erkens](https://github.com/Erkens).
### Internal
- Added a .gitattributes file from [@jrfnl](https://github.com/jrfnl), thanks for issue to [@ondrejmirtes](https://github.com/ondrejmirtes).
- Fixed incorrect unit tests from [@jrfnl](https://github.com/jrfnl).
- Fixed minor grammatical errors from [@jrfnl](https://github.com/jrfnl).
- Added Travis: test against nightly (= PHP 8) from [@jrfnl](https://github.com/jrfnl).
- Travis: removed sudo from [@jrfnl](https://github.com/jrfnl).
- Added info about installing like not a dependency.
- Cleaned readme - new organization from previous package.
- Added checklist for new version from [@szepeviktor](https://github.com/szepeviktor).
+26
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.
+94
View File
@@ -0,0 +1,94 @@
# 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://travis-ci.org/php-parallel-lint/PHP-Parallel-Lint.svg?branch=master)](https://travis-ci.org/php-parallel-lint/PHP-Parallel-Lint)
[![License](https://poser.pugx.org/php-parallel-lint/php-parallel-lint/license.svg)](https://packagist.org/packages/php-parallel-lint/php-parallel-lint)
This tool checks syntax of PHP files faster than serial check with a fancier output.
Running parallel jobs in PHP is inspired by Nette framework tests.
This works from PHP 5.4 to 7.4
## Table of contents
1. [Installation](#installation)
2. [Example output](#example-output)
3. [Fork](#fork)
4. [Options for run](#options-for-run)
5. [Options for Symfony](#recommended-setting-for-usage-with-symfony-framework)
6. [Create Phar package](#create-phar-package)
7. [How upgrade](#how-upgrade)
## Installation
Just run the following command to install it:
composer require --dev php-parallel-lint/php-parallel-lint
When you cannot use tool as dependency then you can install as project. Command for it:
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 also 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")
## Fork
This is a fork of [original project](https://github.com/JakubOnderka/PHP-Parallel-Lint). Why I forked it and why I am the right man?
- Project is used in many and projects.
- I am [second most active](https://github.com/JakubOnderka/PHP-Parallel-Lint/graphs/contributors) contributor in original project.
- Author does [not responds to issues and PRs](https://github.com/JakubOnderka/PHP-Parallel-Lint/pulls) and my mail messages.
## Options for run
- `-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 (require PHP 5.4).
- `--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.
- `-h, --help` Print this help.
- `-V, --version` Display this application version.
## Recommended setting for usage with Symfony framework
For run from 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
and then run this command in parallel lint folder, which creates `parallel-lint.phar` file.
box build
## How upgrade
Are you using original package? You can easy use this fork. Steps for upgrade are:
composer remove --dev jakub-onderka/php-parallel-lint
composer require --dev php-parallel-lint/php-parallel-lint
@@ -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;
}
@@ -0,0 +1,34 @@
{
"name": "php-parallel-lint/php-parallel-lint",
"description": "This tool check syntax of PHP files about 20x faster than serial check.",
"homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
"license": "BSD-2-Clause",
"require": {
"php": ">=5.4.0",
"ext-json": "*"
},
"require-dev": {
"nette/tester": "^1.3 || ^2.0",
"php-parallel-lint/php-console-highlighter": "~0.3",
"squizlabs/php_codesniffer": "~3.0"
},
"suggest": {
"php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet"
},
"authors": [
{
"name": "Jakub Onderka",
"email": "ahoj@jakubonderka.cz"
}
],
"autoload": {
"classmap": [
"./"
]
},
"bin": ["parallel-lint"],
"replace": {
"jakub-onderka/php-parallel-lint": "*",
"grogy/php-parallel-lint": "*"
}
}
@@ -0,0 +1,65 @@
#!/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 < 50400) {
fwrite(STDERR, "PHP Parallel Lint require PHP 5.4.0 or newer." . PHP_EOL);
exit(254);
}
$autoloadLocations = [
getcwd() . '/vendor/autoload.php',
getcwd() . '/../../autoload.php',
__DIR__ . '/vendor/autoload.php',
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
__DIR__ . '/../../autoload.php',
];
$loaded = false;
foreach ($autoloadLocations as $autoload) {
if (is_file($autoload)) {
require_once($autoload);
$loaded = true;
}
}
if (!$loaded) {
fwrite(STDERR,
'You must set up the project dependencies, run the following commands:' . PHP_EOL .
'curl -s http://getcomposer.org/installer | php' . PHP_EOL .
'php composer.phar install' . PHP_EOL
);
exit(254);
}
$app = new JakubOnderka\PhpParallelLint\Application();
exit($app->run());
@@ -0,0 +1,123 @@
<?php
namespace JakubOnderka\PhpParallelLint;
class Application
{
const VERSION = '1.2.0';
// 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.
--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.
-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();
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
namespace JakubOnderka\PhpParallelLint;
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.
*/
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.
*/
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,
);
}
}
@@ -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);
}
}
@@ -0,0 +1,257 @@
<?php
namespace JakubOnderka\PhpParallelLint;
use JakubOnderka\PhpParallelLint\Process\GitBlameProcess;
use JakubOnderka\PhpParallelLint\Process\PhpExecutable;
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->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_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_flip($extensions);
$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
);
/** @var \SplFileInfo[] $iterator */
foreach ($iterator as $directoryFile) {
if (isset($extensions[pathinfo($directoryFile->getFilename(), PATHINFO_EXTENSION)])) {
$files[] = (string) $directoryFile;
}
}
} else {
throw new NotExistsPathException($path);
}
}
$files = array_unique($files);
return $files;
}
}
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.
*/
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
*/
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.
*/
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);
}
}
@@ -0,0 +1,513 @@
<?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 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);
}
}
}
@@ -0,0 +1,262 @@
<?php
namespace JakubOnderka\PhpParallelLint;
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;
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[] = 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->getErrorOutput()) {
$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[] = 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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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
);
}
}
@@ -0,0 +1,32 @@
<?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)
{
if ($isHhvm) {
$parameters = array_merge(array('-php'), $parameters);
}
return $parameters;
}
}
@@ -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;
}
}
@@ -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;
}
}
}
}
@@ -0,0 +1,168 @@
<?php
namespace JakubOnderka\PhpParallelLint;
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.
*/
function jsonSerialize()
{
return array(
'checkedFiles' => $this->getCheckedFiles(),
'filesWithSyntaxError' => $this->getFilesWithSyntaxError(),
'skippedFiles' => $this->getSkippedFiles(),
'errors' => $this->getErrors(),
);
}
}
@@ -0,0 +1,232 @@
<?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_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;
/**
* @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 '--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;
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();
}
}
@@ -0,0 +1,51 @@
<?php
namespace JakubOnderka\PhpParallelLint;
class Exception extends \Exception implements \JsonSerializable
{
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;
}
}