Actually commit api-client-php's vendor/ - it was silently gitignored
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 6s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 4s
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 6s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 4s
omsorgCore/CLAUDE.md documents vendor/ as committed so the app can run without a composer install step, but the generator's own .gitignore (api-client-php/.gitignore) excludes /vendor/ - it only ever existed untracked on disk locally, which is why the Docker build worked for me but mitarbeiter-app crashed at runtime on a real (fresh-checkout) deploy: "Failed opening required '.../api-client-php/vendor/autoload.php'". Verified by building from a git-archive-simulated fresh checkout with this fix applied - mitarbeiter-app's login page now renders without the fatal error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c8a514af9b
commit
dca0349e8c
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2012+ Fabien Potencier, Dariusz Rumiński
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<p align="center">
|
||||
<a href="https://cs.symfony.com">
|
||||
<img src="./logo.png" title="PHP CS Fixer" alt="PHP CS Fixer logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# PHP Coding Standards Fixer
|
||||
|
||||
The PHP Coding Standards Fixer (PHP CS Fixer) fixes your code to follow the standards.
|
||||
|
||||
If you are already using a linter to identify coding standards problems in your
|
||||
code, you know that fixing them by hand is tedious, especially on large
|
||||
projects. This tool not only detects them, but also fixes them for you.
|
||||
|
||||
PHP CS Fixer has built-in rule sets, whether you want to follow PHP coding standards as defined by [PHP-FIG's PER Coding Style](https://www.php-fig.org/per/coding-style/) - [`@PER-CS`](./doc/ruleSets/PER-CS.rst),
|
||||
a wide community like the [Symfony](https://symfony.com/doc/current/contributing/code/standards.html) - [`@Symfony`](./doc/ruleSets/Symfony.rst),
|
||||
or our opinionated one - [@PhpCsFixer](./doc/ruleSets/PhpCsFixer.rst).
|
||||
You can also define your (team's) style through the [configuration file](./doc/config.rst).
|
||||
|
||||
PHP CS Fixer can not only unify the style of your code, but also help to modernise your codebase towards
|
||||
newer PHP (e.g. [`@autoPHPMigration`](./doc/ruleSets/AutoPHPMigration.rst) and [`@autoPHPMigration:risky`](./doc/ruleSets/AutoPHPMigrationRisky.rst)) and newer PHPUnit (e.g. [`@autoPHPUnitMigration:risky`](./doc/ruleSets/AutoPHPUnitMigrationRisky.rst)).
|
||||
|
||||
There are also [`@auto`](./doc/ruleSets/Auto.rst) and [`@auto:risky`](./doc/ruleSets/AutoRisky.rst) that aim to provide good base rules.
|
||||
|
||||
## Supported PHP Versions
|
||||
|
||||
* PHP 7.4 - PHP 8.5
|
||||
|
||||
> [!NOTE]
|
||||
> Each new PHP version requires a huge effort to support the new syntax.
|
||||
> That's why the latest PHP version might not be supported yet. If you need it,
|
||||
> please consider supporting the project in any convenient way, for example,
|
||||
> with code contributions or reviewing existing PRs. To run PHP CS Fixer on yet
|
||||
> unsupported versions "at your own risk" - use `--allow-unsupported-php-version=yes` option.
|
||||
|
||||
## Documentation
|
||||
|
||||
### Installation
|
||||
|
||||
The recommended way to install PHP CS Fixer is to use [Composer](https://getcomposer.org/download/):
|
||||
|
||||
```sh
|
||||
composer require --dev friendsofphp/php-cs-fixer
|
||||
## or when facing conflicts in dependencies:
|
||||
composer require --dev php-cs-fixer/shim
|
||||
```
|
||||
|
||||
For more details and other installation methods (also with Docker or behind CI), see
|
||||
[installation instructions](./doc/installation.rst).
|
||||
|
||||
### Usage
|
||||
|
||||
Assuming you installed PHP CS Fixer as instructed above, you can
|
||||
initialise base config for your project by using following command:
|
||||
|
||||
```sh
|
||||
./vendor/bin/php-cs-fixer init
|
||||
```
|
||||
|
||||
To automatically fix your project, or only check against the need of changes, run:
|
||||
|
||||
```sh
|
||||
./vendor/bin/php-cs-fixer fix
|
||||
./vendor/bin/php-cs-fixer check
|
||||
```
|
||||
|
||||
See [usage](./doc/usage.rst), list of [built-in rules](./doc/rules/index.rst), list of [rule sets](./doc/ruleSets/index.rst)
|
||||
and [configuration file](./doc/config.rst) documentation for more details.
|
||||
|
||||
If you need to apply code styles that are not built-in into the tool, you can
|
||||
[create custom rules](./doc/custom_rules.rst).
|
||||
|
||||
## Editor Integration
|
||||
|
||||
Native support exists for:
|
||||
|
||||
* [PhpStorm](https://www.jetbrains.com/help/phpstorm/using-php-cs-fixer.html)
|
||||
|
||||
Community plugins exist for:
|
||||
|
||||
* [NetBeans](https://plugins.netbeans.apache.org/catalogue/?id=36)
|
||||
* [Sublime Text](https://github.com/benmatselby/sublime-phpcs)
|
||||
* [Vim](https://github.com/stephpy/vim-php-cs-fixer)
|
||||
* [VS Code](https://github.com/junstyle/vscode-php-cs-fixer)
|
||||
|
||||
## Community
|
||||
|
||||
The PHP CS Fixer is maintained on GitHub at <https://github.com/PHP-CS-Fixer/PHP-CS-Fixer>.
|
||||
Contributions, bug reports and ideas about new features are welcome there.
|
||||
|
||||
You can reach us in the [GitHub Discussions](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/discussions/) regarding the
|
||||
project, configuration, possible improvements, ideas and questions.
|
||||
|
||||
## Contribute
|
||||
|
||||
The tool comes with quite a few built-in fixers, but everyone is more than
|
||||
welcome to [contribute](./CONTRIBUTING.md) more of them.
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"description": "A tool to automatically fix PHP code style",
|
||||
"license": "MIT",
|
||||
"type": "application",
|
||||
"keywords": [
|
||||
"fixer",
|
||||
"standards",
|
||||
"static analysis",
|
||||
"static code analysis"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Dariusz Rumiński",
|
||||
"email": "dariusz.ruminski@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"ext-filter": "*",
|
||||
"ext-hash": "*",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"clue/ndjson-react": "^1.3",
|
||||
"composer/semver": "^3.4",
|
||||
"composer/xdebug-handler": "^3.0.5",
|
||||
"ergebnis/agent-detector": "^1.2",
|
||||
"fidry/cpu-core-counter": "^1.3",
|
||||
"react/child-process": "^0.6.6",
|
||||
"react/event-loop": "^1.5",
|
||||
"react/socket": "^1.16",
|
||||
"react/stream": "^1.4",
|
||||
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.0",
|
||||
"symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0",
|
||||
"symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
|
||||
"symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
|
||||
"symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
|
||||
"symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.37",
|
||||
"symfony/polyfill-php80": "^1.37",
|
||||
"symfony/polyfill-php81": "^1.37",
|
||||
"symfony/polyfill-php84": "^1.37",
|
||||
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0",
|
||||
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"facile-it/paraunit": "^1.3.1 || ^2.11.0",
|
||||
"infection/infection": "^0.32.7",
|
||||
"justinrainbow/json-schema": "^6.10.0",
|
||||
"keradus/cli-executor": "^2.3",
|
||||
"mikey179/vfsstream": "^1.6.12",
|
||||
"php-coveralls/php-coveralls": "^2.9.1",
|
||||
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8",
|
||||
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8",
|
||||
"phpunit/phpunit": "^9.6.35 || ^10.5.64 || ^11.5.56 || ^12.5.31 || ^13.0.6",
|
||||
"symfony/polyfill-php85": "^1.38",
|
||||
"symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.1",
|
||||
"symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "For handling output formats in XML",
|
||||
"ext-mbstring": "For handling non-UTF8 characters."
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpCsFixer\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/**/Internal/"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"PhpCsFixer\\PHPStan\\": "dev-tools/phpstan/src/",
|
||||
"PhpCsFixer\\Tests\\": "tests/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"tests/Fixtures/"
|
||||
]
|
||||
},
|
||||
"bin": [
|
||||
"php-cs-fixer"
|
||||
],
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"ergebnis/composer-normalize": true,
|
||||
"infection/extension-installer": false
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"@install-tools"
|
||||
],
|
||||
"analyse-deps": "@php dev-tools/vendor/bin/composer-dependency-analyser",
|
||||
"auto-review": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php ./vendor/bin/paraunit run --testsuite auto-review"
|
||||
],
|
||||
"cs:check": "@php php-cs-fixer check --verbose --diff",
|
||||
"cs:fix": "@php php-cs-fixer fix",
|
||||
"cs:fix:parallel": [
|
||||
"echo '⚠️ This script is deprecated! Utilise built-in parallelisation instead.';",
|
||||
"@cs:fix"
|
||||
],
|
||||
"docs": "@php dev-tools/php-cs-fixer-internal docs",
|
||||
"infection": "@test:mutation",
|
||||
"install-tools": [
|
||||
"./dev-tools/install.sh",
|
||||
"@composer install --working-dir=dev-tools"
|
||||
],
|
||||
"internal": "@php dev-tools/php-cs-fixer-internal",
|
||||
"mess-detector": "@php dev-tools/vendor/bin/phpmd . ansi dev-tools/mess-detector/phpmd.xml --exclude vendor/*,dev-tools/vendor/*,dev-tools/phpstan/*,tests/Fixtures/*",
|
||||
"normalize": [
|
||||
"@composer normalize --working-dir=dev-tools --dry-run ../composer.json",
|
||||
"@composer normalize --working-dir=dev-tools --dry-run composer.json"
|
||||
],
|
||||
"normalize:fix": [
|
||||
"@composer normalize --working-dir=dev-tools ../composer.json",
|
||||
"@composer normalize --working-dir=dev-tools composer.json"
|
||||
],
|
||||
"php-compatibility": "@php dev-tools/vendor/bin/phpcs -p --standard=dev-tools/php-compatibility/phpcs-php-compatibility.xml",
|
||||
"phpstan": "@php -d memory_limit=512M dev-tools/vendor/bin/phpstan analyse",
|
||||
"phpstan:baseline": [
|
||||
"@php -d memory_limit=512M dev-tools/vendor/bin/phpstan analyse --generate-baseline=./dev-tools/phpstan/baseline/_loader.php",
|
||||
"@php dev-tools/vendor/bin/split-phpstan-baseline ./dev-tools/phpstan/baseline/_loader.php --no-error-count"
|
||||
],
|
||||
"qa": "@quality-assurance",
|
||||
"quality-assurance": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@install-tools --quiet",
|
||||
"@self-check",
|
||||
"@static-analysis",
|
||||
"@test"
|
||||
],
|
||||
"sa": "@static-analysis",
|
||||
"self-check": [
|
||||
"./dev-tools/check_file_permissions.sh",
|
||||
"./dev-tools/check_trailing_spaces.sh",
|
||||
"./dev-tools/check_shell_scripts.sh",
|
||||
"./dev-tools/check_no_american_english.sh",
|
||||
"@composer dump-autoload --dry-run --optimize --strict-psr",
|
||||
"@normalize",
|
||||
"@analyse-deps",
|
||||
"@auto-review"
|
||||
],
|
||||
"static-analysis": [
|
||||
"@cs:check",
|
||||
"@phpstan",
|
||||
"@php-compatibility",
|
||||
"@mess-detector"
|
||||
],
|
||||
"test": "@test:all",
|
||||
"test:all": [
|
||||
"@test:unit",
|
||||
"@test:short-open-tag",
|
||||
"@test:integration"
|
||||
],
|
||||
"test:coverage": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@composer show facile-it/paraunit ^2 && (paraunit coverage --testsuite unit --pass-through=--exclude-group=covers-nothing --pass-through=--do-not-fail-on-empty-test-suite) || (paraunit coverage --testsuite unit --exclude-group covers-nothing)"
|
||||
],
|
||||
"test:integration": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php ./vendor/bin/paraunit run --testsuite integration"
|
||||
],
|
||||
"test:mutation": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"infection --threads=max --only-covering-test-cases --min-covered-msi=80"
|
||||
],
|
||||
"test:short-open-tag": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php -d short_open_tag=1 ./vendor/bin/phpunit --do-not-cache-result --testsuite short-open-tag"
|
||||
],
|
||||
"test:smoke": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php ./vendor/bin/paraunit run --testsuite smoke"
|
||||
],
|
||||
"test:unit": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php ./vendor/bin/paraunit run --testsuite unit"
|
||||
]
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"analyse-deps": "Analyse Composer dependencies",
|
||||
"auto-review": "Execute Auto-review",
|
||||
"cs:check": "Check coding standards",
|
||||
"cs:fix": "Fix coding standards",
|
||||
"cs:fix:parallel": "⚠️DEPRECATED! Use cs:fix with proper parallel config",
|
||||
"docs": "Regenerate docs",
|
||||
"infection": "Alias for 'test:mutation'",
|
||||
"install-tools": "Install DEV tools",
|
||||
"internal": "Run internal commands",
|
||||
"mess-detector": "Analyse code with Mess Detector",
|
||||
"normalize": "Check normalization for composer.json files",
|
||||
"normalize:fix": "Run normalization for composer.json files",
|
||||
"php-compatibility": "Check compatibility with all supported PHP versions",
|
||||
"phpstan": "Run PHPStan analysis",
|
||||
"phpstan:baseline": "Dump PHPStan baseline file - use only for updating, do not add new errors when possible",
|
||||
"post-autoload-dump": "Run additional tasks after installing/updating main dependencies",
|
||||
"qa": "Alias for 'quality-assurance'",
|
||||
"quality-assurance": "Run QA suite",
|
||||
"sa": "Alias for 'static-analysis'",
|
||||
"self-check": "Run set of self-checks ensuring repository's validity",
|
||||
"static-analysis": "Run static analysis",
|
||||
"test": "Alias for 'test:all'",
|
||||
"test:all": "Run Unit and Integration tests (but *NOT* Smoke tests)",
|
||||
"test:coverage": "Run tests that provide code coverage",
|
||||
"test:integration": "Run Integration tests",
|
||||
"test:mutation": "Run mutation tests",
|
||||
"test:short-open-tag": "Run tests with \"short_open_tag\" enabled",
|
||||
"test:smoke": "Run Smoke tests",
|
||||
"test:unit": "Run Unit tests"
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
use PhpCsFixer\Console\Application;
|
||||
|
||||
error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
|
||||
|
||||
set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
|
||||
if (0 !== ($severity & error_reporting())) {
|
||||
throw new \ErrorException($message, 0, $severity, $file, $line);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// check environment requirements
|
||||
(static function (): void {
|
||||
if (\PHP_VERSION_ID === (int) '80000') { // TODO use 8_00_00 once only PHP 7.4+ is supported by this entry file
|
||||
fwrite(\STDERR, "PHP CS Fixer is not able run on PHP 8.0.0 due to bug in PHP tokenizer (https://bugs.php.net/bug.php?id=80462).\n");
|
||||
fwrite(\STDERR, "Update PHP version to unblock execution.\n");
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// PHPStan knows our min PHP version, but we want to check the min version here
|
||||
// because entrypoint file allows wider PHP range than project itself
|
||||
// @phpstan-ignore smaller.alwaysFalse
|
||||
if (\PHP_VERSION_ID < (int) '70400') {
|
||||
fwrite(\STDERR, "PHP needs to be a minimum version of PHP 7.4.0.\n");
|
||||
fwrite(\STDERR, 'Current PHP version: '.\PHP_VERSION.".\n");
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// @TODO 4.0 cleanup
|
||||
if (false !== getenv('PHP_CS_FIXER_IGNORE_ENV')) {
|
||||
fwrite(\STDERR, "Setting PHP_CS_FIXER_IGNORE_ENV environment variable is deprecated and will be removed in 4.0, use unsupportedPhpVersionAllowed config instead.\n");
|
||||
}
|
||||
|
||||
foreach (['json', 'tokenizer'] as $extension) {
|
||||
if (!\extension_loaded($extension)) {
|
||||
fwrite(\STDERR, \sprintf("PHP extension ext-%s is missing from your system. Install or enable it.\n", $extension));
|
||||
|
||||
if (filter_var(getenv('PHP_CS_FIXER_IGNORE_ENV'), \FILTER_VALIDATE_BOOLEAN)) {
|
||||
fwrite(\STDERR, "Ignoring environment requirements because `PHP_CS_FIXER_IGNORE_ENV` is set. Execution may be unstable.\n");
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// load dependencies
|
||||
(static function (): void {
|
||||
$require = true;
|
||||
if (class_exists(\Phar::class)) {
|
||||
// Maybe this file is used as phar-stub? Let's try!
|
||||
try {
|
||||
\Phar::mapPhar('php-cs-fixer.phar');
|
||||
|
||||
/** @phpstan-ignore requireOnce.fileNotFound */
|
||||
require_once 'phar://php-cs-fixer.phar/vendor/autoload.php';
|
||||
|
||||
$require = false;
|
||||
} catch (\PharException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($require) {
|
||||
// OK, it's not, let give Composer autoloader a try!
|
||||
$possibleFiles = [__DIR__.'/../../autoload.php', __DIR__.'/../autoload.php', __DIR__.'/vendor/autoload.php'];
|
||||
$file = null;
|
||||
foreach ($possibleFiles as $possibleFile) {
|
||||
if (file_exists($possibleFile)) {
|
||||
$file = $possibleFile;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $file) {
|
||||
throw new \RuntimeException('Unable to locate autoload.php file.');
|
||||
}
|
||||
|
||||
require_once $file;
|
||||
}
|
||||
})();
|
||||
|
||||
// Restart if xdebug is loaded, unless the environment variable PHP_CS_FIXER_ALLOW_XDEBUG is set.
|
||||
$xdebug = new XdebugHandler('PHP_CS_FIXER');
|
||||
$xdebug->check();
|
||||
unset($xdebug);
|
||||
|
||||
$application = new Application();
|
||||
$application->run();
|
||||
|
||||
__HALT_COMPILER();
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
|
||||
return (new Config())
|
||||
->setRiskyAllowed(/*{{ IS_RISKY_ALLOWED }}*/)
|
||||
->setRules(/*{{ RULES }}*/)
|
||||
// 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config
|
||||
->setFinder(
|
||||
(new Finder())
|
||||
// 💡 root folder to check
|
||||
->in(__DIR__)
|
||||
// 💡 additional files, eg bin entry file
|
||||
// ->append([__DIR__.'/bin-entry-file'])
|
||||
// 💡 folders to exclude, if any
|
||||
// ->exclude([/* ... */])
|
||||
// 💡 path patterns to exclude, if any
|
||||
// ->notPath([/* ... */])
|
||||
// 💡 extra configs
|
||||
// ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode
|
||||
// ->ignoreVCS(true) // true by default
|
||||
)
|
||||
;
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Doctrine\Annotation\Tokens as DoctrineAnnotationTokens;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
|
||||
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
|
||||
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
|
||||
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
|
||||
use PhpCsFixer\Tokenizer\CT;
|
||||
use PhpCsFixer\Tokenizer\FCT;
|
||||
use PhpCsFixer\Tokenizer\Token;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
use PhpCsFixer\Tokenizer\TokensAnalyzer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @phpstan-type _AutogeneratedInputConfiguration array{
|
||||
* ignored_tags?: list<string>,
|
||||
* }
|
||||
* @phpstan-type _AutogeneratedComputedConfiguration array{
|
||||
* ignored_tags: list<string>,
|
||||
* }
|
||||
*
|
||||
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractDoctrineAnnotationFixer extends AbstractFixer implements ConfigurableFixerInterface
|
||||
{
|
||||
private const CLASS_MODIFIERS = [\T_ABSTRACT, \T_FINAL, FCT::T_READONLY];
|
||||
private const MODIFIER_KINDS = [\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_FINAL, \T_ABSTRACT, \T_NS_SEPARATOR, \T_STRING, CT::T_NULLABLE_TYPE, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
|
||||
|
||||
/**
|
||||
* @var array<int, array{classIndex: int, token: Token, type: string}>
|
||||
*/
|
||||
private array $classyElements;
|
||||
|
||||
public function isCandidate(Tokens $tokens): bool
|
||||
{
|
||||
return $tokens->isTokenKindFound(\T_DOC_COMMENT);
|
||||
}
|
||||
|
||||
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
|
||||
{
|
||||
// fetch indices one time, this is safe as we never add or remove a token during fixing
|
||||
$analyzer = new TokensAnalyzer($tokens);
|
||||
$this->classyElements = $analyzer->getClassyElements();
|
||||
|
||||
foreach ($tokens->findGivenKind(\T_DOC_COMMENT) as $index => $docCommentToken) {
|
||||
if (!$this->nextElementAcceptsDoctrineAnnotations($tokens, $index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$doctrineAnnotationTokens = DoctrineAnnotationTokens::createFromDocComment(
|
||||
$docCommentToken,
|
||||
$this->configuration['ignored_tags'], // @phpstan-ignore-line
|
||||
);
|
||||
|
||||
$this->fixAnnotations($doctrineAnnotationTokens);
|
||||
$tokens[$index] = new Token([\T_DOC_COMMENT, $doctrineAnnotationTokens->getCode()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes Doctrine annotations from the given PHPDoc style comment.
|
||||
*/
|
||||
abstract protected function fixAnnotations(DoctrineAnnotationTokens $doctrineAnnotationTokens): void;
|
||||
|
||||
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
|
||||
{
|
||||
return new FixerConfigurationResolver([
|
||||
(new FixerOptionBuilder('ignored_tags', 'List of tags that must not be treated as Doctrine Annotations.'))
|
||||
->setAllowedTypes(['string[]'])
|
||||
->setDefault([
|
||||
// PHPDocumentor 1
|
||||
'abstract',
|
||||
'access',
|
||||
'code',
|
||||
'deprec',
|
||||
'encode',
|
||||
'exception',
|
||||
'final',
|
||||
'ingroup',
|
||||
'inheritdoc',
|
||||
'inheritDoc',
|
||||
'magic',
|
||||
'name',
|
||||
'toc',
|
||||
'tutorial',
|
||||
'private',
|
||||
'static',
|
||||
'staticvar',
|
||||
'staticVar',
|
||||
'throw',
|
||||
|
||||
// PHPDocumentor 2
|
||||
'api',
|
||||
'author',
|
||||
'category',
|
||||
'copyright',
|
||||
'deprecated',
|
||||
'example',
|
||||
'filesource',
|
||||
'global',
|
||||
'ignore',
|
||||
'internal',
|
||||
'license',
|
||||
'link',
|
||||
'method',
|
||||
'package',
|
||||
'param',
|
||||
'property',
|
||||
'property-read',
|
||||
'property-write',
|
||||
'return',
|
||||
'see',
|
||||
'since',
|
||||
'source',
|
||||
'subpackage',
|
||||
'throws',
|
||||
'todo',
|
||||
'TODO',
|
||||
'usedBy',
|
||||
'uses',
|
||||
'var',
|
||||
'version',
|
||||
|
||||
// PHPUnit
|
||||
'after',
|
||||
'afterClass',
|
||||
'backupGlobals',
|
||||
'backupStaticAttributes',
|
||||
'before',
|
||||
'beforeClass',
|
||||
'codeCoverageIgnore',
|
||||
'codeCoverageIgnoreStart',
|
||||
'codeCoverageIgnoreEnd',
|
||||
'covers',
|
||||
'coversDefaultClass',
|
||||
'coversNothing',
|
||||
'dataProvider',
|
||||
'depends',
|
||||
'expectedException',
|
||||
'expectedExceptionCode',
|
||||
'expectedExceptionMessage',
|
||||
'expectedExceptionMessageRegExp',
|
||||
'group',
|
||||
'large',
|
||||
'medium',
|
||||
'preserveGlobalState',
|
||||
'requires',
|
||||
'runTestsInSeparateProcesses',
|
||||
'runInSeparateProcess',
|
||||
'small',
|
||||
'test',
|
||||
'testdox',
|
||||
'ticket',
|
||||
'uses',
|
||||
|
||||
// PHPCheckStyle
|
||||
'SuppressWarnings',
|
||||
|
||||
// PHPStorm
|
||||
'noinspection',
|
||||
|
||||
// PEAR
|
||||
'package_version',
|
||||
|
||||
// PlantUML
|
||||
'enduml',
|
||||
'startuml',
|
||||
|
||||
// Psalm
|
||||
'psalm',
|
||||
|
||||
// PHPStan
|
||||
'phpstan',
|
||||
'template',
|
||||
|
||||
// other
|
||||
'fix',
|
||||
'FIXME',
|
||||
'fixme',
|
||||
'override',
|
||||
])
|
||||
->getOption(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function nextElementAcceptsDoctrineAnnotations(Tokens $tokens, int $index): bool
|
||||
{
|
||||
do {
|
||||
$index = $tokens->getNextMeaningfulToken($index);
|
||||
|
||||
if (null === $index) {
|
||||
return false;
|
||||
}
|
||||
} while ($tokens[$index]->isGivenKind(self::CLASS_MODIFIERS));
|
||||
|
||||
if ($tokens[$index]->isGivenKind(\T_CLASS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
while ($tokens[$index]->isGivenKind(self::MODIFIER_KINDS)) {
|
||||
$index = $tokens->getNextMeaningfulToken($index);
|
||||
}
|
||||
|
||||
if (!isset($this->classyElements[$index])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $tokens[$this->classyElements[$index]['classIndex']]->isGivenKind(\T_CLASS); // interface, enums and traits cannot have doctrine annotations
|
||||
}
|
||||
}
|
||||
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\ConfigurationException\RequiredFixerConfigurationException;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractFixer implements FixerInterface
|
||||
{
|
||||
protected WhitespacesFixerConfig $whitespacesConfig;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private string $name;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$nameParts = explode('\\', static::class);
|
||||
$name = substr(end($nameParts), 0, -\strlen('Fixer'));
|
||||
$this->name = Utils::camelCaseToUnderscore($name);
|
||||
|
||||
if ($this instanceof ConfigurableFixerInterface) {
|
||||
try {
|
||||
$this->configure([]);
|
||||
} catch (RequiredFixerConfigurationException $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if ($this instanceof WhitespacesAwareFixerInterface) {
|
||||
$this->whitespacesConfig = $this->getDefaultWhitespacesFixerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
final public function fix(\SplFileInfo $file, Tokens $tokens): void
|
||||
{
|
||||
if ($this instanceof ConfigurableFixerInterface && property_exists($this, 'configuration') && null === $this->configuration) {
|
||||
throw new RequiredFixerConfigurationException($this->getName(), 'Configuration is required.');
|
||||
}
|
||||
|
||||
if (0 < $tokens->count() && $this->isCandidate($tokens) && $this->supports($file)) {
|
||||
$this->applyFix($file, $tokens);
|
||||
}
|
||||
}
|
||||
|
||||
public function isRisky(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function supports(\SplFileInfo $file): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setWhitespacesConfig(WhitespacesFixerConfig $config): void
|
||||
{
|
||||
if (!$this instanceof WhitespacesAwareFixerInterface) {
|
||||
throw new \LogicException('Cannot run method for class not implementing "PhpCsFixer\Fixer\WhitespacesAwareFixerInterface".');
|
||||
}
|
||||
|
||||
$this->whitespacesConfig = $config;
|
||||
}
|
||||
|
||||
abstract protected function applyFix(\SplFileInfo $file, Tokens $tokens): void;
|
||||
|
||||
private function getDefaultWhitespacesFixerConfig(): WhitespacesFixerConfig
|
||||
{
|
||||
static $defaultWhitespacesFixerConfig = null;
|
||||
|
||||
if (null === $defaultWhitespacesFixerConfig) {
|
||||
$defaultWhitespacesFixerConfig = new WhitespacesFixerConfig(' ', "\n");
|
||||
}
|
||||
|
||||
return $defaultWhitespacesFixerConfig;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractFopenFlagFixer extends AbstractFunctionReferenceFixer
|
||||
{
|
||||
public function isCandidate(Tokens $tokens): bool
|
||||
{
|
||||
return $tokens->isAllTokenKindsFound([\T_STRING, \T_CONSTANT_ENCAPSED_STRING]);
|
||||
}
|
||||
|
||||
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
|
||||
{
|
||||
$argumentsAnalyzer = new ArgumentsAnalyzer();
|
||||
|
||||
$index = 0;
|
||||
$end = $tokens->count() - 1;
|
||||
while (true) {
|
||||
$candidate = $this->find('fopen', $tokens, $index, $end);
|
||||
|
||||
if (null === $candidate) {
|
||||
break;
|
||||
}
|
||||
|
||||
$index = $candidate[1]; // proceed to '(' of `fopen`
|
||||
|
||||
// fetch arguments
|
||||
$arguments = $argumentsAnalyzer->getArguments(
|
||||
$tokens,
|
||||
$index,
|
||||
$candidate[2],
|
||||
);
|
||||
|
||||
$argumentsCount = \count($arguments); // argument count sanity check
|
||||
|
||||
if ($argumentsCount < 2 || $argumentsCount > 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get second argument index
|
||||
$argumentKeys = array_keys($arguments);
|
||||
\assert(isset($argumentKeys[1]));
|
||||
$argumentStartIndex = $argumentKeys[1];
|
||||
|
||||
\assert(isset($arguments[$argumentStartIndex]));
|
||||
$this->fixFopenFlagToken(
|
||||
$tokens,
|
||||
$argumentStartIndex,
|
||||
$arguments[$argumentStartIndex],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function fixFopenFlagToken(Tokens $tokens, int $argumentStartIndex, int $argumentEndIndex): void;
|
||||
|
||||
protected function isValidModeString(string $mode): bool
|
||||
{
|
||||
$modeLength = \strlen($mode);
|
||||
if ($modeLength < 1 || $modeLength > 13) { // 13 === length 'r+w+a+x+c+etb'
|
||||
return false;
|
||||
}
|
||||
|
||||
$validFlags = [
|
||||
'a' => true,
|
||||
'b' => true,
|
||||
'c' => true,
|
||||
'e' => true,
|
||||
'r' => true,
|
||||
't' => true,
|
||||
'w' => true,
|
||||
'x' => true,
|
||||
];
|
||||
|
||||
if (!isset($validFlags[$mode[0]])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($validFlags[$mode[0]]);
|
||||
|
||||
for ($i = 1; $i < $modeLength; ++$i) {
|
||||
if (isset($validFlags[$mode[$i]])) {
|
||||
unset($validFlags[$mode[$i]]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('+' !== $mode[$i]
|
||||
|| (
|
||||
'a' !== $mode[$i - 1] // 'a+','c+','r+','w+','x+'
|
||||
&& 'c' !== $mode[$i - 1]
|
||||
&& 'r' !== $mode[$i - 1]
|
||||
&& 'w' !== $mode[$i - 1]
|
||||
&& 'x' !== $mode[$i - 1]
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @author Vladimir Reznichenko <kalessil@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractFunctionReferenceFixer extends AbstractFixer
|
||||
{
|
||||
private ?FunctionsAnalyzer $functionsAnalyzer = null;
|
||||
|
||||
public function isCandidate(Tokens $tokens): bool
|
||||
{
|
||||
return $tokens->isTokenKindFound(\T_STRING);
|
||||
}
|
||||
|
||||
public function isRisky(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up Tokens sequence for suitable candidates and delivers boundaries information,
|
||||
* which can be supplied by other methods in this abstract class.
|
||||
*
|
||||
* @return ?array{int, int, int} returns $functionName, $openParenthesis, $closeParenthesis packed into array
|
||||
*/
|
||||
protected function find(string $functionNameToSearch, Tokens $tokens, int $start = 0, ?int $end = null): ?array
|
||||
{
|
||||
if (null === $this->functionsAnalyzer) {
|
||||
$this->functionsAnalyzer = new FunctionsAnalyzer();
|
||||
}
|
||||
|
||||
// make interface consistent with findSequence
|
||||
$end ??= $tokens->count();
|
||||
|
||||
// find raw sequence which we can analyse for context
|
||||
$candidateSequence = [[\T_STRING, $functionNameToSearch], '('];
|
||||
$matches = $tokens->findSequence($candidateSequence, $start, $end, false);
|
||||
|
||||
if (null === $matches) {
|
||||
return null; // not found, simply return without further attempts
|
||||
}
|
||||
|
||||
// translate results for humans
|
||||
\assert(isset(array_keys($matches)[1]));
|
||||
[$functionName, $openParenthesis] = array_keys($matches);
|
||||
|
||||
if (!$this->functionsAnalyzer->isGlobalFunctionCall($tokens, $functionName)) {
|
||||
return $this->find($functionNameToSearch, $tokens, $openParenthesis, $end);
|
||||
}
|
||||
|
||||
return [$functionName, $openParenthesis, $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS, $openParenthesis)];
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractNoUselessElseFixer extends AbstractFixer
|
||||
{
|
||||
public function getPriority(): int
|
||||
{
|
||||
// should be run before NoWhitespaceInBlankLineFixer, NoExtraBlankLinesFixer, BracesFixer and after NoEmptyStatementFixer.
|
||||
return 39;
|
||||
}
|
||||
|
||||
protected function isSuperfluousElse(Tokens $tokens, int $index): bool
|
||||
{
|
||||
$previousBlockStart = $index;
|
||||
|
||||
do {
|
||||
// Check if all 'if', 'else if ' and 'elseif' blocks above this 'else' always end,
|
||||
// if so this 'else' is overcomplete.
|
||||
[$previousBlockStart, $previousBlockEnd] = $this->getPreviousBlock($tokens, $previousBlockStart);
|
||||
|
||||
// short 'if' detection
|
||||
$previous = $previousBlockEnd;
|
||||
if ($tokens[$previous]->equals('}')) {
|
||||
$previous = $tokens->getPrevMeaningfulToken($previous);
|
||||
}
|
||||
|
||||
if (
|
||||
!$tokens[$previous]->equals(';') // 'if' block doesn't end with semicolon, keep 'else'
|
||||
|| $tokens[$tokens->getPrevMeaningfulToken($previous)]->equals('{') // empty 'if' block, keep 'else'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$candidateIndex = $tokens->getPrevTokenOfKind(
|
||||
$previous,
|
||||
[
|
||||
';',
|
||||
[\T_BREAK],
|
||||
[\T_CLOSE_TAG],
|
||||
[\T_CONTINUE],
|
||||
[\T_EXIT],
|
||||
[\T_GOTO],
|
||||
[\T_IF],
|
||||
[\T_RETURN],
|
||||
[\T_THROW],
|
||||
],
|
||||
);
|
||||
|
||||
if (null === $candidateIndex || $tokens[$candidateIndex]->equalsAny([';', [\T_CLOSE_TAG], [\T_IF]])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($tokens[$candidateIndex]->isGivenKind(\T_THROW)) {
|
||||
$previousIndex = $tokens->getPrevMeaningfulToken($candidateIndex);
|
||||
|
||||
if (!$tokens[$previousIndex]->equalsAny([';', '{'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isInConditional($tokens, $candidateIndex, $previousBlockStart)
|
||||
|| $this->isInConditionWithoutBraces($tokens, $candidateIndex, $previousBlockStart)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// implicit continue, i.e. delete candidate
|
||||
} while (!$tokens[$previousBlockStart]->isGivenKind(\T_IF));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first and last token index of the previous block.
|
||||
*
|
||||
* [0] First is either T_IF, T_ELSE or T_ELSEIF
|
||||
* [1] Last is either '}' or ';' / T_CLOSE_TAG for short notation blocks
|
||||
*
|
||||
* @param int $index T_IF, T_ELSE, T_ELSEIF
|
||||
*
|
||||
* @return array{int, int}
|
||||
*/
|
||||
private function getPreviousBlock(Tokens $tokens, int $index): array
|
||||
{
|
||||
$close = $previous = $tokens->getPrevMeaningfulToken($index);
|
||||
// short 'if' detection
|
||||
if ($tokens[$close]->equals('}')) {
|
||||
$previous = $tokens->findBlockStart(Tokens::BLOCK_TYPE_BRACE, $close);
|
||||
}
|
||||
|
||||
$open = $tokens->getPrevTokenOfKind($previous, [[\T_IF], [\T_ELSE], [\T_ELSEIF]]);
|
||||
if ($tokens[$open]->isGivenKind(\T_IF)) {
|
||||
$elseCandidate = $tokens->getPrevMeaningfulToken($open);
|
||||
if ($tokens[$elseCandidate]->isGivenKind(\T_ELSE)) {
|
||||
$open = $elseCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return [$open, $close];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $index Index of the token to check
|
||||
* @param int $lowerLimitIndex Lower limit index. Since the token to check will always be in a conditional we must stop checking at this index
|
||||
*/
|
||||
private function isInConditional(Tokens $tokens, int $index, int $lowerLimitIndex): bool
|
||||
{
|
||||
$candidateIndex = $tokens->getPrevTokenOfKind($index, [')', ';', ':']);
|
||||
if ($tokens[$candidateIndex]->equals(':')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$tokens[$candidateIndex]->equals(')')) {
|
||||
return false; // token is ';' or close tag
|
||||
}
|
||||
|
||||
// token is always ')' here.
|
||||
// If it is part of the condition the token is always in, return false.
|
||||
// If it is not it is a nested condition so return true
|
||||
$open = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS, $candidateIndex);
|
||||
|
||||
return $tokens->getPrevMeaningfulToken($open) > $lowerLimitIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* For internal use only, as it is not perfect.
|
||||
*
|
||||
* Returns if the token at given index is part of an if/elseif/else statement
|
||||
* without {}. Assumes not passing the last `;`/close tag of the statement, not
|
||||
* out of range index, etc.
|
||||
*
|
||||
* @param int $index Index of the token to check
|
||||
*/
|
||||
private function isInConditionWithoutBraces(Tokens $tokens, int $index, int $lowerLimitIndex): bool
|
||||
{
|
||||
do {
|
||||
if ($tokens[$index]->isComment() || $tokens[$index]->isWhitespace()) {
|
||||
$index = $tokens->getPrevMeaningfulToken($index);
|
||||
}
|
||||
|
||||
$token = $tokens[$index];
|
||||
if ($token->isGivenKind([\T_IF, \T_ELSEIF, \T_ELSE])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($token->equals(';')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($token->equals('{')) {
|
||||
$index = $tokens->getPrevMeaningfulToken($index);
|
||||
|
||||
// OK if belongs to: for, do, while, foreach
|
||||
// Not OK if belongs to: if, else, elseif
|
||||
if ($tokens[$index]->isGivenKind(\T_DO)) {
|
||||
--$index;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$tokens[$index]->equals(')')) {
|
||||
return false; // like `else {`
|
||||
}
|
||||
|
||||
$index = $tokens->findBlockStart(
|
||||
Tokens::BLOCK_TYPE_PARENTHESIS,
|
||||
$index,
|
||||
);
|
||||
|
||||
$index = $tokens->getPrevMeaningfulToken($index);
|
||||
if ($tokens[$index]->isGivenKind([\T_IF, \T_ELSEIF])) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($token->equals(')')) {
|
||||
$type = Tokens::detectBlockType($token);
|
||||
$index = $tokens->findBlockStart(
|
||||
$type['type'],
|
||||
$index,
|
||||
);
|
||||
|
||||
$index = $tokens->getPrevMeaningfulToken($index);
|
||||
} else {
|
||||
--$index;
|
||||
}
|
||||
} while ($index > $lowerLimitIndex);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\DocBlock\Annotation;
|
||||
use PhpCsFixer\DocBlock\DocBlock;
|
||||
use PhpCsFixer\DocBlock\TypeExpression;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
|
||||
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
|
||||
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
|
||||
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
|
||||
use PhpCsFixer\Tokenizer\CT;
|
||||
use PhpCsFixer\Tokenizer\Token;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @phpstan-type _CommonTypeInfo array{commonType: string, isNullable: bool}
|
||||
* @phpstan-type _AutogeneratedInputConfiguration array{
|
||||
* scalar_types?: bool,
|
||||
* types_map?: array<string, string>,
|
||||
* union_types?: bool,
|
||||
* }
|
||||
* @phpstan-type _AutogeneratedComputedConfiguration array{
|
||||
* scalar_types: bool,
|
||||
* types_map: array<string, string>,
|
||||
* union_types: bool,
|
||||
* }
|
||||
*
|
||||
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer implements ConfigurableFixerInterface
|
||||
{
|
||||
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
|
||||
use ConfigurableFixerTrait;
|
||||
|
||||
private const REGEX_CLASS = '(?:\\\?+'.TypeExpression::REGEX_IDENTIFIER
|
||||
.'(\\\\'.TypeExpression::REGEX_IDENTIFIER.')*+)';
|
||||
|
||||
/**
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $versionSpecificTypes = [
|
||||
'void' => 7_01_00,
|
||||
'iterable' => 7_01_00,
|
||||
'object' => 7_02_00,
|
||||
'mixed' => 8_00_00,
|
||||
'never' => 8_01_00,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $scalarTypes = [
|
||||
'bool' => true,
|
||||
'float' => true,
|
||||
'int' => true,
|
||||
'string' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private static array $syntaxValidationCache = [];
|
||||
|
||||
public function isRisky(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract protected function isSkippedType(string $type): bool;
|
||||
|
||||
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
|
||||
{
|
||||
return new FixerConfigurationResolver([
|
||||
(new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.'))
|
||||
->setAllowedTypes(['bool'])
|
||||
->setDefault(true)
|
||||
->getOption(),
|
||||
(new FixerOptionBuilder('union_types', 'Fix also union types; turned on by default on PHP >= 8.0.0.'))
|
||||
->setAllowedTypes(['bool'])
|
||||
->setDefault(\PHP_VERSION_ID >= 8_00_00)
|
||||
->getOption(),
|
||||
(new FixerOptionBuilder('types_map', 'Map of custom types, e.g. template types from PHPStan.'))
|
||||
->setAllowedTypes(['array<string, string>'])
|
||||
->setDefault([])
|
||||
->getOption(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $index The index of the function token
|
||||
*/
|
||||
protected function findFunctionDocComment(Tokens $tokens, int $index): ?int
|
||||
{
|
||||
do {
|
||||
$index = $tokens->getPrevNonWhitespace($index);
|
||||
} while ($tokens[$index]->isGivenKind([
|
||||
\T_COMMENT,
|
||||
\T_ABSTRACT,
|
||||
\T_FINAL,
|
||||
\T_PRIVATE,
|
||||
\T_PROTECTED,
|
||||
\T_PUBLIC,
|
||||
\T_STATIC,
|
||||
]));
|
||||
|
||||
if ($tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
|
||||
return $index;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Annotation>
|
||||
*/
|
||||
protected function getAnnotationsFromDocComment(string $name, Tokens $tokens, int $docCommentIndex): array
|
||||
{
|
||||
$namespacesAnalyzer = new NamespacesAnalyzer();
|
||||
$namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $docCommentIndex);
|
||||
|
||||
$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
|
||||
$namespaceUses = $namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);
|
||||
|
||||
$doc = new DocBlock(
|
||||
$tokens[$docCommentIndex]->getContent(),
|
||||
$namespace,
|
||||
$namespaceUses,
|
||||
);
|
||||
|
||||
return $doc->getAnnotationsOfType($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Token>
|
||||
*/
|
||||
protected function createTypeDeclarationTokens(string $type, bool $isNullable): array
|
||||
{
|
||||
$newTokens = [];
|
||||
|
||||
if (true === $isNullable && 'mixed' !== $type) {
|
||||
$newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']);
|
||||
}
|
||||
|
||||
$newTokens = array_merge(
|
||||
$newTokens,
|
||||
$this->createTokensFromRawType($type)->toArray(),
|
||||
);
|
||||
|
||||
// 'scalar's, 'void', 'iterable' and 'object' must be unqualified
|
||||
foreach ($newTokens as $i => $token) {
|
||||
if ($token->isGivenKind(\T_STRING)) {
|
||||
$typeUnqualified = $token->getContent();
|
||||
|
||||
if (
|
||||
(isset($this->scalarTypes[$typeUnqualified]) || isset($this->versionSpecificTypes[$typeUnqualified]))
|
||||
&& isset($newTokens[$i - 1])
|
||||
&& '\\' === $newTokens[$i - 1]->getContent()
|
||||
) {
|
||||
unset($newTokens[$i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($newTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each fixer inheriting from this class must define a way of creating token collection representing type
|
||||
* gathered from phpDoc, e.g. `Foo|Bar` should be transformed into 3 tokens (`Foo`, `|` and `Bar`).
|
||||
* This can't be standardised, because some types may be allowed in one place, and invalid in others.
|
||||
*
|
||||
* @param string $type Type determined (and simplified) from phpDoc
|
||||
*/
|
||||
abstract protected function createTokensFromRawType(string $type): Tokens;
|
||||
|
||||
/**
|
||||
* @return ?_CommonTypeInfo
|
||||
*/
|
||||
protected function getCommonTypeInfo(TypeExpression $typesExpression, bool $isReturnType): ?array
|
||||
{
|
||||
$commonType = $typesExpression->getCommonType();
|
||||
$isNullable = $typesExpression->allowsNull();
|
||||
|
||||
if (null === $commonType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($isNullable && 'void' === $commonType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('static' === $commonType && (!$isReturnType || \PHP_VERSION_ID < 8_00_00)) {
|
||||
$commonType = 'self';
|
||||
}
|
||||
|
||||
if ($this->isSkippedType($commonType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\array_key_exists($commonType, $this->configuration['types_map'])) {
|
||||
$commonType = $this->configuration['types_map'][$commonType];
|
||||
}
|
||||
|
||||
if (isset($this->versionSpecificTypes[$commonType]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$commonType]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($this->scalarTypes[$commonType])) {
|
||||
if (false === $this->configuration['scalar_types']) {
|
||||
return null;
|
||||
}
|
||||
} elseif (!Preg::match('/^'.self::REGEX_CLASS.'$/', $commonType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['commonType' => $commonType, 'isNullable' => $isNullable];
|
||||
}
|
||||
|
||||
protected function getUnionTypes(TypeExpression $typesExpression, bool $isReturnType): ?string
|
||||
{
|
||||
if (\PHP_VERSION_ID < 8_00_00) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$typesExpression->isUnionType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (false === $this->configuration['union_types']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$types = $typesExpression->getTypes();
|
||||
$isNullable = $typesExpression->allowsNull();
|
||||
$unionTypes = [];
|
||||
$containsOtherThanIterableType = false;
|
||||
$containsOtherThanEmptyType = false;
|
||||
|
||||
foreach ($types as $type) {
|
||||
if ('null' === $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isSkippedType($type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($this->versionSpecificTypes[$type]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$type]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$typeExpression = new TypeExpression($type, null, []);
|
||||
$commonTypeInfo = $this->getCommonTypeInfo($typeExpression, $isReturnType);
|
||||
|
||||
if (null === $commonTypeInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$commonType = $commonTypeInfo['commonType'];
|
||||
|
||||
if (!$containsOtherThanIterableType && !\in_array($commonType, ['array', \Traversable::class, 'iterable'], true)) {
|
||||
$containsOtherThanIterableType = true;
|
||||
}
|
||||
if ($isReturnType && !$containsOtherThanEmptyType && !\in_array($commonType, ['null', 'void', 'never'], true)) {
|
||||
$containsOtherThanEmptyType = true;
|
||||
}
|
||||
|
||||
if (!$isNullable && $commonTypeInfo['isNullable']) {
|
||||
$isNullable = true;
|
||||
}
|
||||
|
||||
$unionTypes[] = $commonType;
|
||||
}
|
||||
|
||||
if (!$containsOtherThanIterableType) {
|
||||
return null;
|
||||
}
|
||||
if ($isReturnType && !$containsOtherThanEmptyType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($isNullable) {
|
||||
$unionTypes[] = 'null';
|
||||
}
|
||||
|
||||
return implode($typesExpression->getTypesGlue(), array_unique($unionTypes));
|
||||
}
|
||||
|
||||
final protected function isValidSyntax(string $code): bool
|
||||
{
|
||||
if (!isset(self::$syntaxValidationCache[$code])) {
|
||||
try {
|
||||
Tokens::fromCode($code);
|
||||
self::$syntaxValidationCache[$code] = true;
|
||||
} catch (\ParseError $e) {
|
||||
self::$syntaxValidationCache[$code] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$syntaxValidationCache[$code];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
final protected static function getTypesToExclude(string $content): array
|
||||
{
|
||||
$typesToExclude = [];
|
||||
|
||||
$docBlock = new DocBlock($content);
|
||||
|
||||
foreach ($docBlock->getAnnotationsOfType(['phpstan-type', 'psalm-type']) as $annotation) {
|
||||
$typesToExclude[] = $annotation->getTypeExpression()->toString();
|
||||
}
|
||||
|
||||
foreach ($docBlock->getAnnotationsOfType(['phpstan-import-type', 'psalm-import-type']) as $annotation) {
|
||||
$content = trim($annotation->getContent());
|
||||
if (Preg::match('/\bas\s+('.TypeExpression::REGEX_IDENTIFIER.')$/', $content, $matches)) {
|
||||
$typesToExclude[] = $matches[1];
|
||||
|
||||
continue;
|
||||
}
|
||||
$typesToExclude[] = $annotation->getTypeExpression()->toString();
|
||||
}
|
||||
|
||||
return $typesToExclude;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\DocBlock\Annotation;
|
||||
use PhpCsFixer\DocBlock\DocBlock;
|
||||
use PhpCsFixer\DocBlock\TypeExpression;
|
||||
use PhpCsFixer\Tokenizer\Token;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* This abstract fixer provides a base for fixers to fix types in PHPDoc.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractPhpdocTypesFixer extends AbstractFixer
|
||||
{
|
||||
public function isCandidate(Tokens $tokens): bool
|
||||
{
|
||||
return $tokens->isTokenKindFound(\T_DOC_COMMENT);
|
||||
}
|
||||
|
||||
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
|
||||
{
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (!$token->isGivenKind(\T_DOC_COMMENT)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DocBlock($token->getContent());
|
||||
$annotations = $doc->getAnnotationsOfType(Annotation::TAGS_WITH_TYPES);
|
||||
|
||||
if (0 === \count($annotations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($annotations as $annotation) {
|
||||
$this->fixType($annotation);
|
||||
}
|
||||
|
||||
$tokens[$index] = new Token([\T_DOC_COMMENT, $doc->getContent()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually normalize the given type.
|
||||
*/
|
||||
abstract protected function normalize(string $type): string;
|
||||
|
||||
/**
|
||||
* Fix the type at the given line.
|
||||
*
|
||||
* We must be super careful not to modify parts of words.
|
||||
*
|
||||
* This will be nicely handled behind the scenes for us by the annotation class.
|
||||
*/
|
||||
private function fixType(Annotation $annotation): void
|
||||
{
|
||||
$typeExpression = $annotation->getTypeExpression();
|
||||
|
||||
if (null === $typeExpression) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newTypeExpression = $typeExpression->mapTypes(function (TypeExpression $type) {
|
||||
if (!$type->isCompositeType()) {
|
||||
$value = $this->normalize($type->toString());
|
||||
|
||||
return new TypeExpression($value, null, []);
|
||||
}
|
||||
|
||||
return $type;
|
||||
});
|
||||
|
||||
$annotation->setTypes([$newTypeExpression->toString()]);
|
||||
}
|
||||
}
|
||||
omsorgWeb/mitarbeiter-app/api-client-php/vendor/friendsofphp/php-cs-fixer/src/AbstractProxyFixer.php
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
abstract class AbstractProxyFixer extends AbstractFixer
|
||||
{
|
||||
/**
|
||||
* @var non-empty-array<string, FixerInterface>
|
||||
*/
|
||||
protected array $proxyFixers;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$proxyFixers = [];
|
||||
foreach (Utils::sortFixers($this->createProxyFixers()) as $proxyFixer) {
|
||||
$proxyFixers[$proxyFixer->getName()] = $proxyFixer;
|
||||
}
|
||||
$this->proxyFixers = $proxyFixers;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function isCandidate(Tokens $tokens): bool
|
||||
{
|
||||
foreach ($this->proxyFixers as $fixer) {
|
||||
if ($fixer->isCandidate($tokens)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isRisky(): bool
|
||||
{
|
||||
foreach ($this->proxyFixers as $fixer) {
|
||||
if ($fixer->isRisky()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
if (\count($this->proxyFixers) > 1) {
|
||||
throw new \LogicException('You need to override this method to provide the priority of combined fixers.');
|
||||
}
|
||||
|
||||
return reset($this->proxyFixers)->getPriority();
|
||||
}
|
||||
|
||||
public function supports(\SplFileInfo $file): bool
|
||||
{
|
||||
foreach ($this->proxyFixers as $fixer) {
|
||||
if ($fixer->supports($file)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setWhitespacesConfig(WhitespacesFixerConfig $config): void
|
||||
{
|
||||
parent::setWhitespacesConfig($config);
|
||||
|
||||
foreach ($this->proxyFixers as $fixer) {
|
||||
if ($fixer instanceof WhitespacesAwareFixerInterface) {
|
||||
$fixer->setWhitespacesConfig($config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
|
||||
{
|
||||
foreach ($this->proxyFixers as $fixer) {
|
||||
$fixer->fix($file, $tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<FixerInterface>
|
||||
*/
|
||||
abstract protected function createProxyFixers(): array;
|
||||
}
|
||||
Vendored
+154
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
use PhpCsFixer\Config\NullRuleCustomisationPolicy;
|
||||
use PhpCsFixer\Utils;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Cache implements CacheInterface
|
||||
{
|
||||
private SignatureInterface $signature;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $hashes = [];
|
||||
|
||||
public function __construct(SignatureInterface $signature)
|
||||
{
|
||||
$this->signature = $signature;
|
||||
}
|
||||
|
||||
public function getSignature(): SignatureInterface
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
public function has(string $file): bool
|
||||
{
|
||||
return \array_key_exists($file, $this->hashes);
|
||||
}
|
||||
|
||||
public function get(string $file): ?string
|
||||
{
|
||||
return $this->hashes[$file] ?? null;
|
||||
}
|
||||
|
||||
public function set(string $file, string $hash): void
|
||||
{
|
||||
$this->hashes[$file] = $hash;
|
||||
}
|
||||
|
||||
public function clear(string $file): void
|
||||
{
|
||||
unset($this->hashes[$file]);
|
||||
}
|
||||
|
||||
public function toJson(): string
|
||||
{
|
||||
try {
|
||||
return json_encode(
|
||||
[
|
||||
'php' => $this->getSignature()->getPhpVersion(),
|
||||
'version' => $this->getSignature()->getFixerVersion(),
|
||||
'indent' => $this->getSignature()->getIndent(),
|
||||
'lineEnding' => $this->getSignature()->getLineEnding(),
|
||||
'rules' => $this->getSignature()->getRules(),
|
||||
'ruleCustomisationPolicyVersion' => $this->getSignature()->getRuleCustomisationPolicyVersion(),
|
||||
'hashes' => $this->hashes,
|
||||
],
|
||||
\JSON_THROW_ON_ERROR,
|
||||
);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \UnexpectedValueException(\sprintf(
|
||||
'Cannot encode cache signature to JSON, error: "%s". If you have non-UTF8 chars in your signature, like in license for `header_comment`, consider enabling `ext-mbstring` or install `symfony/polyfill-mbstring`.',
|
||||
$e->getMessage(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function fromJson(string $json): self
|
||||
{
|
||||
try {
|
||||
$data = json_decode($json, true, 512, \JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'Value needs to be a valid JSON string, got "%s", error: "%s".',
|
||||
$json,
|
||||
$e->getMessage(),
|
||||
));
|
||||
}
|
||||
|
||||
$requiredKeys = [
|
||||
'php',
|
||||
'version',
|
||||
'indent',
|
||||
'lineEnding',
|
||||
'rules',
|
||||
// 'ruleCustomisationPolicyVersion', // @TODO v4: require me
|
||||
'hashes',
|
||||
];
|
||||
|
||||
$missingKeys = array_diff_key(array_flip($requiredKeys), $data);
|
||||
|
||||
if (\count($missingKeys) > 0) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'JSON data is missing keys %s',
|
||||
Utils::naturalLanguageJoin(array_keys($missingKeys)),
|
||||
));
|
||||
}
|
||||
|
||||
$signature = new Signature(
|
||||
$data['php'],
|
||||
$data['version'],
|
||||
$data['indent'],
|
||||
$data['lineEnding'],
|
||||
$data['rules'],
|
||||
$data['ruleCustomisationPolicyVersion'] ?? NullRuleCustomisationPolicy::VERSION_FOR_CACHE,
|
||||
);
|
||||
|
||||
$cache = new self($signature);
|
||||
|
||||
// before v3.11.1 the hashes were crc32 encoded and saved as integers
|
||||
// @TODO v4: remove the to string cast/array_map
|
||||
$cache->hashes = array_map(static fn ($v): string => \is_int($v) ? (string) $v : $v, $data['hashes']);
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function backfillHashes(self $oldCache): bool
|
||||
{
|
||||
if (!$this->getSignature()->equals($oldCache->getSignature())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->hashes = array_merge($oldCache->hashes, $this->hashes);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface CacheInterface
|
||||
{
|
||||
public function getSignature(): SignatureInterface;
|
||||
|
||||
public function has(string $file): bool;
|
||||
|
||||
public function get(string $file): ?string;
|
||||
|
||||
public function set(string $file, string $hash): void;
|
||||
|
||||
public function clear(string $file): void;
|
||||
|
||||
public function toJson(): string;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface CacheManagerInterface
|
||||
{
|
||||
public function needFixing(string $file, string $fileContent): bool;
|
||||
|
||||
public function setFile(string $file, string $fileContent): void;
|
||||
|
||||
public function setFileHash(string $file, string $hash): void;
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Directory implements DirectoryInterface
|
||||
{
|
||||
private string $directoryName;
|
||||
|
||||
public function __construct(string $directoryName)
|
||||
{
|
||||
$this->directoryName = $directoryName;
|
||||
}
|
||||
|
||||
public function getRelativePathTo(string $file): string
|
||||
{
|
||||
$file = $this->normalizePath($file);
|
||||
|
||||
if (
|
||||
'' === $this->directoryName
|
||||
|| !str_starts_with(strtolower($file), strtolower($this->directoryName.\DIRECTORY_SEPARATOR))
|
||||
) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return substr($file, \strlen($this->directoryName) + 1);
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
return str_replace(['\\', '/'], \DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface DirectoryInterface
|
||||
{
|
||||
public function getRelativePathTo(string $file): string;
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
use PhpCsFixer\Hasher;
|
||||
|
||||
/**
|
||||
* Class supports caching information about state of fixing files.
|
||||
*
|
||||
* Cache is supported only for phar version and version installed via composer.
|
||||
*
|
||||
* File will be processed by PHP CS Fixer only if any of the following conditions is fulfilled:
|
||||
* - cache is corrupt
|
||||
* - fixer version changed
|
||||
* - rules changed
|
||||
* - file is new
|
||||
* - file changed
|
||||
*
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class FileCacheManager implements CacheManagerInterface
|
||||
{
|
||||
public const WRITE_FREQUENCY = 10;
|
||||
|
||||
private FileHandlerInterface $handler;
|
||||
|
||||
private SignatureInterface $signature;
|
||||
|
||||
private bool $isDryRun;
|
||||
|
||||
private DirectoryInterface $cacheDirectory;
|
||||
|
||||
private int $writeCounter = 0;
|
||||
|
||||
private bool $signatureWasUpdated = false;
|
||||
|
||||
private CacheInterface $cache;
|
||||
|
||||
public function __construct(
|
||||
FileHandlerInterface $handler,
|
||||
SignatureInterface $signature,
|
||||
bool $isDryRun = false,
|
||||
?DirectoryInterface $cacheDirectory = null
|
||||
) {
|
||||
$this->handler = $handler;
|
||||
$this->signature = $signature;
|
||||
$this->isDryRun = $isDryRun;
|
||||
$this->cacheDirectory = $cacheDirectory ?? new Directory('');
|
||||
|
||||
$this->readCache();
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (true === $this->signatureWasUpdated || 0 !== $this->writeCounter) {
|
||||
$this->writeCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is not intended to be serialized,
|
||||
* and cannot be deserialized (see __wakeup method).
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the deserialization of the class to prevent attacker executing
|
||||
* code by leveraging the __destruct method.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function needFixing(string $file, string $fileContent): bool
|
||||
{
|
||||
$file = $this->cacheDirectory->getRelativePathTo($file);
|
||||
|
||||
return !$this->cache->has($file) || $this->cache->get($file) !== $this->calcHash($fileContent);
|
||||
}
|
||||
|
||||
public function setFile(string $file, string $fileContent): void
|
||||
{
|
||||
$this->setFileHash($file, $this->calcHash($fileContent));
|
||||
}
|
||||
|
||||
public function setFileHash(string $file, string $hash): void
|
||||
{
|
||||
$file = $this->cacheDirectory->getRelativePathTo($file);
|
||||
|
||||
if ($this->isDryRun && $this->cache->has($file) && $this->cache->get($file) !== $hash) {
|
||||
$this->cache->clear($file);
|
||||
} else {
|
||||
$this->cache->set($file, $hash);
|
||||
}
|
||||
|
||||
if (self::WRITE_FREQUENCY === ++$this->writeCounter) {
|
||||
$this->writeCounter = 0;
|
||||
$this->writeCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function readCache(): void
|
||||
{
|
||||
$cache = $this->handler->read();
|
||||
|
||||
if (null === $cache || !$this->signature->equals($cache->getSignature())) {
|
||||
$cache = new Cache($this->signature);
|
||||
$this->signatureWasUpdated = true;
|
||||
}
|
||||
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
private function writeCache(): void
|
||||
{
|
||||
$this->handler->write($this->cache);
|
||||
}
|
||||
|
||||
private function calcHash(string $content): string
|
||||
{
|
||||
return Hasher::calculate($content);
|
||||
}
|
||||
}
|
||||
Vendored
+186
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class FileHandler implements FileHandlerInterface
|
||||
{
|
||||
private \SplFileInfo $fileInfo;
|
||||
|
||||
private int $fileMTime = 0;
|
||||
|
||||
public function __construct(string $file)
|
||||
{
|
||||
$this->fileInfo = new \SplFileInfo($file);
|
||||
}
|
||||
|
||||
public function getFile(): string
|
||||
{
|
||||
return $this->fileInfo->getPathname();
|
||||
}
|
||||
|
||||
public function read(): ?CacheInterface
|
||||
{
|
||||
if (!$this->fileInfo->isFile() || !$this->fileInfo->isReadable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fileObject = $this->fileInfo->openFile('r');
|
||||
|
||||
$cache = $this->readFromHandle($fileObject);
|
||||
$this->fileMTime = $this->getFileCurrentMTime();
|
||||
|
||||
unset($fileObject); // explicitly close file handler
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
public function write(CacheInterface $cache): void
|
||||
{
|
||||
$this->ensureFileIsWriteable();
|
||||
|
||||
$fileObject = $this->fileInfo->openFile('r+');
|
||||
|
||||
if (method_exists($cache, 'backfillHashes') && $this->fileMTime < $this->getFileCurrentMTime()) {
|
||||
$resultOfFlock = $fileObject->flock(\LOCK_EX);
|
||||
if (false === $resultOfFlock) {
|
||||
// Lock failed, OK - we continue without the lock.
|
||||
// noop
|
||||
}
|
||||
|
||||
$oldCache = $this->readFromHandle($fileObject);
|
||||
|
||||
$fileObject->rewind();
|
||||
|
||||
if (null !== $oldCache) {
|
||||
$cache->backfillHashes($oldCache);
|
||||
}
|
||||
}
|
||||
|
||||
$resultOfTruncate = $fileObject->ftruncate(0);
|
||||
if (false === $resultOfTruncate) {
|
||||
// Truncate failed. OK - we do not save the cache.
|
||||
return;
|
||||
}
|
||||
|
||||
$resultOfWrite = $fileObject->fwrite($cache->toJson());
|
||||
if (false === $resultOfWrite) {
|
||||
// Write failed. OK - we did not save the cache.
|
||||
return;
|
||||
}
|
||||
|
||||
$resultOfFlush = $fileObject->fflush();
|
||||
if (false === $resultOfFlush) {
|
||||
// Flush failed. OK - part of cache can be missing, in case this was last chunk in this pid.
|
||||
// noop
|
||||
}
|
||||
|
||||
$this->fileMTime = time(); // we could take the fresh `mtime` of file that we just modified with `$this->getFileCurrentMTime()`, but `time()` should be good enough here and reduce IO operation
|
||||
}
|
||||
|
||||
private function getFileCurrentMTime(): int
|
||||
{
|
||||
clearstatcache(true, $this->fileInfo->getPathname());
|
||||
|
||||
$mtime = $this->fileInfo->getMTime();
|
||||
|
||||
if (false === $mtime) {
|
||||
// cannot check mtime? OK - let's pretend file is old.
|
||||
$mtime = 0;
|
||||
}
|
||||
|
||||
return $mtime;
|
||||
}
|
||||
|
||||
private function readFromHandle(\SplFileObject $fileObject): ?CacheInterface
|
||||
{
|
||||
try {
|
||||
$size = $fileObject->getSize();
|
||||
if (false === $size || 0 === $size) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = $fileObject->fread($size);
|
||||
|
||||
if (false === $content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cache::fromJson($content);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureFileIsWriteable(): void
|
||||
{
|
||||
if ($this->fileInfo->isFile() && $this->fileInfo->isWritable()) {
|
||||
// all good
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->fileInfo->isDir()) {
|
||||
throw new IOException(
|
||||
\sprintf('Cannot write cache file "%s" as the location exists as directory.', $this->fileInfo->getRealPath()),
|
||||
0,
|
||||
null,
|
||||
$this->fileInfo->getPathname(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->fileInfo->isFile() && !$this->fileInfo->isWritable()) {
|
||||
throw new IOException(
|
||||
\sprintf('Cannot write to file "%s" as it is not writable.', $this->fileInfo->getRealPath()),
|
||||
0,
|
||||
null,
|
||||
$this->fileInfo->getPathname(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->createFile($this->fileInfo->getPathname());
|
||||
}
|
||||
|
||||
private function createFile(string $file): void
|
||||
{
|
||||
$dir = \dirname($file);
|
||||
|
||||
// Ensure path is created, but ignore if already exists. FYI: ignore EA suggestion in IDE,
|
||||
// `mkdir()` returns `false` for existing paths, so we can't mix it with `is_dir()` in one condition.
|
||||
if (!@is_dir($dir)) {
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
if (!@is_dir($dir)) {
|
||||
throw new IOException(
|
||||
\sprintf('Directory of cache file "%s" does not exists and couldn\'t be created.', $file),
|
||||
0,
|
||||
null,
|
||||
$file,
|
||||
);
|
||||
}
|
||||
|
||||
@touch($file);
|
||||
@chmod($file, 0666);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface FileHandlerInterface
|
||||
{
|
||||
public function getFile(): string;
|
||||
|
||||
public function read(): ?CacheInterface;
|
||||
|
||||
public function write(CacheInterface $cache): void;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class NullCacheManager implements CacheManagerInterface
|
||||
{
|
||||
public function needFixing(string $file, string $fileContent): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setFile(string $file, string $fileContent): void {}
|
||||
|
||||
public function setFileHash(string $file, string $hash): void {}
|
||||
}
|
||||
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
use PhpCsFixer\Future;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Signature implements SignatureInterface
|
||||
{
|
||||
private string $phpVersion;
|
||||
|
||||
private string $fixerVersion;
|
||||
|
||||
private string $indent;
|
||||
|
||||
private string $lineEnding;
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, mixed>|bool>
|
||||
*/
|
||||
private array $rules;
|
||||
|
||||
private string $ruleCustomisationPolicyVersion;
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>|bool> $rules
|
||||
*/
|
||||
public function __construct(string $phpVersion, string $fixerVersion, string $indent, string $lineEnding, array $rules, string $ruleCustomisationPolicyVersion)
|
||||
{
|
||||
$this->phpVersion = $phpVersion;
|
||||
$this->fixerVersion = $fixerVersion;
|
||||
$this->indent = $indent;
|
||||
$this->lineEnding = $lineEnding;
|
||||
$this->rules = self::makeJsonEncodable($rules);
|
||||
$this->ruleCustomisationPolicyVersion = $ruleCustomisationPolicyVersion;
|
||||
}
|
||||
|
||||
public function getPhpVersion(): string
|
||||
{
|
||||
return $this->phpVersion;
|
||||
}
|
||||
|
||||
public function getFixerVersion(): string
|
||||
{
|
||||
return $this->fixerVersion;
|
||||
}
|
||||
|
||||
public function getIndent(): string
|
||||
{
|
||||
return $this->indent;
|
||||
}
|
||||
|
||||
public function getLineEnding(): string
|
||||
{
|
||||
return $this->lineEnding;
|
||||
}
|
||||
|
||||
public function getRules(): array
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
|
||||
public function getRuleCustomisationPolicyVersion(): string
|
||||
{
|
||||
return $this->ruleCustomisationPolicyVersion;
|
||||
}
|
||||
|
||||
public function equals(SignatureInterface $signature): bool
|
||||
{
|
||||
return $this->phpVersion === $signature->getPhpVersion()
|
||||
&& $this->fixerVersion === $signature->getFixerVersion()
|
||||
&& $this->indent === $signature->getIndent()
|
||||
&& $this->lineEnding === $signature->getLineEnding()
|
||||
&& $this->rules === $signature->getRules()
|
||||
&& $this->ruleCustomisationPolicyVersion === $signature->getRuleCustomisationPolicyVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>|bool> $data
|
||||
*
|
||||
* @return array<string, array<string, mixed>|bool>
|
||||
*/
|
||||
private static function makeJsonEncodable(array $data): array
|
||||
{
|
||||
array_walk_recursive($data, static function (&$item, $key): void {
|
||||
if (\is_string($item) && false === mb_detect_encoding($item, 'utf-8', true)) {
|
||||
$item = base64_encode($item);
|
||||
} elseif (\is_object($item)) {
|
||||
if ($item instanceof \JsonSerializable) {
|
||||
$item = \get_class($item).'#'.json_encode($item, \JSON_THROW_ON_ERROR);
|
||||
} else {
|
||||
Future::triggerDeprecation(new \InvalidArgumentException(\sprintf(
|
||||
'Can not serialize cache signature, unhandled object under "%s" key: "%s" - implement "%s".',
|
||||
$key,
|
||||
\get_class($item),
|
||||
\JsonSerializable::class,
|
||||
)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Cache;
|
||||
|
||||
/**
|
||||
* @author Andreas Möller <am@localheinz.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface SignatureInterface
|
||||
{
|
||||
public function getPhpVersion(): string;
|
||||
|
||||
public function getFixerVersion(): string;
|
||||
|
||||
public function getIndent(): string;
|
||||
|
||||
public function getLineEnding(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>|bool>
|
||||
*/
|
||||
public function getRules(): array;
|
||||
|
||||
public function getRuleCustomisationPolicyVersion(): string;
|
||||
|
||||
public function equals(self $signature): bool;
|
||||
}
|
||||
omsorgWeb/mitarbeiter-app/api-client-php/vendor/friendsofphp/php-cs-fixer/src/ComposerJsonReader.php
Vendored
+178
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use Composer\Semver\Semver;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ComposerJsonReader
|
||||
{
|
||||
private const COMPOSER_FILENAME = 'composer.json';
|
||||
|
||||
private bool $isProcessed = false;
|
||||
|
||||
private ?string $php = null;
|
||||
|
||||
private ?string $phpUnit = null;
|
||||
|
||||
private static ?self $singleton = null;
|
||||
|
||||
public static function createSingleton(): self
|
||||
{
|
||||
if (null === self::$singleton) {
|
||||
self::$singleton = new self();
|
||||
}
|
||||
|
||||
return self::$singleton;
|
||||
}
|
||||
|
||||
public function getPhp(): ?string
|
||||
{
|
||||
$this->processFile();
|
||||
|
||||
return $this->php;
|
||||
}
|
||||
|
||||
public function getPhpUnit(): ?string
|
||||
{
|
||||
$this->processFile();
|
||||
|
||||
return $this->phpUnit;
|
||||
}
|
||||
|
||||
private function processFile(): void
|
||||
{
|
||||
if (true === $this->isProcessed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists(self::COMPOSER_FILENAME)) {
|
||||
throw new IOException(\sprintf('Failed to read file "%s".', self::COMPOSER_FILENAME));
|
||||
}
|
||||
|
||||
$readResult = file_get_contents(self::COMPOSER_FILENAME);
|
||||
if (false === $readResult) {
|
||||
throw new IOException(\sprintf('Failed to read file "%s".', self::COMPOSER_FILENAME));
|
||||
}
|
||||
|
||||
$this->processJson($readResult);
|
||||
}
|
||||
|
||||
private function processJson(string $json): void
|
||||
{
|
||||
if (true === $this->isProcessed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$composerJson = json_decode($json, true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
$this->php = self::getMinSemVer(self::detectPhp($composerJson));
|
||||
$this->phpUnit = self::getMinSemVer(self::detectPackage($composerJson, 'phpunit/phpunit'));
|
||||
|
||||
$this->isProcessed = true;
|
||||
}
|
||||
|
||||
private static function getMinSemVer(?string $version): ?string
|
||||
{
|
||||
if ('' === $version || null === $version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var non-empty-list<string> $arr */
|
||||
$arr = Preg::split('/\s*\|\|?\s*/', trim($version));
|
||||
|
||||
$arr = array_map(static function ($v) {
|
||||
$v = ltrim($v, 'v^~>= ');
|
||||
|
||||
$v = substr($v, 0, strcspn($v, ' ,-'));
|
||||
|
||||
if (str_ends_with($v, '.*')) {
|
||||
$v = substr($v, 0, -\strlen('.*'));
|
||||
}
|
||||
|
||||
return $v;
|
||||
}, $arr);
|
||||
|
||||
$textVersion = array_find($arr, static fn ($v) => true === Preg::match('/^\D/', $v));
|
||||
|
||||
if (null !== $textVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var non-empty-list<string> $sortedArr */
|
||||
$sortedArr = Semver::sort($arr);
|
||||
|
||||
$min = $sortedArr[0];
|
||||
$parts = explode('.', $min);
|
||||
|
||||
return \sprintf('%s.%s', (int) $parts[0], (int) ($parts[1] ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $composerJson
|
||||
*/
|
||||
private static function detectPhp(array $composerJson): ?string
|
||||
{
|
||||
$version = [];
|
||||
|
||||
if (isset($composerJson['config']['platform']['php'])) {
|
||||
$version[] = $composerJson['config']['platform']['php'];
|
||||
}
|
||||
|
||||
if (isset($composerJson['require-dev']['php'])) {
|
||||
$version[] = $composerJson['require-dev']['php'];
|
||||
}
|
||||
|
||||
if (isset($composerJson['require']['php'])) {
|
||||
$version[] = $composerJson['require']['php'];
|
||||
}
|
||||
|
||||
if (\count($version) > 0) {
|
||||
return implode(' || ', $version);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $composerJson
|
||||
* @param non-empty-string $package
|
||||
*/
|
||||
private static function detectPackage(array $composerJson, string $package): ?string
|
||||
{
|
||||
$version = [];
|
||||
|
||||
if (isset($composerJson['require-dev'][$package])) {
|
||||
$version[] = $composerJson['require-dev'][$package];
|
||||
}
|
||||
|
||||
if (isset($composerJson['require'][$package])) {
|
||||
$version[] = $composerJson['require'][$package];
|
||||
}
|
||||
|
||||
if (\count($version) > 0) {
|
||||
return implode(' || ', $version);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Config\RuleCustomisationPolicyAwareConfigInterface;
|
||||
use PhpCsFixer\Config\RuleCustomisationPolicyInterface;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
use PhpCsFixer\Runner\Parallel\ParallelConfig;
|
||||
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*
|
||||
* @api-extendable
|
||||
*/
|
||||
class Config implements ConfigInterface, ParallelAwareConfigInterface, UnsupportedPhpVersionAllowedConfigInterface, CustomRulesetsAwareConfigInterface, RuleCustomisationPolicyAwareConfigInterface
|
||||
{
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private string $cacheFile = '.php-cs-fixer.cache';
|
||||
|
||||
/**
|
||||
* @var list<FixerInterface>
|
||||
*/
|
||||
private array $customFixers = [];
|
||||
|
||||
/**
|
||||
* @var array<string, RuleSetDefinitionInterface>
|
||||
*/
|
||||
private array $customRuleSets = [];
|
||||
|
||||
/**
|
||||
* @var null|iterable<\SplFileInfo>
|
||||
*/
|
||||
private ?iterable $finder = null;
|
||||
|
||||
private string $format;
|
||||
|
||||
private bool $hideProgress = false;
|
||||
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private string $indent = ' ';
|
||||
|
||||
private bool $isRiskyAllowed = false;
|
||||
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private string $lineEnding = "\n";
|
||||
|
||||
private string $name;
|
||||
|
||||
private ParallelConfig $parallelConfig;
|
||||
|
||||
private ?string $phpExecutable = null;
|
||||
|
||||
/**
|
||||
* @TODO: 4.0 - update to @PER
|
||||
*
|
||||
* @var array<string, array<string, mixed>|bool>
|
||||
*/
|
||||
private array $rules;
|
||||
|
||||
private bool $usingCache = true;
|
||||
|
||||
private bool $isUnsupportedPhpVersionAllowed = false;
|
||||
|
||||
private ?RuleCustomisationPolicyInterface $ruleCustomisationPolicy = null;
|
||||
|
||||
public function __construct(string $name = 'default')
|
||||
{
|
||||
$this->name = $name.(Future::isFutureModeEnabled() ? ' (future mode)' : '');
|
||||
$this->rules = Future::getV4OrV3(['@PER-CS' => true], ['@PSR12' => true]); // @TODO 4.0 | 3.x switch to '@auto' for v4
|
||||
$this->format = Future::getV4OrV3('@auto', 'txt');
|
||||
$this->parallelConfig = ParallelConfigFactory::detect();
|
||||
|
||||
// @TODO 4.0 cleanup
|
||||
if (false !== getenv('PHP_CS_FIXER_IGNORE_ENV')) {
|
||||
$this->isUnsupportedPhpVersionAllowed = filter_var(getenv('PHP_CS_FIXER_IGNORE_ENV'), \FILTER_VALIDATE_BOOL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getCacheFile(): string
|
||||
{
|
||||
return $this->cacheFile;
|
||||
}
|
||||
|
||||
public function getCustomFixers(): array
|
||||
{
|
||||
return $this->customFixers;
|
||||
}
|
||||
|
||||
public function getCustomRuleSets(): array
|
||||
{
|
||||
return array_values($this->customRuleSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<\SplFileInfo>
|
||||
*/
|
||||
public function getFinder(): iterable
|
||||
{
|
||||
$this->finder ??= new Finder();
|
||||
|
||||
return $this->finder;
|
||||
}
|
||||
|
||||
public function getFormat(): string
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
public function getHideProgress(): bool
|
||||
{
|
||||
return $this->hideProgress;
|
||||
}
|
||||
|
||||
public function getIndent(): string
|
||||
{
|
||||
return $this->indent;
|
||||
}
|
||||
|
||||
public function getLineEnding(): string
|
||||
{
|
||||
return $this->lineEnding;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getParallelConfig(): ParallelConfig
|
||||
{
|
||||
return $this->parallelConfig;
|
||||
}
|
||||
|
||||
public function getPhpExecutable(): ?string
|
||||
{
|
||||
return $this->phpExecutable;
|
||||
}
|
||||
|
||||
public function getRiskyAllowed(): bool
|
||||
{
|
||||
return $this->isRiskyAllowed;
|
||||
}
|
||||
|
||||
public function getRules(): array
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
|
||||
public function getUsingCache(): bool
|
||||
{
|
||||
return $this->usingCache;
|
||||
}
|
||||
|
||||
public function getUnsupportedPhpVersionAllowed(): bool
|
||||
{
|
||||
return $this->isUnsupportedPhpVersionAllowed;
|
||||
}
|
||||
|
||||
public function getRuleCustomisationPolicy(): ?RuleCustomisationPolicyInterface
|
||||
{
|
||||
return $this->ruleCustomisationPolicy;
|
||||
}
|
||||
|
||||
public function registerCustomFixers(iterable $fixers): ConfigInterface
|
||||
{
|
||||
foreach ($fixers as $fixer) {
|
||||
$this->addCustomFixer($fixer);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<RuleSetDefinitionInterface> $ruleSets
|
||||
*/
|
||||
public function registerCustomRuleSets(array $ruleSets): ConfigInterface
|
||||
{
|
||||
foreach ($ruleSets as $ruleset) {
|
||||
$this->customRuleSets[$ruleset->getName()] = $ruleset;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $cacheFile
|
||||
*/
|
||||
public function setCacheFile(string $cacheFile): ConfigInterface
|
||||
{
|
||||
$this->cacheFile = $cacheFile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFinder(iterable $finder): ConfigInterface
|
||||
{
|
||||
$this->finder = $finder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFormat(string $format): ConfigInterface
|
||||
{
|
||||
$this->format = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setHideProgress(bool $hideProgress): ConfigInterface
|
||||
{
|
||||
$this->hideProgress = $hideProgress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $indent
|
||||
*/
|
||||
public function setIndent(string $indent): ConfigInterface
|
||||
{
|
||||
$this->indent = $indent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $lineEnding
|
||||
*/
|
||||
public function setLineEnding(string $lineEnding): ConfigInterface
|
||||
{
|
||||
$this->lineEnding = $lineEnding;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setParallelConfig(ParallelConfig $config): ConfigInterface
|
||||
{
|
||||
$this->parallelConfig = $config;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPhpExecutable(?string $phpExecutable): ConfigInterface
|
||||
{
|
||||
$this->phpExecutable = $phpExecutable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRiskyAllowed(bool $isRiskyAllowed): ConfigInterface
|
||||
{
|
||||
$this->isRiskyAllowed = $isRiskyAllowed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRules(array $rules): ConfigInterface
|
||||
{
|
||||
$this->rules = $rules;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUsingCache(bool $usingCache): ConfigInterface
|
||||
{
|
||||
$this->usingCache = $usingCache;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUnsupportedPhpVersionAllowed(bool $isUnsupportedPhpVersionAllowed): ConfigInterface
|
||||
{
|
||||
$this->isUnsupportedPhpVersionAllowed = $isUnsupportedPhpVersionAllowed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRuleCustomisationPolicy(?RuleCustomisationPolicyInterface $ruleCustomisationPolicy): ConfigInterface
|
||||
{
|
||||
// explicitly prevent policy with no proper version defined
|
||||
if (null !== $ruleCustomisationPolicy && '' === $ruleCustomisationPolicy->getPolicyVersionForCache()) {
|
||||
throw new \InvalidArgumentException('The Rule Customisation Policy version cannot be an empty string.');
|
||||
}
|
||||
|
||||
$this->ruleCustomisationPolicy = $ruleCustomisationPolicy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function addCustomFixer(FixerInterface $fixer): void
|
||||
{
|
||||
$this->customFixers[] = $fixer;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Config;
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL: This feature is experimental and does not fall under the backward compatibility promise.
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class NullRuleCustomisationPolicy implements RuleCustomisationPolicyInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public const VERSION_FOR_CACHE = 'null-policy';
|
||||
|
||||
public function getPolicyVersionForCache(): string
|
||||
{
|
||||
return self::VERSION_FOR_CACHE;
|
||||
}
|
||||
|
||||
public function getRuleCustomisers(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Config;
|
||||
|
||||
use PhpCsFixer\ConfigInterface;
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL: This feature is experimental and does not fall under the backward compatibility promise.
|
||||
*
|
||||
* @TODO 4.0 Include support for this in main ConfigInterface
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface RuleCustomisationPolicyAwareConfigInterface extends ConfigInterface
|
||||
{
|
||||
/**
|
||||
* EXPERIMENTAL: This feature is experimental and does not fall under the backward compatibility promise.
|
||||
* Registers a filter to be applied to fixers right before running them.
|
||||
*
|
||||
* @todo v4 Introduce it in main ConfigInterface
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRuleCustomisationPolicy(?RuleCustomisationPolicyInterface $ruleCustomisationPolicy): ConfigInterface;
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL: This feature is experimental and does not fall under the backward compatibility promise.
|
||||
* Gets the filter to be applied to fixers right before running them.
|
||||
*
|
||||
* @todo v4 Introduce it in main ConfigInterface
|
||||
*/
|
||||
public function getRuleCustomisationPolicy(): ?RuleCustomisationPolicyInterface;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Config;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL: This feature is experimental and does not fall under the backward compatibility promise.
|
||||
*
|
||||
* @todo v3.999 replace \SplFileInfo with \Symfony\Component\Finder\SplFileInfo
|
||||
*
|
||||
* @phpstan-type _RuleCustomisationPolicyCallback \Closure(\SplFileInfo): (bool|FixerInterface)
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface RuleCustomisationPolicyInterface
|
||||
{
|
||||
/**
|
||||
* Returns a string that changes when the policy implementation changes in a way that
|
||||
* would affect the cache validity.
|
||||
*
|
||||
* @example you may use the following snippet if your policy does not depend on any code outside of the file
|
||||
* `return hash_file(\PHP_VERSION_ID >= 8_01_00 ? 'xxh128' : 'md5', __FILE__);`
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getPolicyVersionForCache(): string;
|
||||
|
||||
/**
|
||||
* Customise fixers for given files.
|
||||
*
|
||||
* Array keys are fixer names, values are closures that will be invoked before applying the fixer to a specific file.
|
||||
* The closures receive the file as argument and must return:
|
||||
* - true to apply the fixer as is to the file
|
||||
* - false to skip applying the fixer to the file
|
||||
* - a new fixer instance to apply a customised version of the fixer
|
||||
*
|
||||
* When PHP CS Fixer is about to start fixing files, it will check that the currently used fixers include at least
|
||||
* all the fixers for which customisation rules are defined. If a customiser is defined for a fixer that is not currently applied,
|
||||
* an exception will be thrown.
|
||||
* This ensures that customisers are actually used for expected fixerswhich may be replaced by newer fixers in newer versions of PHP CS Fixer.
|
||||
* Since fixer sets may change even in patch releases, this also means that your implementation of this interface may need to be updated accordingly, even in patch releases.
|
||||
* So, we can't guarantee semver compatibility for Rule Customisation Policies.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* [
|
||||
* 'array_syntax' => static function (\SplFileInfo $file) {
|
||||
* if (str_ends_with($file->getPathname(), '/tests/foo.php')) {
|
||||
* // Disable the fixer for the file tests/foo.php
|
||||
* return false;
|
||||
* }
|
||||
* if (str_ends_with($file->getPathname(), '/bin/entrypoint')) {
|
||||
* // For the file bin/entrypoint let's create a new fixer instance with a different configuration
|
||||
* $fixer = new ArraySyntaxFixer();
|
||||
* $fixer->configure(['syntax' => 'long']);
|
||||
* return $fixer;
|
||||
* }
|
||||
* // Keep the default configuration for other files
|
||||
* return true;
|
||||
* },
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @return array<non-empty-string, _RuleCustomisationPolicyCallback>
|
||||
*/
|
||||
public function getRuleCustomisers(): array;
|
||||
}
|
||||
Vendored
+192
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface ConfigInterface
|
||||
{
|
||||
/** @internal */
|
||||
public const PHP_VERSION_SYNTAX_SUPPORTED = '8.5';
|
||||
|
||||
/**
|
||||
* Returns the path to the cache file.
|
||||
*
|
||||
* @return null|non-empty-string Returns null if not using cache
|
||||
*/
|
||||
public function getCacheFile(): ?string;
|
||||
|
||||
/**
|
||||
* Returns the custom fixers to use.
|
||||
*
|
||||
* @return list<FixerInterface>
|
||||
*/
|
||||
public function getCustomFixers(): array;
|
||||
|
||||
/**
|
||||
* Returns files to scan.
|
||||
*
|
||||
* @return iterable<\SplFileInfo>
|
||||
*/
|
||||
public function getFinder(): iterable;
|
||||
|
||||
public function getFormat(): string;
|
||||
|
||||
/**
|
||||
* Returns true if progress should be hidden.
|
||||
*/
|
||||
public function getHideProgress(): bool;
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getIndent(): string;
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getLineEnding(): string;
|
||||
|
||||
/**
|
||||
* Returns the name of the configuration.
|
||||
*
|
||||
* The name must be all lowercase and without any spaces.
|
||||
*
|
||||
* @return string The name of the configuration
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Get configured PHP executable, if any.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @TODO 4.0 remove me
|
||||
*/
|
||||
public function getPhpExecutable(): ?string;
|
||||
|
||||
/**
|
||||
* Check if it is allowed to run risky fixers.
|
||||
*/
|
||||
public function getRiskyAllowed(): bool;
|
||||
|
||||
/**
|
||||
* Get rules.
|
||||
*
|
||||
* Keys of array are names of fixers/sets, values are true/false.
|
||||
*
|
||||
* @return array<string, array<string, mixed>|bool>
|
||||
*/
|
||||
public function getRules(): array;
|
||||
|
||||
/**
|
||||
* Returns true if caching should be enabled.
|
||||
*/
|
||||
public function getUsingCache(): bool;
|
||||
|
||||
/**
|
||||
* Adds a suite of custom fixers.
|
||||
*
|
||||
* Name of custom fixer should follow `VendorName/rule_name` convention.
|
||||
*
|
||||
* @param iterable<FixerInterface> $fixers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function registerCustomFixers(iterable $fixers): self;
|
||||
|
||||
/**
|
||||
* Sets the path to the cache file.
|
||||
*
|
||||
* @param non-empty-string $cacheFile
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCacheFile(string $cacheFile): self;
|
||||
|
||||
/**
|
||||
* @param iterable<\SplFileInfo> $finder
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFinder(iterable $finder): self;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setFormat(string $format): self;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHideProgress(bool $hideProgress): self;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $indent
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIndent(string $indent): self;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $lineEnding
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLineEnding(string $lineEnding): self;
|
||||
|
||||
/**
|
||||
* Set PHP executable.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @TODO 4.0 remove me
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPhpExecutable(?string $phpExecutable): self;
|
||||
|
||||
/**
|
||||
* Set if it is allowed to run risky fixers.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRiskyAllowed(bool $isRiskyAllowed): self;
|
||||
|
||||
/**
|
||||
* Set rules.
|
||||
*
|
||||
* Keys of array are names of fixers or sets.
|
||||
* Value for set must be bool (turn it on or off).
|
||||
* Value for fixer may be bool (turn it on or off) or array of configuration
|
||||
* (turn it on and contains configuration for FixerInterface::configure method).
|
||||
*
|
||||
* @param array<string, array<string, mixed>|bool> $rules
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRules(array $rules): self;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setUsingCache(bool $usingCache): self;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\ConfigurationException;
|
||||
|
||||
use PhpCsFixer\Console\Command\FixCommandExitStatusCalculator;
|
||||
|
||||
/**
|
||||
* Exceptions of this type are thrown on misconfiguration of the Fixer.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @final Only internal extending this class is supported
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
class InvalidConfigurationException extends \InvalidArgumentException
|
||||
{
|
||||
public function __construct(string $message, ?int $code = null, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$message,
|
||||
$code ?? FixCommandExitStatusCalculator::EXIT_STATUS_FLAG_HAS_INVALID_CONFIG,
|
||||
$previous,
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\ConfigurationException;
|
||||
|
||||
use PhpCsFixer\Console\Command\FixCommandExitStatusCalculator;
|
||||
|
||||
/**
|
||||
* Exception thrown by Fixers on misconfiguration.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @final Only internal extending this class is supported
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
class InvalidFixerConfigurationException extends InvalidConfigurationException
|
||||
{
|
||||
private string $fixerName;
|
||||
|
||||
public function __construct(string $fixerName, string $message, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
\sprintf('[%s] %s', $fixerName, $message),
|
||||
FixCommandExitStatusCalculator::EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG,
|
||||
$previous,
|
||||
);
|
||||
|
||||
$this->fixerName = $fixerName;
|
||||
}
|
||||
|
||||
public function getFixerName(): string
|
||||
{
|
||||
return $this->fixerName;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\ConfigurationException;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class InvalidForEnvFixerConfigurationException extends InvalidFixerConfigurationException {}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\ConfigurationException;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class RequiredFixerConfigurationException extends InvalidFixerConfigurationException {}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\ConfigurationException;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class UnresolvableAutoRuleSetConfigurationException extends InvalidConfigurationException {}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console;
|
||||
|
||||
use PhpCsFixer\Console\Command\CheckCommand;
|
||||
use PhpCsFixer\Console\Command\DescribeCommand;
|
||||
use PhpCsFixer\Console\Command\FixCommand;
|
||||
use PhpCsFixer\Console\Command\HelpCommand;
|
||||
use PhpCsFixer\Console\Command\InitCommand;
|
||||
use PhpCsFixer\Console\Command\ListFilesCommand;
|
||||
use PhpCsFixer\Console\Command\ListRulesCommand;
|
||||
use PhpCsFixer\Console\Command\ListSetsCommand;
|
||||
use PhpCsFixer\Console\Command\SelfUpdateCommand;
|
||||
use PhpCsFixer\Console\Command\WorkerCommand;
|
||||
use PhpCsFixer\Console\SelfUpdate\GithubClient;
|
||||
use PhpCsFixer\Console\SelfUpdate\NewVersionChecker;
|
||||
use PhpCsFixer\Future;
|
||||
use PhpCsFixer\PharChecker;
|
||||
use PhpCsFixer\Runner\Parallel\WorkerException;
|
||||
use PhpCsFixer\ToolInfo;
|
||||
use Symfony\Component\Console\Application as BaseApplication;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Command\CompleteCommand;
|
||||
use Symfony\Component\Console\Command\DumpCompletionCommand;
|
||||
use Symfony\Component\Console\Command\ListCommand;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Application extends BaseApplication
|
||||
{
|
||||
public const NAME = 'PHP CS Fixer';
|
||||
public const VERSION = '3.95.18';
|
||||
public const VERSION_CODENAME = 'Adalbertus';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
private ToolInfo $toolInfo;
|
||||
|
||||
private ?Command $executedCommand = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::NAME, self::VERSION);
|
||||
|
||||
$this->toolInfo = new ToolInfo();
|
||||
|
||||
// in alphabetical order
|
||||
$this->add(new CheckCommand($this->toolInfo));
|
||||
$this->add(new DescribeCommand());
|
||||
$this->add(new FixCommand($this->toolInfo));
|
||||
$this->add(new InitCommand());
|
||||
$this->add(new ListFilesCommand($this->toolInfo));
|
||||
$this->add(new ListRulesCommand());
|
||||
$this->add(new ListSetsCommand());
|
||||
$this->add(new SelfUpdateCommand(
|
||||
new NewVersionChecker(new GithubClient()),
|
||||
$this->toolInfo,
|
||||
new PharChecker(),
|
||||
));
|
||||
$this->add(new WorkerCommand($this->toolInfo));
|
||||
}
|
||||
|
||||
// polyfill for `add` method, as it is not available in Symfony 8.0
|
||||
public function add(Command $command): ?Command
|
||||
{
|
||||
if (method_exists($this, 'addCommand')) { // @phpstan-ignore-line
|
||||
return $this->addCommand($command);
|
||||
}
|
||||
|
||||
return parent::add($command); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public static function getMajorVersion(): int
|
||||
{
|
||||
return (int) explode('.', self::VERSION)[0];
|
||||
}
|
||||
|
||||
public function doRun(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$stdErr = $output instanceof ConsoleOutputInterface
|
||||
? $output->getErrorOutput()
|
||||
: ($input->hasParameterOption('--format', true) && 'txt' !== $input->getParameterOption('--format', null, true) ? null : $output);
|
||||
|
||||
if (null !== $stdErr) {
|
||||
$warningsDetector = new WarningsDetector($this->toolInfo);
|
||||
$warningsDetector->detectOldVendor();
|
||||
$warningsDetector->detectOldMajor();
|
||||
|
||||
try {
|
||||
$commandName = $this->getCommandName($input);
|
||||
if (null === $commandName) {
|
||||
throw new CommandNotFoundException('No command name found.');
|
||||
}
|
||||
$command = $this->find($commandName);
|
||||
|
||||
if (($command instanceof CheckCommand) || ($command instanceof FixCommand)) {
|
||||
$warningsDetector->detectHigherPhpVersion();
|
||||
$warningsDetector->detectNonMonolithic();
|
||||
}
|
||||
} catch (CommandNotFoundException $e) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
$warnings = $warningsDetector->getWarnings();
|
||||
|
||||
if (\count($warnings) > 0) {
|
||||
foreach ($warnings as $warning) {
|
||||
$stdErr->writeln(\sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', $warning));
|
||||
}
|
||||
$stdErr->writeln('');
|
||||
}
|
||||
}
|
||||
|
||||
$result = parent::doRun($input, $output);
|
||||
|
||||
if (
|
||||
null !== $stdErr
|
||||
&& $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE
|
||||
) {
|
||||
$triggeredDeprecations = Future::getTriggeredDeprecations();
|
||||
|
||||
if (\count($triggeredDeprecations) > 0) {
|
||||
$stdErr->writeln('');
|
||||
$stdErr->writeln($stdErr->isDecorated() ? '<bg=yellow;fg=black;>Detected deprecations in use (they will stop working in next major release):</>' : 'Detected deprecations in use (they will stop working in next major release):');
|
||||
foreach ($triggeredDeprecations as $deprecation) {
|
||||
$stdErr->writeln(\sprintf('- %s', $deprecation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function getAbout(bool $decorated = false): string
|
||||
{
|
||||
$longVersion = \sprintf('%s <info>%s</info>', self::NAME, self::VERSION);
|
||||
|
||||
// value of `$commitPlaceholderPossiblyEvaluated` will be changed during phar building, other value will not
|
||||
$commitPlaceholderPossiblyEvaluated = '@git-commit@';
|
||||
$commitPlaceholder = implode('', ['@', 'git-commit@']); // do not replace with imploded value, as here we need to prevent phar builder to replace the placeholder
|
||||
|
||||
$versionCommit = $commitPlaceholder !== $commitPlaceholderPossiblyEvaluated
|
||||
? substr($commitPlaceholderPossiblyEvaluated, 0, 7) // for phar builds
|
||||
: '';
|
||||
|
||||
$about = implode('', [
|
||||
$longVersion,
|
||||
$versionCommit ? \sprintf(' <info>(%s)</info>', $versionCommit) : '', // @phpstan-ignore-line to avoid `Ternary operator condition is always true|false.`
|
||||
self::VERSION_CODENAME ? \sprintf(' <info>%s</info>', self::VERSION_CODENAME) : '', // @phpstan-ignore-line to avoid `Ternary operator condition is always true|false.`
|
||||
' by <comment>Fabien Potencier</comment>, <comment>Dariusz Ruminski</comment> and <comment>contributors</comment>.',
|
||||
]);
|
||||
|
||||
if (false === $decorated) {
|
||||
return strip_tags($about);
|
||||
}
|
||||
|
||||
return $about;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function getAboutWithRuntime(bool $decorated = false): string
|
||||
{
|
||||
$about = self::getAbout(true)."\nPHP runtime: <info>".\PHP_VERSION.'</info>';
|
||||
if (false === $decorated) {
|
||||
return strip_tags($about);
|
||||
}
|
||||
|
||||
return $about;
|
||||
}
|
||||
|
||||
public function getLongVersion(): string
|
||||
{
|
||||
return self::getAboutWithRuntime(true);
|
||||
}
|
||||
|
||||
protected function getDefaultCommands(): array
|
||||
{
|
||||
return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->executedCommand = $command;
|
||||
|
||||
return parent::doRunCommand($command, $input, $output);
|
||||
}
|
||||
|
||||
protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void
|
||||
{
|
||||
// Since parallel analysis utilises child processes, and they have their own output,
|
||||
// we need to capture the output of the child process to determine it there was an exception.
|
||||
// Default render format is not machine-friendly, so we need to override it for `worker` command,
|
||||
// in order to be able to easily parse exception data for further displaying on main process' side.
|
||||
if ($this->executedCommand instanceof WorkerCommand) {
|
||||
$output->writeln(WorkerCommand::ERROR_PREFIX.json_encode(
|
||||
[
|
||||
'class' => \get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'code' => $e->getCode(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
],
|
||||
\JSON_THROW_ON_ERROR,
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
parent::doRenderThrowable($e, $output);
|
||||
|
||||
if ($output->isVeryVerbose() && $e instanceof WorkerException) {
|
||||
$output->writeln('<comment>Original trace from worker:</comment>');
|
||||
$output->writeln('');
|
||||
$output->writeln($e->getOriginalTraceAsString());
|
||||
$output->writeln('');
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\ToolInfoInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* @author Greg Korba <greg@codito.dev>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'check', description: 'Checks if configured files/directories comply with configured rules.')]
|
||||
final class CheckCommand extends FixCommand
|
||||
{
|
||||
public function __construct(ToolInfoInterface $toolInfo)
|
||||
{
|
||||
parent::__construct($toolInfo);
|
||||
$this->setName('check');
|
||||
$this->setDescription('Checks if configured files/directories comply with configured rules.');
|
||||
}
|
||||
|
||||
public function getHelp(): string
|
||||
{
|
||||
return Preg::replace('@\v\V*<comment>--dry-run</comment>\V*\v@', '', parent::getHelp());
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setDefinition([
|
||||
...array_values($this->getDefinition()->getArguments()),
|
||||
...array_values(array_filter(
|
||||
$this->getDefinition()->getOptions(),
|
||||
static fn (InputOption $option): bool => 'dry-run' !== $option->getName(),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function isDryRun(InputInterface $input): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+769
@@ -0,0 +1,769 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\Console\ConfigurationResolver;
|
||||
use PhpCsFixer\Differ\DiffConsoleFormatter;
|
||||
use PhpCsFixer\Differ\FullDiffer;
|
||||
use PhpCsFixer\Documentation\DocumentationTag;
|
||||
use PhpCsFixer\Documentation\DocumentationTagGenerator;
|
||||
use PhpCsFixer\Documentation\DocumentationTagType;
|
||||
use PhpCsFixer\Documentation\FixerDocumentGenerator;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
|
||||
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
|
||||
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOption;
|
||||
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
|
||||
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
|
||||
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
|
||||
use PhpCsFixer\FixerFactory;
|
||||
use PhpCsFixer\Future;
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\RuleSet\AutomaticRuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\DeprecatedRuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\RuleSet;
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\RuleSets;
|
||||
use PhpCsFixer\StdinFileInfo;
|
||||
use PhpCsFixer\Tokenizer\Tokens;
|
||||
use PhpCsFixer\ToolInfo;
|
||||
use PhpCsFixer\Utils;
|
||||
use PhpCsFixer\WordMatcher;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Helper\TreeHelper;
|
||||
use Symfony\Component\Console\Helper\TreeNode;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'describe', description: 'Describe rule / ruleset.')]
|
||||
final class DescribeCommand extends Command
|
||||
{
|
||||
private const SET_ALIAS_TO_DESCRIBE_CONFIG = '@';
|
||||
private const SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET = '@-';
|
||||
|
||||
/**
|
||||
* @var ?list<string>
|
||||
*/
|
||||
private ?array $setNames = null;
|
||||
|
||||
private FixerFactory $fixerFactory;
|
||||
|
||||
/**
|
||||
* @var null|array<string, FixerInterface>
|
||||
*/
|
||||
private ?array $fixers = null;
|
||||
|
||||
public function __construct(?FixerFactory $fixerFactory = null)
|
||||
{
|
||||
parent::__construct('describe');
|
||||
$this->setDescription('Describe rule / ruleset.');
|
||||
|
||||
if (null === $fixerFactory) {
|
||||
$fixerFactory = new FixerFactory();
|
||||
$fixerFactory->registerBuiltInFixers();
|
||||
}
|
||||
|
||||
$this->fixerFactory = $fixerFactory;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDefinition(
|
||||
[
|
||||
new InputArgument('name', InputArgument::OPTIONAL, 'Name of rule / set.', null, fn () => array_merge($this->getSetNames(), array_keys($this->getFixers()))),
|
||||
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
|
||||
new InputOption('expand', '', InputOption::VALUE_NONE, 'Shall nested sets be expanded into nested rules.'),
|
||||
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats (txt, tree).', 'txt', ['txt', 'tree']),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$stdErr = $output->getErrorOutput();
|
||||
$stdErr->writeln(Application::getAboutWithRuntime(true));
|
||||
}
|
||||
|
||||
$resolver = new ConfigurationResolver(
|
||||
new Config(),
|
||||
['config' => $input->getOption('config')],
|
||||
getcwd(), // @phpstan-ignore argument.type
|
||||
new ToolInfo(),
|
||||
);
|
||||
|
||||
$this->fixerFactory->registerCustomFixers($resolver->getConfig()->getCustomFixers());
|
||||
|
||||
/** @var ?string $name */
|
||||
$name = $input->getArgument('name');
|
||||
$expand = $input->getOption('expand');
|
||||
$format = $input->getOption('format');
|
||||
|
||||
if (null === $name) {
|
||||
if (false === $input->isInteractive()) {
|
||||
throw new RuntimeException('Not enough arguments (missing: "name") when not running interactively.');
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shallDescribeConfigInUse = 'yes' === $io->choice(
|
||||
'Do you want to describe used configuration? (alias:`@`',
|
||||
['yes', 'no'],
|
||||
'yes',
|
||||
);
|
||||
if ($shallDescribeConfigInUse) {
|
||||
$name = self::SET_ALIAS_TO_DESCRIBE_CONFIG;
|
||||
} else {
|
||||
$name = $io->choice(
|
||||
'Please select rule / set to describe',
|
||||
array_merge($this->getSetNames(), array_keys($this->getFixers())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ('tree' === $format) {
|
||||
if (!str_starts_with($name, '@')) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The "--format=tree" option is available only when describing a set (name starting with "@").',
|
||||
);
|
||||
}
|
||||
if (!class_exists(TreeHelper::class)) {
|
||||
throw new \RuntimeException('The "--format=tree" option requires symfony/console 7.3+.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!str_starts_with($name, '@')) {
|
||||
if (true === $expand) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The "--expand" option is available only when describing a set (name starting with "@").',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (str_starts_with($name, '@')) {
|
||||
$this->describeSet($input, $output, $name, $resolver);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->describeRule($output, $name);
|
||||
} catch (DescribeNameNotFoundException $e) {
|
||||
$matcher = new WordMatcher(
|
||||
'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers()),
|
||||
);
|
||||
|
||||
$alternative = $matcher->match($name);
|
||||
|
||||
$this->describeList($output, $e->getType());
|
||||
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'%s "%s" not found.%s',
|
||||
ucfirst($e->getType()),
|
||||
$name,
|
||||
null === $alternative ? '' : ' Did you mean "'.$alternative.'"?',
|
||||
));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function describeRule(OutputInterface $output, string $name): void
|
||||
{
|
||||
$fixers = $this->getFixers();
|
||||
|
||||
if (!isset($fixers[$name])) {
|
||||
throw new DescribeNameNotFoundException($name, 'rule');
|
||||
}
|
||||
|
||||
$fixer = $fixers[$name];
|
||||
|
||||
$definition = $fixer->getDefinition();
|
||||
|
||||
$output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> rule.</>', $name));
|
||||
$output->writeln('');
|
||||
|
||||
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$output->writeln(\sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
$output->writeln($definition->getSummary());
|
||||
|
||||
$description = $definition->getDescription();
|
||||
|
||||
if (null !== $description) {
|
||||
$output->writeln($description);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
|
||||
$tags = DocumentationTagGenerator::analyseRule($fixer);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (DocumentationTagType::DEPRECATED === $tag->type) {
|
||||
Future::triggerDeprecation(new \RuntimeException(str_replace(
|
||||
'`',
|
||||
'"',
|
||||
\sprintf(
|
||||
'%s%s',
|
||||
str_replace('This rule', \sprintf('Rule "%s"', $name), $tag->title),
|
||||
null !== $tag->description ? '. '.$tag->description : '',
|
||||
),
|
||||
)));
|
||||
} elseif (DocumentationTagType::CONFIGURABLE === $tag->type) {
|
||||
continue; // skip, handled later
|
||||
}
|
||||
|
||||
$output->writeln(\sprintf('<error>%s</error>', $tag->title));
|
||||
$tagDescription = $tag->description;
|
||||
|
||||
if (null !== $tagDescription) {
|
||||
$tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription);
|
||||
$output->writeln($tagDescription);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if ($fixer instanceof ConfigurableFixerInterface) {
|
||||
$configurationDefinition = $fixer->getConfigurationDefinition();
|
||||
$options = $configurationDefinition->getOptions();
|
||||
|
||||
$output->writeln(\sprintf('Fixer is configurable using following option%s:', 1 === \count($options) ? '' : 's'));
|
||||
|
||||
foreach ($options as $option) {
|
||||
$line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
|
||||
$allowed = HelpCommand::getDisplayableAllowedValues($option);
|
||||
|
||||
if (null === $allowed) {
|
||||
$allowedTypes = $option->getAllowedTypes();
|
||||
if (null !== $allowedTypes) {
|
||||
$allowed = array_map(
|
||||
static fn (string $type): string => '<comment>'.$type.'</comment>',
|
||||
$allowedTypes,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
|
||||
? 'a subset of <comment>'.Utils::toString($value->getAllowedValues()).'</comment>'
|
||||
: '<comment>'.Utils::toString($value).'</comment>', $allowed);
|
||||
}
|
||||
|
||||
if (null !== $allowed) {
|
||||
$line .= ' ('.Utils::naturalLanguageJoin($allowed, '').')';
|
||||
}
|
||||
|
||||
$description = Preg::replace('/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape($option->getDescription()));
|
||||
$line .= ': '.lcfirst(Preg::replace('/\.$/', '', $description)).'; ';
|
||||
|
||||
if ($option->hasDefault()) {
|
||||
$line .= \sprintf(
|
||||
'defaults to <comment>%s</comment>',
|
||||
Utils::toString($option->getDefault()),
|
||||
);
|
||||
} else {
|
||||
$line .= '<comment>required</comment>';
|
||||
}
|
||||
|
||||
if ($option instanceof DeprecatedFixerOption) {
|
||||
$line .= '. <error>DEPRECATED</error>: '.Preg::replace(
|
||||
'/(`.+?`)/',
|
||||
'<info>$1</info>',
|
||||
OutputFormatter::escape(lcfirst($option->getDeprecationMessage())),
|
||||
);
|
||||
}
|
||||
|
||||
if ($option instanceof AliasedFixerOption) {
|
||||
$line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
|
||||
}
|
||||
|
||||
$output->writeln($line);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
$codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool {
|
||||
if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
|
||||
return $codeSample->isSuitableFor(\PHP_VERSION_ID);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (0 === \count($definition->getCodeSamples())) {
|
||||
$output->writeln([
|
||||
'Fixing examples are not available for this rule.',
|
||||
'',
|
||||
]);
|
||||
} elseif (0 === \count($codeSamples)) {
|
||||
$output->writeln([
|
||||
'Fixing examples <error>cannot be</error> demonstrated on the current PHP version.',
|
||||
'',
|
||||
]);
|
||||
} else {
|
||||
$output->writeln('Fixing examples:');
|
||||
|
||||
$differ = new FullDiffer();
|
||||
$diffFormatter = new DiffConsoleFormatter(
|
||||
$output->isDecorated(),
|
||||
\sprintf(
|
||||
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
|
||||
\PHP_EOL,
|
||||
\PHP_EOL,
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($codeSamples as $index => $codeSample) {
|
||||
$old = $codeSample->getCode();
|
||||
$tokens = Tokens::fromCode($old);
|
||||
|
||||
$configuration = $codeSample->getConfiguration();
|
||||
|
||||
if ($fixer instanceof ConfigurableFixerInterface) {
|
||||
$fixer->configure($configuration ?? []);
|
||||
}
|
||||
|
||||
$file = $codeSample instanceof FileSpecificCodeSampleInterface
|
||||
? $codeSample->getSplFileInfo()
|
||||
: new StdinFileInfo();
|
||||
|
||||
$fixer->fix($file, $tokens);
|
||||
|
||||
$diff = $differ->diff($old, $tokens->generateCode());
|
||||
|
||||
if ($fixer instanceof ConfigurableFixerInterface) {
|
||||
if (null === $configuration) {
|
||||
$output->writeln(\sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1));
|
||||
} else {
|
||||
$output->writeln(\sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, Utils::toString($codeSample->getConfiguration())));
|
||||
}
|
||||
} else {
|
||||
$output->writeln(\sprintf(' * Example #%d.', $index + 1));
|
||||
}
|
||||
|
||||
$output->writeln([$diffFormatter->format($diff, ' %s'), '']);
|
||||
}
|
||||
}
|
||||
|
||||
$ruleSetConfigs = FixerDocumentGenerator::getSetsOfRule($name);
|
||||
|
||||
if ([] !== $ruleSetConfigs) {
|
||||
ksort($ruleSetConfigs);
|
||||
$plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
|
||||
$output->writeln("The fixer is part of the following rule set{$plural}:");
|
||||
|
||||
$ruleSetDefinitions = RuleSets::getSetDefinitions();
|
||||
|
||||
foreach ($ruleSetConfigs as $set => $config) {
|
||||
\assert(isset($ruleSetDefinitions[$set]));
|
||||
$ruleSetDefinition = $ruleSetDefinitions[$set];
|
||||
|
||||
if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deprecatedDesc = ($ruleSetDefinition instanceof DeprecatedRuleSetDefinitionInterface) ? ' *(deprecated)*' : '';
|
||||
if (null !== $config) {
|
||||
$output->writeln(\sprintf('* <info>%s</info> with config: <comment>%s</comment>', $set.$deprecatedDesc, Utils::toString($config)));
|
||||
} else {
|
||||
$output->writeln(\sprintf('* <info>%s</info> with <comment>default</comment> config', $set.$deprecatedDesc));
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
}
|
||||
}
|
||||
|
||||
private function describeSet(InputInterface $input, OutputInterface $output, string $name, ConfigurationResolver $resolver): void
|
||||
{
|
||||
if (
|
||||
!\in_array($name, [self::SET_ALIAS_TO_DESCRIBE_CONFIG, self::SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET], true)
|
||||
&& !\in_array($name, $this->getSetNames(), true)) {
|
||||
throw new DescribeNameNotFoundException($name, 'set');
|
||||
}
|
||||
|
||||
if (self::SET_ALIAS_TO_DESCRIBE_CONFIG === $name) {
|
||||
$aliasedRuleSetDefinition = $this->createRuleSetDefinition(
|
||||
null,
|
||||
[],
|
||||
[
|
||||
'getDescription' => null === $resolver->getConfigFile() ? 'Default rules, no config file.' : 'Rules defined in used config.',
|
||||
'getName' => \sprintf('@ - %s', $resolver->getConfig()->getName()),
|
||||
'getRules' => $resolver->getConfig()->getRules(),
|
||||
'isRisky' => $resolver->getRiskyAllowed(),
|
||||
],
|
||||
);
|
||||
} elseif (self::SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET === $name) {
|
||||
$rulesWithoutSet = array_filter(
|
||||
$this->getFixers(),
|
||||
static fn (string $name): bool => [] === FixerDocumentGenerator::getSetsOfRule($name),
|
||||
\ARRAY_FILTER_USE_KEY,
|
||||
);
|
||||
|
||||
$aliasedRuleSetDefinition = $this->createRuleSetDefinition(
|
||||
null,
|
||||
[],
|
||||
[
|
||||
'getDescription' => 'Rules that are not part of any set.',
|
||||
'getName' => '@- - rules without set',
|
||||
'getRules' => array_combine(
|
||||
array_map(
|
||||
static fn (FixerInterface $fixer): string => $fixer->getName(),
|
||||
$rulesWithoutSet,
|
||||
),
|
||||
array_fill(0, \count($rulesWithoutSet), true),
|
||||
),
|
||||
'isRisky' => array_any(
|
||||
$rulesWithoutSet,
|
||||
static fn (FixerInterface $fixer): bool => $fixer->isRisky(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
$ruleSetDefinitions = RuleSets::getSetDefinitions();
|
||||
$ruleSetDefinition = $aliasedRuleSetDefinition ?? $ruleSetDefinitions[$name];
|
||||
$fixers = $this->getFixers();
|
||||
|
||||
if (true === $input->getOption('expand')) {
|
||||
$ruleSetDefinition = $this->createRuleSetDefinition($ruleSetDefinition, ['expand'], []);
|
||||
} else {
|
||||
$output->writeln("You may the '--expand' option to see nested sets expanded into nested rules.");
|
||||
}
|
||||
|
||||
$output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> set.</>', $ruleSetDefinition->getName()));
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln($this->replaceRstLinks($ruleSetDefinition->getDescription()));
|
||||
$output->writeln('');
|
||||
|
||||
$tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (DocumentationTagType::DEPRECATED === $tag->type) {
|
||||
Future::triggerDeprecation(new \RuntimeException(str_replace(
|
||||
'`',
|
||||
'"',
|
||||
\sprintf(
|
||||
'%s%s',
|
||||
str_replace('This rule set', \sprintf('Rule set "%s"', $name), $tag->title),
|
||||
null !== $tag->description ? '. '.$tag->description : '',
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
$output->writeln(\sprintf('<error>%s</error>', $tag->title));
|
||||
$tagDescription = $tag->description;
|
||||
|
||||
if (null !== $tagDescription) {
|
||||
$tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription);
|
||||
$output->writeln($tagDescription);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if ('tree' === $input->getOption('format')) {
|
||||
$this->describeSetContentAsTree($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers);
|
||||
} else {
|
||||
$this->describeSetContentAsTxt($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
|
||||
* @param array<string, FixerInterface> $fixers
|
||||
*/
|
||||
private function createTreeNode(RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): TreeNode
|
||||
{
|
||||
$tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition);
|
||||
$extra = [] !== $tags
|
||||
? ' '.implode(' ', array_map(
|
||||
static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
|
||||
$tags,
|
||||
))
|
||||
: '';
|
||||
|
||||
$node = new TreeNode($ruleSetDefinition->getName().$extra);
|
||||
|
||||
$rules = $ruleSetDefinition->getRules();
|
||||
$rulesKeys = array_keys($rules);
|
||||
natcasesort($rulesKeys);
|
||||
|
||||
foreach ($rulesKeys as $rule) {
|
||||
\assert(isset($rules[$rule]));
|
||||
$config = $rules[$rule];
|
||||
if (str_starts_with($rule, '@')) {
|
||||
\assert(isset($ruleSetDefinitions[$rule]));
|
||||
$child = $this->createTreeNode($ruleSetDefinitions[$rule], $ruleSetDefinitions, $fixers);
|
||||
} else {
|
||||
\assert(isset($fixers[$rule]));
|
||||
$fixer = $fixers[$rule];
|
||||
$tags = DocumentationTagGenerator::analyseRule($fixer);
|
||||
$extra = [] !== $tags
|
||||
? ' '.implode(' ', array_map(
|
||||
static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
|
||||
$tags,
|
||||
))
|
||||
: '';
|
||||
if (false === $config) {
|
||||
$extra = \sprintf(' | <error>Configuration: %s</>', Utils::toString($config));
|
||||
} elseif (true !== $config) {
|
||||
$extra = \sprintf(' | <comment>Configuration: %s</>', Utils::toString($config));
|
||||
}
|
||||
$child = new TreeNode($rule.$extra);
|
||||
}
|
||||
$node->addChild($child);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
|
||||
* @param array<string, FixerInterface> $fixers
|
||||
*/
|
||||
private function describeSetContentAsTree(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void
|
||||
{
|
||||
$io = new SymfonyStyle(
|
||||
new ArrayInput([]),
|
||||
$output,
|
||||
);
|
||||
|
||||
$root = $this->createTreeNode($ruleSetDefinition, $ruleSetDefinitions, $fixers);
|
||||
$tree = TreeHelper::createTree($io, $root);
|
||||
$tree->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
|
||||
* @param array<string, FixerInterface> $fixers
|
||||
*/
|
||||
private function describeSetContentAsTxt(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void
|
||||
{
|
||||
$help = '';
|
||||
|
||||
foreach ($ruleSetDefinition->getRules() as $rule => $config) {
|
||||
if (str_starts_with($rule, '@')) {
|
||||
\assert(isset($ruleSetDefinitions[$rule]));
|
||||
$set = $ruleSetDefinitions[$rule];
|
||||
$tags = DocumentationTagGenerator::analyseRuleSet($set);
|
||||
$help .= \sprintf(
|
||||
" * <info>%s</info>%s%s\n | %s\n\n",
|
||||
$rule,
|
||||
[] !== $tags ? ' ' : '',
|
||||
implode(' ', array_map(
|
||||
static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
|
||||
$tags,
|
||||
)),
|
||||
$this->replaceRstLinks($set->getDescription()),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
\assert(isset($fixers[$rule]));
|
||||
$fixer = $fixers[$rule];
|
||||
$tags = DocumentationTagGenerator::analyseRule($fixer);
|
||||
|
||||
$definition = $fixer->getDefinition();
|
||||
$help .= \sprintf(
|
||||
" * <info>%s</info>%s%s\n | %s\n%s\n",
|
||||
$rule,
|
||||
[] !== $tags ? ' ' : '',
|
||||
implode(' ', array_map(
|
||||
static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
|
||||
$tags,
|
||||
)),
|
||||
$definition->getSummary(),
|
||||
true !== $config ? \sprintf(" <comment>| Configuration: %s</comment>\n", Utils::toString($config)) : '',
|
||||
);
|
||||
}
|
||||
|
||||
$output->write($help);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, FixerInterface>
|
||||
*/
|
||||
private function getFixers(): array
|
||||
{
|
||||
if (null !== $this->fixers) {
|
||||
return $this->fixers;
|
||||
}
|
||||
|
||||
$fixers = [];
|
||||
|
||||
foreach ($this->fixerFactory->getFixers() as $fixer) {
|
||||
$fixers[$fixer->getName()] = $fixer;
|
||||
}
|
||||
|
||||
$this->fixers = $fixers;
|
||||
ksort($this->fixers);
|
||||
|
||||
return $this->fixers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function getSetNames(): array
|
||||
{
|
||||
if (null !== $this->setNames) {
|
||||
return $this->setNames;
|
||||
}
|
||||
|
||||
$this->setNames = RuleSets::getSetDefinitionNames();
|
||||
|
||||
return $this->setNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type 'rule'|'set'
|
||||
*/
|
||||
private function describeList(OutputInterface $output, string $type): void
|
||||
{
|
||||
if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'set' === $type) {
|
||||
$output->writeln('<comment>Defined sets:</comment>');
|
||||
|
||||
$items = $this->getSetNames();
|
||||
foreach ($items as $item) {
|
||||
$output->writeln(\sprintf('* <info>%s</info>', $item));
|
||||
}
|
||||
}
|
||||
|
||||
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'rule' === $type) {
|
||||
$output->writeln('<comment>Defined rules:</comment>');
|
||||
|
||||
$items = array_keys($this->getFixers());
|
||||
foreach ($items as $item) {
|
||||
$output->writeln(\sprintf('* <info>%s</info>', $item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function replaceRstLinks(string $content): string
|
||||
{
|
||||
return Preg::replaceCallback(
|
||||
'/(`[^<]+<[^>]+>`_)/',
|
||||
static fn (array $matches) => Preg::replaceCallback(
|
||||
'/`(.*)<(.*)>`_/',
|
||||
static fn (array $matches): string => $matches[1].'('.$matches[2].')',
|
||||
$matches[1],
|
||||
),
|
||||
$content,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<'expand'> $adjustments
|
||||
* @param array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} $overrides
|
||||
*/
|
||||
private function createRuleSetDefinition(?RuleSetDefinitionInterface $ruleSetDefinition, array $adjustments, array $overrides): RuleSetDefinitionInterface
|
||||
{
|
||||
return new class($ruleSetDefinition, $adjustments, $overrides) implements RuleSetDefinitionInterface {
|
||||
private ?RuleSetDefinitionInterface $original;
|
||||
|
||||
/** @var list<'expand'> */
|
||||
private array $adjustments;
|
||||
|
||||
/** @var array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} */
|
||||
private array $overrides;
|
||||
|
||||
/**
|
||||
* @param list<'expand'> $adjustments
|
||||
* @param array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} $overrides
|
||||
*/
|
||||
public function __construct(
|
||||
?RuleSetDefinitionInterface $original,
|
||||
array $adjustments,
|
||||
array $overrides
|
||||
) {
|
||||
$this->original = $original;
|
||||
$this->adjustments = $adjustments;
|
||||
$this->overrides = $overrides;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->overrides[__FUNCTION__]
|
||||
?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown description'); // @phpstan-ignore method.dynamicName
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
$value = $this->overrides[__FUNCTION__]
|
||||
?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown name'); // @phpstan-ignore method.dynamicName
|
||||
|
||||
if (\in_array('expand', $this->adjustments, true)) {
|
||||
$value .= ' (expanded)';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getRules(): array
|
||||
{
|
||||
$value = $this->overrides[__FUNCTION__]
|
||||
?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName
|
||||
|
||||
if (null === $value) {
|
||||
throw new \LogicException('Cannot get rules from unknown original rule set and missing overrides.');
|
||||
}
|
||||
|
||||
if (\in_array('expand', $this->adjustments, true)) {
|
||||
$value = (new RuleSet($value))->getRules();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function isRisky(): bool
|
||||
{
|
||||
$value = $this->overrides[__FUNCTION__]
|
||||
?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName
|
||||
|
||||
if (null === $value) {
|
||||
throw new \LogicException('Cannot get isRisky from unknown original rule set and missing overrides.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DescribeNameNotFoundException extends \InvalidArgumentException
|
||||
{
|
||||
private string $name;
|
||||
|
||||
/**
|
||||
* 'rule'|'set'.
|
||||
*/
|
||||
private string $type;
|
||||
|
||||
public function __construct(string $name, string $type)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\ConfigInterface;
|
||||
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\Console\ConfigurationResolver;
|
||||
use PhpCsFixer\Console\Output\ErrorOutput;
|
||||
use PhpCsFixer\Console\Output\OutputContext;
|
||||
use PhpCsFixer\Console\Output\Progress\ProgressOutputFactory;
|
||||
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
|
||||
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
|
||||
use PhpCsFixer\Console\Report\FixReport\ReportSummary;
|
||||
use PhpCsFixer\Error\ErrorsManager;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\FixerFactory;
|
||||
use PhpCsFixer\RuleSet\RuleSets;
|
||||
use PhpCsFixer\Runner\Event\FileProcessed;
|
||||
use PhpCsFixer\Runner\Runner;
|
||||
use PhpCsFixer\ToolInfoInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*
|
||||
* @TODO 4.0: mark as final
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'fix', description: 'Fixes a directory or a file.')]
|
||||
/* final */ class FixCommand extends Command
|
||||
{
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
private ErrorsManager $errorsManager;
|
||||
|
||||
private Stopwatch $stopwatch;
|
||||
|
||||
private ConfigInterface $defaultConfig;
|
||||
|
||||
private ToolInfoInterface $toolInfo;
|
||||
|
||||
private ProgressOutputFactory $progressOutputFactory;
|
||||
|
||||
public function __construct(ToolInfoInterface $toolInfo)
|
||||
{
|
||||
parent::__construct('fix');
|
||||
$this->setDescription('Fixes a directory or a file.');
|
||||
|
||||
$this->eventDispatcher = new EventDispatcher();
|
||||
$this->errorsManager = new ErrorsManager();
|
||||
$this->stopwatch = new Stopwatch();
|
||||
$this->defaultConfig = new Config();
|
||||
$this->toolInfo = $toolInfo;
|
||||
$this->progressOutputFactory = new ProgressOutputFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Override here to only generate the help copy when used.
|
||||
*/
|
||||
public function getHelp(): string
|
||||
{
|
||||
return <<<'EOF'
|
||||
The <info>%command.name%</info> command tries to %command.name% as much coding standards
|
||||
problems as possible on a given file or files in a given directory and its subdirectories:
|
||||
|
||||
<info>$ php %command.full_name% /path/to/dir</info>
|
||||
<info>$ php %command.full_name% /path/to/file</info>
|
||||
|
||||
By default <comment>--path-mode</comment> is set to `override`, which means, that if you specify the path to a file or a directory via
|
||||
command arguments, then the paths provided to a `Finder` in config file will be ignored. You can use <comment>--path-mode=intersection</comment>
|
||||
to merge paths from the config file and from the argument:
|
||||
|
||||
<info>$ php %command.full_name% --path-mode=intersection /path/to/dir</info>
|
||||
|
||||
The <comment>--format</comment> option for the output format. Supported formats are `@auto` (default one on v4+), `txt` (default one on v3), `json`, `xml`, `checkstyle`, `junit` and `gitlab`.
|
||||
|
||||
* `@auto` aims to auto-select best reporter for given CI or local execution (resolution into best format is outside of BC promise and is future-ready)
|
||||
* `gitlab` for GitLab
|
||||
* `@auto,{format}` takes `@auto` under CI, and {format} otherwise
|
||||
|
||||
NOTE: the output for the following formats are generated in accordance with schemas
|
||||
|
||||
* `checkstyle` follows the common `"checkstyle" XML schema </doc/schemas/fix/checkstyle.xsd>`_
|
||||
* `gitlab` follows the `codeclimate JSON schema </doc/schemas/fix/codeclimate.json>`_
|
||||
* `json` follows the `own JSON schema </doc/schemas/fix/schema.json>`_
|
||||
* `junit` follows the `JUnit XML schema from Jenkins </doc/schemas/fix/junit-10.xsd>`_
|
||||
* `xml` follows the `own XML schema </doc/schemas/fix/xml.xsd>`_
|
||||
|
||||
The <comment>--quiet</comment> Do not output any message.
|
||||
|
||||
The <comment>--verbose</comment> option will show the applied rules. When using the `txt` format it will also display progress notifications.
|
||||
|
||||
NOTE: if there is an error like "errors reported during linting after fixing", you can use this to be even more verbose for debugging purpose
|
||||
|
||||
* `-v`: verbose
|
||||
* `-vv`: very verbose
|
||||
* `-vvv`: debug
|
||||
|
||||
EOF. /* @TODO: 4.0 - change to @PER */ <<<'EOF'
|
||||
|
||||
The <comment>--rules</comment> option allows to explicitly select rules to use,
|
||||
overriding the default PSR-12 or your own project config:
|
||||
|
||||
<info>$ php %command.full_name% . --rules=line_ending,full_opening_tag,indentation_type</info>
|
||||
|
||||
You can also exclude the rules you don't want by placing a dash in front of the rule name, like <comment>-name_of_fixer</comment>.
|
||||
|
||||
<info>$ php %command.full_name% . --rules=@Symfony,-@PSR1,-blank_line_before_statement,strict_comparison</info>
|
||||
|
||||
Complete configuration for rules can be supplied using a `json` formatted string as well.
|
||||
|
||||
<info>$ php %command.full_name% . --rules='{"concat_space": {"spacing": "none"}}'</info>
|
||||
|
||||
The <comment>--dry-run</comment> flag will run the fixer without making changes to your files.
|
||||
|
||||
The <comment>--sequential</comment> flag will enforce sequential analysis even if parallel config is provided.
|
||||
|
||||
The <comment>--diff</comment> flag can be used to let the fixer output all the changes it makes.
|
||||
|
||||
The <comment>--allow-risky</comment> option (pass `yes` or `no`) allows you to set whether risky rules may run. Default value is taken from config file.
|
||||
A rule is considered risky if it could change code behaviour. By default no risky rules are run.
|
||||
|
||||
The <comment>--stop-on-violation</comment> flag stops the execution upon first file that needs to be fixed.
|
||||
|
||||
The <comment>--show-progress</comment> option allows you to choose the way process progress is rendered:
|
||||
|
||||
* <comment>none</comment>: disables progress output;
|
||||
* <comment>dots</comment>: multiline progress output with number of files and percentage on each line.
|
||||
* <comment>bar</comment>: single line progress output with number of files and calculated percentage.
|
||||
|
||||
If the option is not provided, it defaults to <comment>bar</comment> unless a config file that disables output is used, in which case it defaults to <comment>none</comment>. This option has no effect if the verbosity of the command is less than <comment>verbose</comment>.
|
||||
|
||||
<info>$ php %command.full_name% --verbose --show-progress=dots</info>
|
||||
|
||||
By using <comment>--using-cache</comment> option with `yes` or `no` you can set if the caching
|
||||
mechanism should be used.
|
||||
|
||||
The command can also read from standard input, in which case it won't
|
||||
automatically fix anything:
|
||||
|
||||
<info>$ cat foo.php | php %command.full_name% --diff -</info>
|
||||
|
||||
Finally, if you don't need BC kept on CLI level, you might use `PHP_CS_FIXER_FUTURE_MODE` to start using options that
|
||||
would be default in next MAJOR release and to forbid using deprecated configuration:
|
||||
|
||||
<info>$ PHP_CS_FIXER_FUTURE_MODE=1 php %command.full_name% -v --diff</info>
|
||||
|
||||
Exit code
|
||||
---------
|
||||
|
||||
Exit code of the `%command.name%` command is built using following bit flags:
|
||||
|
||||
* 0 - OK.
|
||||
* 1 - General error (or PHP minimal requirement not matched).
|
||||
* 4 - Some files have invalid syntax (only in dry-run mode).
|
||||
* 8 - Some files need fixing (only in dry-run mode).
|
||||
* 16 - Configuration error of the application.
|
||||
* 32 - Configuration error of a Fixer.
|
||||
* 64 - Exception raised within the application.
|
||||
|
||||
EOF;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$reporterFactory = new ReporterFactory();
|
||||
$reporterFactory->registerBuiltInReporters();
|
||||
$formats = $reporterFactory->getFormats();
|
||||
array_unshift($formats, '@auto', '@auto,txt');
|
||||
|
||||
$progressOutputTypes = ProgressOutputType::all();
|
||||
|
||||
$this->setDefinition(
|
||||
[
|
||||
new InputArgument('path', InputArgument::IS_ARRAY, 'The path(s) that rules will be run against (each path can be a file or directory).'),
|
||||
new InputOption('path-mode', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Specify path mode (%s).', ConfigurationResolver::PATH_MODE_VALUES), ConfigurationResolver::PATH_MODE_OVERRIDE, ConfigurationResolver::PATH_MODE_VALUES),
|
||||
new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Are risky fixers allowed (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
|
||||
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a config file.'),
|
||||
new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
|
||||
new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'List of rules that should be run against configured paths.', null, static function () {
|
||||
$fixerFactory = new FixerFactory();
|
||||
$fixerFactory->registerBuiltInFixers();
|
||||
$fixers = array_map(static fn (FixerInterface $fixer) => $fixer->getName(), $fixerFactory->getFixers());
|
||||
|
||||
return array_merge(RuleSets::getSetDefinitionNames(), $fixers);
|
||||
}),
|
||||
new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Should cache be used (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
|
||||
new InputOption('allow-unsupported-php-version', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Should the command refuse to run on unsupported PHP version (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
|
||||
new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
|
||||
new InputOption('diff', '', InputOption::VALUE_NONE, 'Prints diff for each file.'),
|
||||
new InputOption('format', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('To output results in other formats (%s).', $formats), null, $formats),
|
||||
new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
|
||||
new InputOption('show-progress', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Type of progress indicator (%s).', $progressOutputTypes), null, $progressOutputTypes),
|
||||
new InputOption('sequential', '', InputOption::VALUE_NONE, 'Enforce sequential analysis.'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$verbosity = $output->getVerbosity();
|
||||
|
||||
$passedConfig = $input->getOption('config');
|
||||
$passedRules = $input->getOption('rules');
|
||||
|
||||
if (null !== $passedConfig && ConfigurationResolver::IGNORE_CONFIG_FILE !== $passedConfig && null !== $passedRules) {
|
||||
throw new InvalidConfigurationException('Passing both `--config` and `--rules` options is not allowed.');
|
||||
}
|
||||
|
||||
$resolver = new ConfigurationResolver(
|
||||
$this->defaultConfig,
|
||||
[
|
||||
'allow-risky' => $input->getOption('allow-risky'),
|
||||
'config' => $passedConfig,
|
||||
'dry-run' => $this->isDryRun($input),
|
||||
'rules' => $passedRules,
|
||||
'path' => $input->getArgument('path'),
|
||||
'path-mode' => $input->getOption('path-mode'),
|
||||
'using-cache' => $input->getOption('using-cache'),
|
||||
'allow-unsupported-php-version' => $input->getOption('allow-unsupported-php-version'),
|
||||
'cache-file' => $input->getOption('cache-file'),
|
||||
'format' => $input->getOption('format'),
|
||||
'diff' => $input->getOption('diff'),
|
||||
'stop-on-violation' => $input->getOption('stop-on-violation'),
|
||||
'verbosity' => $verbosity,
|
||||
'show-progress' => $input->getOption('show-progress'),
|
||||
'sequential' => $input->getOption('sequential'),
|
||||
],
|
||||
getcwd(), // @phpstan-ignore argument.type
|
||||
$this->toolInfo,
|
||||
);
|
||||
|
||||
$reporter = $resolver->getReporter();
|
||||
|
||||
$stdErr = $output instanceof ConsoleOutputInterface
|
||||
? $output->getErrorOutput()
|
||||
: ('txt' === $reporter->getFormat() ? $output : null);
|
||||
|
||||
if (null !== $stdErr) {
|
||||
$stdErr->writeln(Application::getAboutWithRuntime(true));
|
||||
|
||||
if (version_compare(\PHP_VERSION, ConfigInterface::PHP_VERSION_SYNTAX_SUPPORTED.'.99', '>')) {
|
||||
$message = \sprintf(
|
||||
'PHP CS Fixer currently supports PHP syntax only up to PHP %s, current PHP version: %s.',
|
||||
ConfigInterface::PHP_VERSION_SYNTAX_SUPPORTED,
|
||||
\PHP_VERSION,
|
||||
);
|
||||
|
||||
if (!$resolver->getUnsupportedPhpVersionAllowed()) {
|
||||
$message .= ' Add `Config::setUnsupportedPhpVersionAllowed(true)` to allow executions on unsupported PHP versions. Such execution may be unstable and you may experience code modified in a wrong way.';
|
||||
$stdErr->writeln(\sprintf(
|
||||
$stdErr->isDecorated() ? '<bg=red;fg=white;>%s</>' : '%s',
|
||||
$message,
|
||||
));
|
||||
|
||||
return 1;
|
||||
}
|
||||
$message .= ' Execution may be unstable. You may experience code modified in a wrong way. Please report such cases at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer. Remove Config::setUnsupportedPhpVersionAllowed(true) to allow executions only on supported PHP versions.';
|
||||
$stdErr->writeln(\sprintf(
|
||||
$stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s',
|
||||
$message,
|
||||
));
|
||||
}
|
||||
|
||||
$configFile = $resolver->getConfigFile();
|
||||
$stdErr->writeln(\sprintf('Loaded config <comment>%s</comment>%s.', $resolver->getConfig()->getName(), null === $configFile ? '' : ' from "'.$configFile.'"'));
|
||||
|
||||
if (null === $configFile && ConfigurationResolver::IGNORE_CONFIG_FILE !== $passedConfig && null === $passedRules) {
|
||||
if (false === $input->isInteractive()) {
|
||||
$stdErr->writeln(
|
||||
\sprintf(
|
||||
$stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s',
|
||||
'No config file found. Please create one using `php-cs-fixer init`.',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
$io = new SymfonyStyle($input, $stdErr);
|
||||
$shallCreateConfigFile = 'yes' === $io->choice(
|
||||
'Do you want to create the config file?',
|
||||
['yes', 'no'],
|
||||
'yes',
|
||||
);
|
||||
if ($shallCreateConfigFile) {
|
||||
$returnCode = $this->getApplication()->doRun(
|
||||
new ArrayInput([
|
||||
'command' => 'init',
|
||||
]),
|
||||
$output,
|
||||
);
|
||||
$stdErr->writeln('Config file created, re-run the command to put it in action.');
|
||||
|
||||
return $returnCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$isParallel = $resolver->getParallelConfig()->getMaxProcesses() > 1;
|
||||
|
||||
$stdErr->writeln(\sprintf(
|
||||
'Running analysis on %d core%s.',
|
||||
$resolver->getParallelConfig()->getMaxProcesses(),
|
||||
$isParallel ? \sprintf(
|
||||
's with %d file%s per process',
|
||||
$resolver->getParallelConfig()->getFilesPerProcess(),
|
||||
$resolver->getParallelConfig()->getFilesPerProcess() > 1 ? 's' : '',
|
||||
) : ' sequentially',
|
||||
));
|
||||
|
||||
if ($resolver->getUsingCache()) {
|
||||
$cacheFile = $resolver->getCacheFile();
|
||||
|
||||
if (is_file($cacheFile)) {
|
||||
$stdErr->writeln(\sprintf('Using cache file "%s".', $cacheFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$finder = new \ArrayIterator(array_filter(
|
||||
iterator_to_array($resolver->getFinder()),
|
||||
static fn (\SplFileInfo $fileInfo) => false !== $fileInfo->getRealPath(),
|
||||
));
|
||||
|
||||
if (null !== $stdErr) {
|
||||
if ($resolver->configFinderIsOverridden()) {
|
||||
$stdErr->writeln(
|
||||
\sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', 'Paths from configuration have been overridden by paths provided as command arguments.'),
|
||||
);
|
||||
}
|
||||
|
||||
if ($resolver->configRulesAreOverridden()) {
|
||||
$stdErr->writeln(
|
||||
\sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', 'Rules from configuration have been overridden by rules provided as command argument.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$progressType = $resolver->getProgressType();
|
||||
$progressOutput = $this->progressOutputFactory->create(
|
||||
$progressType,
|
||||
new OutputContext(
|
||||
$stdErr,
|
||||
(new Terminal())->getWidth(),
|
||||
\count($finder),
|
||||
),
|
||||
);
|
||||
|
||||
$runner = new Runner(
|
||||
$finder,
|
||||
$resolver->getFixers(),
|
||||
$resolver->getDiffer(),
|
||||
ProgressOutputType::NONE !== $progressType ? $this->eventDispatcher : null,
|
||||
$this->errorsManager,
|
||||
$resolver->getLinter(),
|
||||
$resolver->isDryRun(),
|
||||
$resolver->getCacheManager(),
|
||||
$resolver->getDirectory(),
|
||||
$resolver->shouldStopOnViolation(),
|
||||
$resolver->getParallelConfig(),
|
||||
$input,
|
||||
$resolver->getConfigFile(),
|
||||
$resolver->getRuleCustomisationPolicy(),
|
||||
);
|
||||
|
||||
$this->eventDispatcher->addListener(FileProcessed::NAME, [$progressOutput, 'onFixerFileProcessed']);
|
||||
$this->stopwatch->start('fixFiles');
|
||||
$changed = $runner->fix();
|
||||
$this->stopwatch->stop('fixFiles');
|
||||
$this->eventDispatcher->removeListener(FileProcessed::NAME, [$progressOutput, 'onFixerFileProcessed']);
|
||||
|
||||
$progressOutput->printLegend();
|
||||
|
||||
$fixEvent = $this->stopwatch->getEvent('fixFiles');
|
||||
|
||||
$reportSummary = new ReportSummary(
|
||||
$changed,
|
||||
\count($finder),
|
||||
(int) $fixEvent->getDuration(), // ignore microseconds fraction
|
||||
memory_get_peak_usage(true) + $runner->getWorkersMemoryUsage(),
|
||||
OutputInterface::VERBOSITY_VERBOSE <= $verbosity,
|
||||
$resolver->isDryRun(),
|
||||
$output->isDecorated(),
|
||||
);
|
||||
|
||||
$output->isDecorated()
|
||||
? $output->write($reporter->generate($reportSummary))
|
||||
: $output->write($reporter->generate($reportSummary), false, OutputInterface::OUTPUT_RAW);
|
||||
|
||||
$invalidErrors = $this->errorsManager->getInvalidErrors();
|
||||
$exceptionErrors = $this->errorsManager->getExceptionErrors();
|
||||
$lintErrors = $this->errorsManager->getLintErrors();
|
||||
|
||||
if (null !== $stdErr) {
|
||||
$errorOutput = new ErrorOutput($stdErr);
|
||||
|
||||
if (\count($invalidErrors) > 0) {
|
||||
$errorOutput->listErrors('linting before fixing', $invalidErrors);
|
||||
}
|
||||
|
||||
if (\count($exceptionErrors) > 0) {
|
||||
$errorOutput->listErrors('fixing', $exceptionErrors);
|
||||
if ($isParallel) {
|
||||
$stdErr->writeln('To see details of the error(s), re-run the command with `--sequential -vvv [file]`');
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($lintErrors) > 0) {
|
||||
$errorOutput->listErrors('linting after fixing', $lintErrors);
|
||||
}
|
||||
}
|
||||
|
||||
$exitStatusCalculator = new FixCommandExitStatusCalculator();
|
||||
|
||||
return $exitStatusCalculator->calculate(
|
||||
$resolver->isDryRun(),
|
||||
\count($changed) > 0,
|
||||
\count($invalidErrors) > 0,
|
||||
\count($exceptionErrors) > 0,
|
||||
\count($lintErrors) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
protected function isDryRun(InputInterface $input): bool
|
||||
{
|
||||
return $input->getOption('dry-run'); // @phpstan-ignore symfonyConsole.optionNotFound (Because PHPStan doesn't recognise the method is overridden in the child class and this parameter is _not_ used in the child class.)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class FixCommandExitStatusCalculator
|
||||
{
|
||||
// Exit status 1 is reserved for environment constraints not matched.
|
||||
public const EXIT_STATUS_FLAG_HAS_INVALID_FILES = 4;
|
||||
public const EXIT_STATUS_FLAG_HAS_CHANGED_FILES = 8;
|
||||
public const EXIT_STATUS_FLAG_HAS_INVALID_CONFIG = 16;
|
||||
public const EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG = 32;
|
||||
public const EXIT_STATUS_FLAG_EXCEPTION_IN_APP = 64;
|
||||
|
||||
public function calculate(
|
||||
bool $isDryRun,
|
||||
bool $hasChangedFiles,
|
||||
bool $hasInvalidErrors,
|
||||
bool $hasExceptionErrors,
|
||||
bool $hasLintErrorsAfterFixing
|
||||
): int {
|
||||
$exitStatus = 0;
|
||||
|
||||
if ($isDryRun) {
|
||||
if ($hasChangedFiles) {
|
||||
$exitStatus |= self::EXIT_STATUS_FLAG_HAS_CHANGED_FILES;
|
||||
}
|
||||
|
||||
if ($hasInvalidErrors) {
|
||||
$exitStatus |= self::EXIT_STATUS_FLAG_HAS_INVALID_FILES;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasExceptionErrors || $hasLintErrorsAfterFixing) {
|
||||
$exitStatus |= self::EXIT_STATUS_FLAG_EXCEPTION_IN_APP;
|
||||
}
|
||||
|
||||
return $exitStatus;
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
|
||||
use PhpCsFixer\FixerConfiguration\FixerOptionInterface;
|
||||
use PhpCsFixer\Utils;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\HelpCommand as BaseHelpCommand;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'help')]
|
||||
final class HelpCommand extends BaseHelpCommand
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('help');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the description of an option to include its allowed values.
|
||||
*
|
||||
* @param string $description description with a single `%s` placeholder for the allowed values
|
||||
* @param non-empty-list<string> $allowedValues
|
||||
*/
|
||||
public static function getDescriptionWithAllowedValues(string $description, array $allowedValues): string
|
||||
{
|
||||
$allowedValues = Utils::naturalLanguageJoinWithBackticks($allowedValues, 'or');
|
||||
|
||||
return \sprintf($description, 'can be '.$allowedValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed values of the given option that can be converted to a string.
|
||||
*
|
||||
* @return null|non-empty-list<AllowedValueSubset|mixed>
|
||||
*/
|
||||
public static function getDisplayableAllowedValues(FixerOptionInterface $option): ?array
|
||||
{
|
||||
$allowed = $option->getAllowedValues();
|
||||
|
||||
if (null !== $allowed) {
|
||||
$allowed = array_filter($allowed, static fn ($value): bool => !$value instanceof \Closure);
|
||||
|
||||
usort($allowed, static function ($valueA, $valueB): int {
|
||||
if ($valueA instanceof AllowedValueSubset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($valueB instanceof AllowedValueSubset) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return strcasecmp(
|
||||
Utils::toString($valueA),
|
||||
Utils::toString($valueB),
|
||||
);
|
||||
});
|
||||
|
||||
if (0 === \count($allowed)) {
|
||||
$allowed = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$output->getFormatter()->setStyle('url', new OutputFormatterStyle('blue'));
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\RuleSets;
|
||||
use PhpCsFixer\RuleSet\Sets\AutoRiskySet;
|
||||
use PhpCsFixer\RuleSet\Sets\AutoSet;
|
||||
use PhpCsFixer\RuleSet\Sets\PhpCsFixerRiskySet;
|
||||
use PhpCsFixer\RuleSet\Sets\PhpCsFixerSet;
|
||||
use PhpCsFixer\RuleSet\Sets\SymfonyRiskySet;
|
||||
use PhpCsFixer\RuleSet\Sets\SymfonySet;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'init', description: 'Create config file.')]
|
||||
final class InitCommand extends Command
|
||||
{
|
||||
private const FIXER_FILENAME = '.php-cs-fixer.dist.php';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('init');
|
||||
$this->setDescription('Create config file.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$stdErr = $output;
|
||||
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$stdErr = $output->getErrorOutput();
|
||||
$stdErr->writeln(Application::getAboutWithRuntime(true));
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $stdErr);
|
||||
|
||||
$io->warning('This command is experimental');
|
||||
|
||||
if (file_exists(self::FIXER_FILENAME)) {
|
||||
$io->error(\sprintf('Configuration file `%s` already exists.', self::FIXER_FILENAME));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note([
|
||||
'While we start, we must tell you that we put our diligence to NOT change the meaning of your codebase.',
|
||||
'Yet, some of the rules are explicitly _risky_ to apply. A rule is _risky_ if it could change code behaviour, e.g. transforming `==` into `===` or removal of trailing whitespaces within multiline strings.',
|
||||
'Such rules are improving your codebase even further, yet you shall always review changes proposed by _risky_ rules carefully.',
|
||||
]);
|
||||
$isRiskyAllowed = 'yes' === $io->choice(
|
||||
'Do you want to enable _risky_ rules?',
|
||||
['yes', 'no'],
|
||||
'no',
|
||||
);
|
||||
|
||||
$setsByName = RuleSets::getBuiltInSetDefinitions();
|
||||
|
||||
$setAuto = new AutoSet();
|
||||
$setAutoRisky = new AutoRiskySet();
|
||||
$setAutoWithOptionalRiskySetNamesTextual = $isRiskyAllowed ? '`@auto`/`@auto:risky`' : '`@auto`';
|
||||
|
||||
$io->note("We recommend usage of {$setAutoWithOptionalRiskySetNamesTextual} rulesets. They take insights from your existing `composer.json` to configure project the best:");
|
||||
|
||||
$generateSetsBehindAutoSet = static function () use ($setAuto, $setAutoRisky, $isRiskyAllowed): array {
|
||||
$sets = array_merge(
|
||||
array_keys($setAuto->getRulesCandidates()),
|
||||
$isRiskyAllowed ? array_keys($setAutoRisky->getRulesCandidates()) : [],
|
||||
);
|
||||
natcasesort($sets);
|
||||
|
||||
return $sets;
|
||||
};
|
||||
$setsBehindAutoSet = $generateSetsBehindAutoSet();
|
||||
|
||||
$io->listing(
|
||||
array_map(
|
||||
static fn (RuleSetDefinitionInterface $item): string => \sprintf(
|
||||
'<fg=blue>`%s`</> - %s',
|
||||
$item->getName(),
|
||||
$item->getDescription(),
|
||||
),
|
||||
array_map(
|
||||
static fn (string $name): RuleSetDefinitionInterface => $setsByName[$name], // @phpstan-ignore-line offsetAccess.notFound
|
||||
$setsBehindAutoSet,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$rules = [];
|
||||
|
||||
$useAutoSet = 'yes' === $io->choice(
|
||||
"Do you want to use <fg=blue>{$setAutoWithOptionalRiskySetNamesTextual}</> ruleset?",
|
||||
['yes', 'no'],
|
||||
'yes',
|
||||
);
|
||||
|
||||
if ($useAutoSet) {
|
||||
$rules[] = $setAuto->getName();
|
||||
if ($isRiskyAllowed) {
|
||||
$rules[] = $setAutoRisky->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$generateExtraSets = static function () use ($isRiskyAllowed): array {
|
||||
$setSymfony = new SymfonySet();
|
||||
$setPhpCsFixer = new PhpCsFixerSet();
|
||||
|
||||
$extraSets = [
|
||||
$setSymfony->getName(),
|
||||
$setPhpCsFixer->getName(),
|
||||
];
|
||||
|
||||
if ($isRiskyAllowed) {
|
||||
$setSymfonyRisky = new SymfonyRiskySet();
|
||||
$setPhpCsFixerRisky = new PhpCsFixerRiskySet();
|
||||
|
||||
$extraSets[] = $setSymfonyRisky->getName();
|
||||
$extraSets[] = $setPhpCsFixerRisky->getName();
|
||||
}
|
||||
|
||||
return $extraSets;
|
||||
};
|
||||
|
||||
$extraSets = array_merge(
|
||||
false === $useAutoSet ? $setsBehindAutoSet : [],
|
||||
$generateExtraSets(),
|
||||
);
|
||||
natcasesort($extraSets);
|
||||
|
||||
$sets = $io->choice(
|
||||
'Do you want to use any of other recommended ruleset? (multi-choice)',
|
||||
array_combine(
|
||||
$extraSets,
|
||||
array_map(
|
||||
static fn (string $item): string => $setsByName[$item]->getDescription(), // @phpstan-ignore-line offsetAccess.notFound
|
||||
$extraSets,
|
||||
),
|
||||
) + ['none' => 'none'],
|
||||
'none',
|
||||
true,
|
||||
);
|
||||
|
||||
// older Symfony version can return single string instead of array with single string, let's unify
|
||||
if (!\is_array($sets)) {
|
||||
$sets = [$sets];
|
||||
}
|
||||
|
||||
$rules = array_merge(
|
||||
$rules,
|
||||
array_unique(array_filter($sets, static fn ($item) => 'none' !== $item)),
|
||||
);
|
||||
|
||||
$readResult = @file_get_contents(__DIR__.'/../../../resources/.php-cs-fixer.dist.php.template');
|
||||
if (false === $readResult) {
|
||||
throw new IOException('Failed to read template file.');
|
||||
}
|
||||
|
||||
$content = str_replace(
|
||||
[
|
||||
'/*{{ IS_RISKY_ALLOWED }}*/',
|
||||
'/*{{ RULES }}*/',
|
||||
],
|
||||
[
|
||||
$isRiskyAllowed ? 'true' : 'false',
|
||||
"[\n".implode(
|
||||
",\n",
|
||||
array_map(
|
||||
static fn ($item) => " '{$item}' => true",
|
||||
$rules,
|
||||
),
|
||||
)."\n ]",
|
||||
],
|
||||
$readResult,
|
||||
);
|
||||
|
||||
$writeResult = @file_put_contents(self::FIXER_FILENAME, $content);
|
||||
if (false === $writeResult) {
|
||||
throw new IOException(\sprintf('Failed to write file "%s".', self::FIXER_FILENAME));
|
||||
}
|
||||
|
||||
$io->success(\sprintf('Configuration file created successfully as `%s`.', self::FIXER_FILENAME));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\ConfigInterface;
|
||||
use PhpCsFixer\Console\ConfigurationResolver;
|
||||
use PhpCsFixer\ToolInfoInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
/**
|
||||
* @author Markus Staab <markus.staab@redaxo.org>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'list-files', description: 'List all files being fixed by the given config.')]
|
||||
final class ListFilesCommand extends Command
|
||||
{
|
||||
private ConfigInterface $defaultConfig;
|
||||
|
||||
private ToolInfoInterface $toolInfo;
|
||||
|
||||
public function __construct(ToolInfoInterface $toolInfo)
|
||||
{
|
||||
parent::__construct('list-files');
|
||||
$this->setDescription('List all files being fixed by the given config.');
|
||||
|
||||
$this->defaultConfig = new Config();
|
||||
$this->toolInfo = $toolInfo;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDefinition(
|
||||
[
|
||||
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$passedConfig = $input->getOption('config');
|
||||
|
||||
$cwd = getcwd();
|
||||
\assert(false !== $cwd);
|
||||
|
||||
$resolver = new ConfigurationResolver(
|
||||
$this->defaultConfig,
|
||||
[
|
||||
'config' => $passedConfig,
|
||||
],
|
||||
$cwd,
|
||||
$this->toolInfo,
|
||||
);
|
||||
|
||||
$finder = $resolver->getFinder();
|
||||
|
||||
foreach ($finder as $file) {
|
||||
if ($file->isFile()) {
|
||||
$relativePath = './'.Path::makeRelative($file->getRealPath(), $cwd);
|
||||
// unify directory separators across operating system
|
||||
$relativePath = str_replace('/', \DIRECTORY_SEPARATOR, $relativePath);
|
||||
|
||||
$output->writeln(escapeshellarg($relativePath));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
|
||||
use PhpCsFixer\Console\Report\ListRulesReport\ReporterFactory;
|
||||
use PhpCsFixer\Console\Report\ListRulesReport\ReporterInterface;
|
||||
use PhpCsFixer\Console\Report\ListRulesReport\ReportSummary;
|
||||
use PhpCsFixer\Console\Report\ListRulesReport\TextReporter;
|
||||
use PhpCsFixer\FixerFactory;
|
||||
use PhpCsFixer\Utils;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'list-rules', description: 'List all available Rules.')]
|
||||
final class ListRulesCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('list-rules');
|
||||
$this->setDescription('List all available Rules.');
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$reporterFactory = new ReporterFactory();
|
||||
$reporterFactory->registerBuiltInReporters();
|
||||
$formats = $reporterFactory->getFormats();
|
||||
\assert([] !== $formats);
|
||||
|
||||
$this->setDefinition(
|
||||
[
|
||||
new InputOption('format', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('To output results in other formats (%s).', $formats), (new TextReporter())->getFormat(), $formats),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$reporter = $this->resolveReporterWithFactory(
|
||||
$input->getOption('format'),
|
||||
new ReporterFactory(),
|
||||
);
|
||||
|
||||
$fixerFactory = new FixerFactory();
|
||||
$fixerFactory->registerBuiltInFixers();
|
||||
|
||||
$reportSummary = new ReportSummary(
|
||||
$fixerFactory->getFixers(),
|
||||
);
|
||||
|
||||
$report = $reporter->generate($reportSummary);
|
||||
|
||||
$output->isDecorated()
|
||||
? $output->write(OutputFormatter::escape($report))
|
||||
: $output->write($report, false, OutputInterface::OUTPUT_RAW);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function resolveReporterWithFactory(string $format, ReporterFactory $factory): ReporterInterface
|
||||
{
|
||||
try {
|
||||
$factory->registerBuiltInReporters();
|
||||
$reporter = $factory->getReporter($format);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$formats = $factory->getFormats();
|
||||
sort($formats);
|
||||
|
||||
throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
|
||||
}
|
||||
|
||||
return $reporter;
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
|
||||
use PhpCsFixer\Console\Report\ListSetsReport\ReporterFactory;
|
||||
use PhpCsFixer\Console\Report\ListSetsReport\ReporterInterface;
|
||||
use PhpCsFixer\Console\Report\ListSetsReport\ReportSummary;
|
||||
use PhpCsFixer\Console\Report\ListSetsReport\TextReporter;
|
||||
use PhpCsFixer\RuleSet\RuleSets;
|
||||
use PhpCsFixer\Utils;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'list-sets', description: 'List all available RuleSets.')]
|
||||
final class ListSetsCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('list-sets');
|
||||
$this->setDescription('List all available RuleSets.');
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$reporterFactory = new ReporterFactory();
|
||||
$reporterFactory->registerBuiltInReporters();
|
||||
$formats = $reporterFactory->getFormats();
|
||||
\assert([] !== $formats);
|
||||
|
||||
$this->setDefinition(
|
||||
[
|
||||
new InputOption('format', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('To output results in other formats (%s).', $formats), (new TextReporter())->getFormat(), $formats),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$reporter = $this->resolveReporterWithFactory(
|
||||
$input->getOption('format'),
|
||||
new ReporterFactory(),
|
||||
);
|
||||
|
||||
$reportSummary = new ReportSummary(
|
||||
array_values(RuleSets::getSetDefinitions()),
|
||||
);
|
||||
|
||||
$report = $reporter->generate($reportSummary);
|
||||
|
||||
$output->isDecorated()
|
||||
? $output->write(OutputFormatter::escape($report))
|
||||
: $output->write($report, false, OutputInterface::OUTPUT_RAW);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function resolveReporterWithFactory(string $format, ReporterFactory $factory): ReporterInterface
|
||||
{
|
||||
try {
|
||||
$factory->registerBuiltInReporters();
|
||||
$reporter = $factory->getReporter($format);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$formats = $factory->getFormats();
|
||||
sort($formats);
|
||||
|
||||
throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
|
||||
}
|
||||
|
||||
return $reporter;
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\Console\SelfUpdate\NewVersionCheckerInterface;
|
||||
use PhpCsFixer\PharCheckerInterface;
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\ToolInfoInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Igor Wiedler <igor@wiedler.ch>
|
||||
* @author Stephane PY <py.stephane1@gmail.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'self-update', description: 'Update php-cs-fixer.phar to the latest stable version.')]
|
||||
final class SelfUpdateCommand extends Command
|
||||
{
|
||||
private NewVersionCheckerInterface $versionChecker;
|
||||
|
||||
private ToolInfoInterface $toolInfo;
|
||||
|
||||
private PharCheckerInterface $pharChecker;
|
||||
|
||||
public function __construct(
|
||||
NewVersionCheckerInterface $versionChecker,
|
||||
ToolInfoInterface $toolInfo,
|
||||
PharCheckerInterface $pharChecker
|
||||
) {
|
||||
parent::__construct('self-update');
|
||||
$this->setDescription('Update php-cs-fixer.phar to the latest stable version.');
|
||||
|
||||
$this->versionChecker = $versionChecker;
|
||||
$this->toolInfo = $toolInfo;
|
||||
$this->pharChecker = $pharChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Override here to only generate the help copy when used.
|
||||
*/
|
||||
public function getHelp(): string
|
||||
{
|
||||
return <<<'EOT'
|
||||
The <info>%command.name%</info> command replace your php-cs-fixer.phar by the
|
||||
latest version released on:
|
||||
<comment>https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases</comment>
|
||||
|
||||
<info>$ php php-cs-fixer.phar %command.name%</info>
|
||||
|
||||
EOT;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setAliases(['selfupdate'])
|
||||
->setDefinition(
|
||||
[
|
||||
new InputOption('--force', '-f', InputOption::VALUE_NONE, 'Force update to next major version if available.'),
|
||||
],
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$stdErr = $output->getErrorOutput();
|
||||
$stdErr->writeln(Application::getAboutWithRuntime(true));
|
||||
}
|
||||
|
||||
if (!$this->toolInfo->isInstalledAsPhar()) {
|
||||
$output->writeln('<error>Self-update is available only for PHAR version.</error>');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$currentVersion = $this->getApplication()->getVersion();
|
||||
|
||||
try {
|
||||
if (Preg::match('/^v?(?<major>\d+)\./', $currentVersion, $matches)) {
|
||||
$currentMajor = (int) $matches['major'];
|
||||
} else {
|
||||
throw new \Exception('Unable to determine major version.');
|
||||
}
|
||||
|
||||
$latestVersion = $this->versionChecker->getLatestVersion();
|
||||
$latestVersionOfCurrentMajor = $this->versionChecker->getLatestVersionOfMajor($currentMajor);
|
||||
} catch (\Exception $exception) {
|
||||
$output->writeln(\sprintf(
|
||||
'<error>Unable to determine newest version: %s</error>',
|
||||
$exception->getMessage(),
|
||||
));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (1 !== $this->versionChecker->compareVersions($latestVersion, $currentVersion)) {
|
||||
$output->writeln('<info>PHP CS Fixer is already up-to-date.</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remoteTag = $latestVersion;
|
||||
|
||||
if (
|
||||
0 !== $this->versionChecker->compareVersions($latestVersionOfCurrentMajor, $latestVersion)
|
||||
&& true !== $input->getOption('force')
|
||||
) {
|
||||
$output->writeln(\sprintf('<info>A new major version of PHP CS Fixer is available</info> (<comment>%s</comment>)', $latestVersion));
|
||||
$output->writeln(\sprintf('<info>Before upgrading please read</info> https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/%s/UPGRADE-v%s.md', $latestVersion, $currentMajor + 1));
|
||||
$output->writeln('<info>If you are ready to upgrade run this command with</info> <comment>-f</comment>');
|
||||
$output->writeln('<info>Checking for new minor/patch version...</info>');
|
||||
|
||||
if (1 !== $this->versionChecker->compareVersions($latestVersionOfCurrentMajor, $currentVersion)) {
|
||||
$output->writeln('<info>No minor update for PHP CS Fixer.</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remoteTag = $latestVersionOfCurrentMajor;
|
||||
}
|
||||
|
||||
\assert(isset($_SERVER['argv']));
|
||||
$localFilename = $_SERVER['argv'][0];
|
||||
$realPath = realpath($localFilename);
|
||||
if (false !== $realPath) {
|
||||
$localFilename = $realPath;
|
||||
}
|
||||
|
||||
if (!is_writable($localFilename)) {
|
||||
$output->writeln(\sprintf('<error>No permission to update</error> "%s" <error>file.</error>', $localFilename));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$tempFilename = \dirname($localFilename).'/'.basename($localFilename, '.phar').'-tmp.phar';
|
||||
$remoteFilename = $this->toolInfo->getPharDownloadUri($remoteTag);
|
||||
|
||||
if (false === @copy($remoteFilename, $tempFilename)) {
|
||||
$output->writeln(\sprintf('<error>Unable to download new version</error> %s <error>from the server.</error>', $remoteTag));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
chmod($tempFilename, 0777 & ~umask());
|
||||
|
||||
$pharInvalidityReason = $this->pharChecker->checkFileValidity($tempFilename);
|
||||
if (null !== $pharInvalidityReason) {
|
||||
unlink($tempFilename);
|
||||
$output->writeln(\sprintf('<error>The download of</error> %s <error>is corrupt (%s).</error>', $remoteTag, $pharInvalidityReason));
|
||||
$output->writeln('<error>Please re-run the "self-update" command to try again.</error>');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('\\' === \DIRECTORY_SEPARATOR) {
|
||||
// On Windows rename() fails to overwrite the .phar file being executed, so we need to copy() and unlink() instead.
|
||||
copy($tempFilename, $localFilename);
|
||||
@unlink($tempFilename);
|
||||
} else {
|
||||
rename($tempFilename, $localFilename);
|
||||
}
|
||||
|
||||
$output->writeln(\sprintf('<info>PHP CS Fixer updated</info> (<comment>%s</comment> -> <comment>%s</comment>)', $currentVersion, $remoteTag));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Command;
|
||||
|
||||
use Clue\React\NDJson\Decoder;
|
||||
use Clue\React\NDJson\Encoder;
|
||||
use PhpCsFixer\Cache\NullCacheManager;
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Console\ConfigurationResolver;
|
||||
use PhpCsFixer\Error\ErrorsManager;
|
||||
use PhpCsFixer\Runner\Event\FileProcessed;
|
||||
use PhpCsFixer\Runner\Parallel\ParallelAction;
|
||||
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
|
||||
use PhpCsFixer\Runner\Parallel\ParallelisationException;
|
||||
use PhpCsFixer\Runner\Runner;
|
||||
use PhpCsFixer\ToolInfoInterface;
|
||||
use React\EventLoop\StreamSelectLoop;
|
||||
use React\Socket\ConnectionInterface;
|
||||
use React\Socket\TcpConnector;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @author Greg Korba <greg@codito.dev>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
#[AsCommand(name: 'worker', description: 'Internal command for running fixers in parallel', hidden: true)]
|
||||
final class WorkerCommand extends Command
|
||||
{
|
||||
/** @var string Prefix used before JSON-encoded error printed in the worker's process */
|
||||
public const ERROR_PREFIX = 'WORKER_ERROR::';
|
||||
|
||||
private ToolInfoInterface $toolInfo;
|
||||
private ConfigurationResolver $configurationResolver;
|
||||
private ErrorsManager $errorsManager;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
/** @var list<FileProcessed> */
|
||||
private array $events;
|
||||
|
||||
public function __construct(ToolInfoInterface $toolInfo)
|
||||
{
|
||||
parent::__construct('worker');
|
||||
$this->setDescription('Internal command for running fixers in parallel');
|
||||
|
||||
$this->setHidden(true);
|
||||
$this->toolInfo = $toolInfo;
|
||||
$this->errorsManager = new ErrorsManager();
|
||||
$this->eventDispatcher = new EventDispatcher();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDefinition(
|
||||
[
|
||||
new InputOption('port', null, InputOption::VALUE_REQUIRED, 'Specifies parallelisation server\'s port.'),
|
||||
new InputOption('identifier', null, InputOption::VALUE_REQUIRED, 'Specifies parallelisation process\' identifier.'),
|
||||
new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Are risky fixers allowed (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
|
||||
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a config file.'),
|
||||
new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
|
||||
new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'List of rules that should be run against configured paths.'),
|
||||
new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Should cache be used (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
|
||||
new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
|
||||
new InputOption('diff', '', InputOption::VALUE_NONE, 'Prints diff for each file.'),
|
||||
new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$errorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
|
||||
$identifier = $input->getOption('identifier');
|
||||
$port = $input->getOption('port');
|
||||
|
||||
if (null === $identifier || !is_numeric($port)) {
|
||||
throw new ParallelisationException('Missing parallelisation options');
|
||||
}
|
||||
|
||||
try {
|
||||
$runner = $this->createRunner($input);
|
||||
} catch (\Throwable $e) {
|
||||
throw new ParallelisationException('Unable to create runner: '.$e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$loop = new StreamSelectLoop();
|
||||
$tcpConnector = new TcpConnector($loop);
|
||||
$tcpConnector
|
||||
->connect(\sprintf('127.0.0.1:%d', $port))
|
||||
// @codeCoverageIgnoreStart
|
||||
->then(
|
||||
function (ConnectionInterface $connection) use ($loop, $runner, $identifier): void {
|
||||
$out = new Encoder($connection, \JSON_INVALID_UTF8_IGNORE);
|
||||
$in = new Decoder($connection, true, 512, \JSON_INVALID_UTF8_IGNORE);
|
||||
|
||||
// [REACT] Initialise connection with the parallelisation operator
|
||||
$out->write(['action' => ParallelAction::WORKER_HELLO, 'identifier' => $identifier]);
|
||||
|
||||
$handleError = static function (\Throwable $error) use ($out): void {
|
||||
$out->write([
|
||||
'action' => ParallelAction::WORKER_ERROR_REPORT,
|
||||
'class' => \get_class($error),
|
||||
'message' => $error->getMessage(),
|
||||
'file' => $error->getFile(),
|
||||
'line' => $error->getLine(),
|
||||
'code' => $error->getCode(),
|
||||
'trace' => $error->getTraceAsString(),
|
||||
]);
|
||||
};
|
||||
$out->on('error', $handleError);
|
||||
$in->on('error', $handleError);
|
||||
|
||||
// [REACT] Listen for messages from the parallelisation operator (analysis requests)
|
||||
$in->on('data', function (array $json) use ($loop, $runner, $out): void {
|
||||
\assert(isset($json['action']));
|
||||
|
||||
$action = $json['action'];
|
||||
|
||||
// Parallelisation operator does not have more to do, let's close the connection
|
||||
if (ParallelAction::RUNNER_THANK_YOU === $action) {
|
||||
// no payload to assert on
|
||||
|
||||
$loop->stop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ParallelAction::RUNNER_REQUEST_ANALYSIS !== $action) {
|
||||
// At this point we only expect analysis requests, if any other action happen, we need to fix the code.
|
||||
throw new \LogicException(\sprintf('Unexpected action ParallelAction::%s.', $action));
|
||||
}
|
||||
|
||||
\assert(isset(
|
||||
$json['files'],
|
||||
));
|
||||
|
||||
/** @var iterable<int, string> $files */
|
||||
$files = $json['files'];
|
||||
|
||||
foreach ($files as $path) {
|
||||
// Reset events because we want to collect only those coming from analysed files chunk
|
||||
$this->events = [];
|
||||
$runner->setFileIterator(new \ArrayIterator([new \SplFileInfo($path)]));
|
||||
$analysisResult = $runner->fix();
|
||||
|
||||
if (1 !== \count($this->events)) {
|
||||
throw new ParallelisationException('Runner did not report a fixing event or reported too many.');
|
||||
}
|
||||
|
||||
if (1 < \count($analysisResult)) {
|
||||
throw new ParallelisationException('Runner returned more analysis results than expected.');
|
||||
}
|
||||
|
||||
$out->write([
|
||||
'action' => ParallelAction::WORKER_RESULT,
|
||||
'errors' => $this->errorsManager->forPath($path),
|
||||
'file' => $path,
|
||||
'fileHash' => $this->events[0]->getFileHash(),
|
||||
'fixInfo' => array_pop($analysisResult),
|
||||
'memoryUsage' => memory_get_peak_usage(true),
|
||||
'status' => $this->events[0]->getStatus(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Request another file chunk (if available, the parallelisation operator will request new "run" action)
|
||||
$out->write(['action' => ParallelAction::WORKER_GET_FILE_CHUNK]);
|
||||
});
|
||||
},
|
||||
static function (\Throwable $error) use ($errorOutput): void {
|
||||
// @TODO Verify onRejected behaviour → https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7777#discussion_r1590399285
|
||||
$errorOutput->writeln($error->getMessage());
|
||||
},
|
||||
)
|
||||
// @codeCoverageIgnoreEnd
|
||||
;
|
||||
|
||||
$loop->run();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function createRunner(InputInterface $input): Runner
|
||||
{
|
||||
$passedConfig = $input->getOption('config');
|
||||
$passedRules = $input->getOption('rules');
|
||||
|
||||
if (null !== $passedConfig && ConfigurationResolver::IGNORE_CONFIG_FILE !== $passedConfig && null !== $passedRules) {
|
||||
throw new \RuntimeException('Passing both `--config` and `--rules` options is not allowed');
|
||||
}
|
||||
|
||||
// There's no one single source of truth when it comes to fixing single file, we need to collect statuses from events.
|
||||
$this->eventDispatcher->addListener(FileProcessed::NAME, function (FileProcessed $event): void {
|
||||
$this->events[] = $event;
|
||||
});
|
||||
|
||||
$this->configurationResolver = new ConfigurationResolver(
|
||||
new Config(),
|
||||
[
|
||||
'allow-risky' => $input->getOption('allow-risky'),
|
||||
'config' => $passedConfig,
|
||||
'dry-run' => $input->getOption('dry-run'),
|
||||
'rules' => $passedRules,
|
||||
'path' => [],
|
||||
'path-mode' => ConfigurationResolver::PATH_MODE_OVERRIDE, // IMPORTANT! WorkerCommand is called with file that already passed filtering, so here we can rely on PATH_MODE_OVERRIDE.
|
||||
'using-cache' => $input->getOption('using-cache'),
|
||||
'cache-file' => $input->getOption('cache-file'),
|
||||
'diff' => $input->getOption('diff'),
|
||||
'stop-on-violation' => $input->getOption('stop-on-violation'),
|
||||
],
|
||||
getcwd(), // @phpstan-ignore argument.type
|
||||
$this->toolInfo,
|
||||
);
|
||||
|
||||
return new Runner(
|
||||
null, // Paths are known when parallelisation server requests new chunk, not now
|
||||
$this->configurationResolver->getFixers(),
|
||||
$this->configurationResolver->getDiffer(),
|
||||
$this->eventDispatcher,
|
||||
$this->errorsManager,
|
||||
$this->configurationResolver->getLinter(),
|
||||
$this->configurationResolver->isDryRun(),
|
||||
new NullCacheManager(), // IMPORTANT! We pass null cache, as cache is read&write in main process and we do not need to do it again.
|
||||
$this->configurationResolver->getDirectory(),
|
||||
$this->configurationResolver->shouldStopOnViolation(),
|
||||
ParallelConfigFactory::sequential(), // IMPORTANT! Worker must run in sequential mode.
|
||||
null,
|
||||
$this->configurationResolver->getConfigFile(),
|
||||
$this->configurationResolver->getRuleCustomisationPolicy(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1064
File diff suppressed because it is too large
Load Diff
+160
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output;
|
||||
|
||||
use PhpCsFixer\Differ\DiffConsoleFormatter;
|
||||
use PhpCsFixer\Error\Error;
|
||||
use PhpCsFixer\Linter\LintingException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ErrorOutput
|
||||
{
|
||||
private OutputInterface $output;
|
||||
|
||||
private bool $isDecorated;
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
$this->isDecorated = $output->isDecorated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Error> $errors
|
||||
*/
|
||||
public function listErrors(string $process, array $errors): void
|
||||
{
|
||||
$this->output->writeln(['', \sprintf(
|
||||
'Files that were not fixed due to errors reported during %s:',
|
||||
$process,
|
||||
)]);
|
||||
|
||||
$showDetails = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
|
||||
$showTrace = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
|
||||
foreach ($errors as $i => $error) {
|
||||
$this->output->writeln(\sprintf('%4d) %s', $i + 1, $error->getFilePath()));
|
||||
$e = $error->getSource();
|
||||
if (!$showDetails || null === $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = \sprintf('[%s]', \get_class($e));
|
||||
$message = $e->getMessage();
|
||||
$code = $e->getCode();
|
||||
if (0 !== $code) {
|
||||
$message .= " ({$code})";
|
||||
}
|
||||
|
||||
$length = max(\strlen($class), \strlen($message));
|
||||
$lines = [
|
||||
'',
|
||||
$class,
|
||||
$message,
|
||||
'',
|
||||
];
|
||||
|
||||
$this->output->writeln('');
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (\strlen($line) < $length) {
|
||||
$line .= str_repeat(' ', $length - \strlen($line));
|
||||
}
|
||||
|
||||
$this->output->writeln(\sprintf(' <error> %s </error>', $this->prepareOutput($line)));
|
||||
}
|
||||
|
||||
if ($showTrace && !$e instanceof LintingException) { // stack trace of lint exception is of no interest
|
||||
$this->output->writeln('');
|
||||
$stackTrace = $e->getTrace();
|
||||
foreach ($stackTrace as $trace) {
|
||||
if (isset($trace['class']) && Command::class === $trace['class'] && 'run' === $trace['function']) {
|
||||
$this->output->writeln(' [ ... ]');
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$this->outputTrace($trace);
|
||||
}
|
||||
}
|
||||
|
||||
if (Error::TYPE_LINT === $error->getType() && 0 < \count($error->getAppliedFixers())) {
|
||||
$this->output->writeln('');
|
||||
$this->output->writeln(\sprintf(' Applied fixers: <comment>%s</comment>', implode(', ', $error->getAppliedFixers())));
|
||||
|
||||
$diff = $error->getDiff();
|
||||
if ('' !== $diff) {
|
||||
$diffFormatter = new DiffConsoleFormatter(
|
||||
$this->isDecorated,
|
||||
\sprintf(
|
||||
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
|
||||
\PHP_EOL,
|
||||
\PHP_EOL,
|
||||
),
|
||||
);
|
||||
|
||||
$this->output->writeln($diffFormatter->format($diff));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* function?: string,
|
||||
* line?: int,
|
||||
* file?: string,
|
||||
* class?: class-string,
|
||||
* type?: '->'|'::',
|
||||
* args?: list<mixed>,
|
||||
* object?: object,
|
||||
* } $trace
|
||||
*/
|
||||
private function outputTrace(array $trace): void
|
||||
{
|
||||
if (isset($trace['class'], $trace['type'], $trace['function'])) {
|
||||
$this->output->writeln(\sprintf(
|
||||
' <comment>%s</comment>%s<comment>%s()</comment>',
|
||||
$this->prepareOutput($trace['class']),
|
||||
$this->prepareOutput($trace['type']),
|
||||
$this->prepareOutput($trace['function']),
|
||||
));
|
||||
} elseif (isset($trace['function'])) {
|
||||
$this->output->writeln(\sprintf(' <comment>%s()</comment>', $this->prepareOutput($trace['function'])));
|
||||
}
|
||||
|
||||
if (isset($trace['file'])) {
|
||||
$this->output->writeln(
|
||||
\sprintf(' in <info>%s</info>', $this->prepareOutput($trace['file']))
|
||||
.(isset($trace['line']) ? \sprintf(' at line <info>%d</info>', $trace['line']) : ' at unknown line'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareOutput(string $string): string
|
||||
{
|
||||
return $this->isDecorated
|
||||
? OutputFormatter::escape($string)
|
||||
: $string;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class OutputContext
|
||||
{
|
||||
private ?OutputInterface $output;
|
||||
private int $terminalWidth;
|
||||
private int $filesCount;
|
||||
|
||||
public function __construct(
|
||||
?OutputInterface $output,
|
||||
int $terminalWidth,
|
||||
int $filesCount
|
||||
) {
|
||||
$this->output = $output;
|
||||
$this->terminalWidth = $terminalWidth;
|
||||
$this->filesCount = $filesCount;
|
||||
}
|
||||
|
||||
public function getOutput(): ?OutputInterface
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
public function getTerminalWidth(): int
|
||||
{
|
||||
return $this->terminalWidth;
|
||||
}
|
||||
|
||||
public function getFilesCount(): int
|
||||
{
|
||||
return $this->filesCount;
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output\Progress;
|
||||
|
||||
use PhpCsFixer\Console\Output\OutputContext;
|
||||
use PhpCsFixer\Runner\Event\FileProcessed;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Output writer to show the progress of a FixCommand using dots and meaningful letters.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DotsOutput implements ProgressOutputInterface
|
||||
{
|
||||
/**
|
||||
* File statuses map.
|
||||
*
|
||||
* @var array<FileProcessed::STATUS_*, array{symbol: string, format: string, description: string}>
|
||||
*/
|
||||
private const EVENT_STATUS_MAP = [
|
||||
FileProcessed::STATUS_NO_CHANGES => ['symbol' => '.', 'format' => '%s', 'description' => 'no changes'],
|
||||
FileProcessed::STATUS_FIXED => ['symbol' => 'F', 'format' => '<fg=green>%s</fg=green>', 'description' => 'fixed'],
|
||||
FileProcessed::STATUS_SKIPPED => ['symbol' => 'S', 'format' => '<fg=cyan>%s</fg=cyan>', 'description' => 'skipped (cached or empty file)'],
|
||||
FileProcessed::STATUS_NON_MONOLITHIC => ['symbol' => 'M', 'format' => '<bg=magenta>%s</bg=magenta>', 'description' => 'skipped (non-monolithic)'],
|
||||
FileProcessed::STATUS_INVALID => ['symbol' => 'I', 'format' => '<bg=red>%s</bg=red>', 'description' => 'invalid file syntax (file ignored)'],
|
||||
FileProcessed::STATUS_EXCEPTION => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
|
||||
FileProcessed::STATUS_LINT => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
|
||||
];
|
||||
|
||||
/** @readonly */
|
||||
private OutputContext $context;
|
||||
|
||||
private int $processedFiles = 0;
|
||||
|
||||
private int $symbolsPerLine;
|
||||
|
||||
public function __construct(OutputContext $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
// max number of characters per line
|
||||
// - total length x 2 (e.g. " 1 / 123" => 6 digits and padding spaces)
|
||||
// - 11 (extra spaces, parentheses and percentage characters, e.g. " x / x (100%)")
|
||||
$this->symbolsPerLine = max(1, $context->getTerminalWidth() - \strlen((string) $context->getFilesCount()) * 2 - 11);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is not intended to be serialized,
|
||||
* and cannot be deserialized (see __wakeup method).
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.self::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the deserialization of the class to prevent attacker executing
|
||||
* code by leveraging the __destruct method.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.self::class);
|
||||
}
|
||||
|
||||
public function onFixerFileProcessed(FileProcessed $event): void
|
||||
{
|
||||
$status = self::EVENT_STATUS_MAP[$event->getStatus()];
|
||||
$this->getOutput()->write($this->getOutput()->isDecorated() ? \sprintf($status['format'], $status['symbol']) : $status['symbol']);
|
||||
|
||||
++$this->processedFiles;
|
||||
|
||||
$symbolsOnCurrentLine = $this->processedFiles % $this->symbolsPerLine;
|
||||
$isLast = $this->processedFiles === $this->context->getFilesCount();
|
||||
|
||||
if (0 === $symbolsOnCurrentLine || $isLast) {
|
||||
$this->getOutput()->write(\sprintf(
|
||||
'%s %'.\strlen((string) $this->context->getFilesCount()).'d / %d (%3d%%)',
|
||||
$isLast && 0 !== $symbolsOnCurrentLine ? str_repeat(' ', $this->symbolsPerLine - $symbolsOnCurrentLine) : '',
|
||||
$this->processedFiles,
|
||||
$this->context->getFilesCount(),
|
||||
round($this->processedFiles / $this->context->getFilesCount() * 100),
|
||||
));
|
||||
|
||||
if (!$isLast) {
|
||||
$this->getOutput()->writeln('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function printLegend(): void
|
||||
{
|
||||
$symbols = [];
|
||||
|
||||
foreach (self::EVENT_STATUS_MAP as $status) {
|
||||
$symbol = $status['symbol'];
|
||||
if (isset($symbols[$symbol])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$symbols[$symbol] = \sprintf('%s-%s', $this->getOutput()->isDecorated() ? \sprintf($status['format'], $symbol) : $symbol, $status['description']);
|
||||
}
|
||||
|
||||
$this->getOutput()->write(\sprintf("\nLegend: %s\n", implode(', ', $symbols)));
|
||||
}
|
||||
|
||||
private function getOutput(): OutputInterface
|
||||
{
|
||||
return $this->context->getOutput();
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output\Progress;
|
||||
|
||||
use PhpCsFixer\Runner\Event\FileProcessed;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class NullOutput implements ProgressOutputInterface
|
||||
{
|
||||
public function printLegend(): void {}
|
||||
|
||||
public function onFixerFileProcessed(FileProcessed $event): void {}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output\Progress;
|
||||
|
||||
use PhpCsFixer\Console\Output\OutputContext;
|
||||
use PhpCsFixer\Runner\Event\FileProcessed;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
/**
|
||||
* Output writer to show the progress of a FixCommand using progress bar (percentage).
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class PercentageBarOutput implements ProgressOutputInterface
|
||||
{
|
||||
/** @readonly */
|
||||
private OutputContext $context;
|
||||
|
||||
private ProgressBar $progressBar;
|
||||
|
||||
public function __construct(OutputContext $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
$this->progressBar = new ProgressBar($context->getOutput(), $this->context->getFilesCount());
|
||||
$this->progressBar->setBarCharacter('▓'); // dark shade character \u2593
|
||||
$this->progressBar->setEmptyBarCharacter('░'); // light shade character \u2591
|
||||
$this->progressBar->setProgressCharacter('');
|
||||
$this->progressBar->setFormat('normal');
|
||||
|
||||
$this->progressBar->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is not intended to be serialized,
|
||||
* and cannot be deserialized (see __wakeup method).
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.self::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the deserialization of the class to prevent attacker executing
|
||||
* code by leveraging the __destruct method.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.self::class);
|
||||
}
|
||||
|
||||
public function onFixerFileProcessed(FileProcessed $event): void
|
||||
{
|
||||
$this->progressBar->advance(1);
|
||||
|
||||
if ($this->progressBar->getProgress() === $this->progressBar->getMaxSteps()) {
|
||||
$this->context->getOutput()->write("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
public function printLegend(): void {}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output\Progress;
|
||||
|
||||
use PhpCsFixer\Console\Output\OutputContext;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ProgressOutputFactory
|
||||
{
|
||||
/**
|
||||
* @var array<ProgressOutputType::*, class-string<ProgressOutputInterface>>
|
||||
*/
|
||||
private const OUTPUT_TYPE_MAP = [
|
||||
ProgressOutputType::NONE => NullOutput::class,
|
||||
ProgressOutputType::DOTS => DotsOutput::class,
|
||||
ProgressOutputType::BAR => PercentageBarOutput::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ProgressOutputType::* $outputType
|
||||
*/
|
||||
public function create(string $outputType, OutputContext $context): ProgressOutputInterface
|
||||
{
|
||||
if (null === $context->getOutput()) {
|
||||
$outputType = ProgressOutputType::NONE;
|
||||
}
|
||||
|
||||
if (!$this->isBuiltInType($outputType)) {
|
||||
throw new \InvalidArgumentException(
|
||||
\sprintf(
|
||||
'Something went wrong, "%s" output type is not supported',
|
||||
$outputType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$outputClass = self::OUTPUT_TYPE_MAP[$outputType];
|
||||
|
||||
// @phpstan-ignore-next-line new.noConstructor
|
||||
return new $outputClass($context);
|
||||
}
|
||||
|
||||
private function isBuiltInType(string $outputType): bool
|
||||
{
|
||||
return \in_array($outputType, ProgressOutputType::all(), true);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output\Progress;
|
||||
|
||||
use PhpCsFixer\Runner\Event\FileProcessed;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface ProgressOutputInterface
|
||||
{
|
||||
public function printLegend(): void;
|
||||
|
||||
public function onFixerFileProcessed(FileProcessed $event): void;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Output\Progress;
|
||||
|
||||
/**
|
||||
* @TODO PHP 8.1 switch this and similar classes to ENUM
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ProgressOutputType
|
||||
{
|
||||
public const NONE = 'none';
|
||||
public const DOTS = 'dots';
|
||||
public const BAR = 'bar';
|
||||
|
||||
/**
|
||||
* @return non-empty-list<ProgressOutputType::*>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
self::BAR,
|
||||
self::DOTS,
|
||||
self::NONE,
|
||||
];
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* @author Kévin Gomez <contact@kevingomez.fr>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class CheckstyleReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'checkstyle';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
if (!\extension_loaded('dom')) {
|
||||
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
|
||||
}
|
||||
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
$checkstyles = $dom->appendChild($dom->createElement('checkstyle'));
|
||||
\assert($checkstyles instanceof \DOMElement);
|
||||
|
||||
$checkstyles->setAttribute('version', Application::getAbout());
|
||||
|
||||
foreach ($reportSummary->getChanged() as $filePath => $fixResult) {
|
||||
$file = $checkstyles->appendChild($dom->createElement('file'));
|
||||
\assert($file instanceof \DOMElement);
|
||||
|
||||
$file->setAttribute('name', $filePath);
|
||||
|
||||
foreach ($fixResult['appliedFixers'] as $appliedFixer) {
|
||||
$error = $this->createError($dom, $appliedFixer);
|
||||
$file->appendChild($error);
|
||||
}
|
||||
}
|
||||
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$result = $dom->saveXML();
|
||||
if (false === $result) {
|
||||
throw new \RuntimeException('Failed to generate XML output');
|
||||
}
|
||||
|
||||
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($result) : $result;
|
||||
}
|
||||
|
||||
private function createError(\DOMDocument $dom, string $appliedFixer): \DOMElement
|
||||
{
|
||||
$error = $dom->createElement('error');
|
||||
$error->setAttribute('severity', 'warning');
|
||||
$error->setAttribute('source', 'PHP-CS-Fixer.'.$appliedFixer);
|
||||
$error->setAttribute('message', 'Found violation(s) of type: '.$appliedFixer);
|
||||
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\Documentation\DocumentationLocator;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\FixerFactory;
|
||||
use SebastianBergmann\Diff\Chunk;
|
||||
use SebastianBergmann\Diff\Diff;
|
||||
use SebastianBergmann\Diff\Line;
|
||||
use SebastianBergmann\Diff\Parser;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* Generates a report according to gitlabs subset of codeclimate json files.
|
||||
*
|
||||
* @author Hans-Christian Otto <c.otto@suora.com>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @see https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class GitlabReporter implements ReporterInterface
|
||||
{
|
||||
private Parser $diffParser;
|
||||
private DocumentationLocator $documentationLocator;
|
||||
private FixerFactory $fixerFactory;
|
||||
|
||||
/**
|
||||
* @var array<string, FixerInterface>
|
||||
*/
|
||||
private array $fixers;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->diffParser = new Parser();
|
||||
$this->documentationLocator = new DocumentationLocator();
|
||||
|
||||
$this->fixerFactory = new FixerFactory();
|
||||
$this->fixerFactory->registerBuiltInFixers();
|
||||
|
||||
$this->fixers = $this->createFixers();
|
||||
}
|
||||
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'gitlab';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process changed files array. Returns generated report.
|
||||
*/
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$about = Application::getAbout();
|
||||
|
||||
$report = [];
|
||||
foreach ($reportSummary->getChanged() as $fileName => $change) {
|
||||
foreach ($change['appliedFixers'] as $fixerName) {
|
||||
$fixer = $this->fixers[$fixerName] ?? null;
|
||||
|
||||
$report[] = [
|
||||
'check_name' => 'PHP-CS-Fixer.'.$fixerName,
|
||||
'description' => null !== $fixer
|
||||
? $fixer->getDefinition()->getSummary()
|
||||
: 'PHP-CS-Fixer.'.$fixerName.' (custom rule)',
|
||||
'content' => [
|
||||
'body' => \sprintf(
|
||||
"%s\n%s",
|
||||
$about,
|
||||
null !== $fixer
|
||||
? \sprintf(
|
||||
'Check [docs](https://cs.symfony.com/doc/rules/%s.html) for more information.',
|
||||
substr($this->documentationLocator->getFixerDocumentationFileRelativePath($fixer), 0, -4), // -4 to drop `.rst`
|
||||
)
|
||||
: 'Check performed with a custom rule.',
|
||||
),
|
||||
],
|
||||
'categories' => ['Style'],
|
||||
'fingerprint' => md5($fileName.$fixerName),
|
||||
'severity' => 'minor',
|
||||
'location' => [
|
||||
'path' => $fileName,
|
||||
'lines' => self::getLines(
|
||||
array_values( // before PHPUnit 13, result of `->parse(...)` is array and not list
|
||||
$this->diffParser->parse($change['diff']),
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$jsonString = json_encode($report, \JSON_THROW_ON_ERROR);
|
||||
|
||||
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($jsonString) : $jsonString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Diff> $diffs
|
||||
*
|
||||
* @return array{begin: int, end: int}
|
||||
*/
|
||||
private static function getLines(array $diffs): array
|
||||
{
|
||||
if (isset($diffs[0])) {
|
||||
$firstDiff = $diffs[0];
|
||||
|
||||
$firstChunk = \Closure::bind(static fn (Diff $diff) => array_shift($diff->chunks), null, $firstDiff)($firstDiff);
|
||||
|
||||
if ($firstChunk instanceof Chunk) {
|
||||
return self::getBeginEndForDiffChunk($firstChunk);
|
||||
}
|
||||
}
|
||||
|
||||
return ['begin' => 0, 'end' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{begin: int, end: int}
|
||||
*/
|
||||
private static function getBeginEndForDiffChunk(Chunk $chunk): array
|
||||
{
|
||||
$start = \Closure::bind(static fn (Chunk $chunk): int => $chunk->start, null, $chunk)($chunk);
|
||||
$startRange = \Closure::bind(static fn (Chunk $chunk): int => $chunk->startRange, null, $chunk)($chunk);
|
||||
$lines = \Closure::bind(static fn (Chunk $chunk): array => $chunk->lines, null, $chunk)($chunk);
|
||||
|
||||
\assert(\count($lines) > 0);
|
||||
|
||||
$firstModifiedLineOffset = array_find_key($lines, static function (Line $line): bool {
|
||||
$type = \Closure::bind(static fn (Line $line): int => $line->type, null, $line)($line);
|
||||
|
||||
return Line::UNCHANGED !== $type;
|
||||
});
|
||||
\assert(\is_int($firstModifiedLineOffset));
|
||||
|
||||
return [
|
||||
// offset the start by where the first line is actually modified
|
||||
'begin' => $start + $firstModifiedLineOffset,
|
||||
// it's not where last modification takes place, only where diff (with --context) ends
|
||||
'end' => $start + $startRange,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, FixerInterface>
|
||||
*/
|
||||
private function createFixers(): array
|
||||
{
|
||||
$fixers = [];
|
||||
|
||||
foreach ($this->fixerFactory->getFixers() as $fixer) {
|
||||
$fixers[$fixer->getName()] = $fixer;
|
||||
}
|
||||
|
||||
ksort($fixers);
|
||||
|
||||
return $fixers;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class JsonReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'json';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$jsonFiles = [];
|
||||
|
||||
foreach ($reportSummary->getChanged() as $file => $fixResult) {
|
||||
$jsonFile = ['name' => $file];
|
||||
|
||||
if ($reportSummary->shouldAddAppliedFixers()) {
|
||||
$jsonFile['appliedFixers'] = $fixResult['appliedFixers'];
|
||||
}
|
||||
|
||||
if ('' !== $fixResult['diff']) {
|
||||
$jsonFile['diff'] = $fixResult['diff'];
|
||||
}
|
||||
|
||||
$jsonFiles[] = $jsonFile;
|
||||
}
|
||||
|
||||
$json = [
|
||||
'about' => Application::getAbout(),
|
||||
'files' => $jsonFiles,
|
||||
'time' => [
|
||||
'total' => round($reportSummary->getTime() / 1_000, 3),
|
||||
],
|
||||
'memory' => round($reportSummary->getMemory() / 1_024 / 1_024, 3),
|
||||
];
|
||||
|
||||
$json = json_encode($json, \JSON_THROW_ON_ERROR);
|
||||
|
||||
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($json) : $json;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\Preg;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class JunitReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'junit';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
if (!\extension_loaded('dom')) {
|
||||
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
|
||||
}
|
||||
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$testsuites = $dom->appendChild($dom->createElement('testsuites'));
|
||||
|
||||
$testsuite = $testsuites->appendChild($dom->createElement('testsuite'));
|
||||
\assert($testsuite instanceof \DOMElement);
|
||||
|
||||
$testsuite->setAttribute('name', 'PHP CS Fixer');
|
||||
|
||||
$properties = $dom->createElement('properties');
|
||||
$property = $dom->createElement('property');
|
||||
$property->setAttribute('name', 'about');
|
||||
$property->setAttribute('value', Application::getAbout());
|
||||
$properties->appendChild($property);
|
||||
$testsuite->appendChild($properties);
|
||||
|
||||
if (\count($reportSummary->getChanged()) > 0) {
|
||||
$this->createFailedTestCases($dom, $testsuite, $reportSummary);
|
||||
} else {
|
||||
$this->createSuccessTestCase($dom, $testsuite);
|
||||
}
|
||||
|
||||
if ($reportSummary->getTime() > 0) {
|
||||
$testsuite->setAttribute(
|
||||
'time',
|
||||
\sprintf(
|
||||
'%.3f',
|
||||
$reportSummary->getTime() / 1_000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$result = $dom->saveXML();
|
||||
if (false === $result) {
|
||||
throw new \RuntimeException('Failed to generate XML output');
|
||||
}
|
||||
|
||||
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($result) : $result;
|
||||
}
|
||||
|
||||
private function createSuccessTestCase(\DOMDocument $dom, \DOMElement $testsuite): void
|
||||
{
|
||||
$testcase = $dom->createElement('testcase');
|
||||
$testcase->setAttribute('name', 'All OK');
|
||||
$testcase->setAttribute('assertions', '1');
|
||||
|
||||
$testsuite->appendChild($testcase);
|
||||
$testsuite->setAttribute('tests', '1');
|
||||
$testsuite->setAttribute('assertions', '1');
|
||||
$testsuite->setAttribute('failures', '0');
|
||||
$testsuite->setAttribute('errors', '0');
|
||||
}
|
||||
|
||||
private function createFailedTestCases(\DOMDocument $dom, \DOMElement $testsuite, ReportSummary $reportSummary): void
|
||||
{
|
||||
$assertionsCount = 0;
|
||||
foreach ($reportSummary->getChanged() as $file => $fixResult) {
|
||||
$testcase = $this->createFailedTestCase(
|
||||
$dom,
|
||||
$file,
|
||||
$fixResult,
|
||||
$reportSummary->shouldAddAppliedFixers(),
|
||||
);
|
||||
$testsuite->appendChild($testcase);
|
||||
$assertionsCount += (int) $testcase->getAttribute('assertions');
|
||||
}
|
||||
|
||||
$testsuite->setAttribute('tests', (string) \count($reportSummary->getChanged()));
|
||||
$testsuite->setAttribute('assertions', (string) $assertionsCount);
|
||||
$testsuite->setAttribute('failures', (string) $assertionsCount);
|
||||
$testsuite->setAttribute('errors', '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{appliedFixers: list<string>, diff: string} $fixResult
|
||||
*/
|
||||
private function createFailedTestCase(\DOMDocument $dom, string $file, array $fixResult, bool $shouldAddAppliedFixers): \DOMElement
|
||||
{
|
||||
$appliedFixersCount = \count($fixResult['appliedFixers']);
|
||||
|
||||
$testName = str_replace('.', '_DOT_', Preg::replace('@\.'.pathinfo($file, \PATHINFO_EXTENSION).'$@', '', $file));
|
||||
|
||||
$testcase = $dom->createElement('testcase');
|
||||
$testcase->setAttribute('name', $testName);
|
||||
$testcase->setAttribute('file', $file);
|
||||
$testcase->setAttribute('assertions', (string) $appliedFixersCount);
|
||||
|
||||
$failure = $dom->createElement('failure');
|
||||
$failure->setAttribute('type', 'code_style');
|
||||
$testcase->appendChild($failure);
|
||||
|
||||
if ($shouldAddAppliedFixers) {
|
||||
$failureContent = "applied fixers:\n---------------\n";
|
||||
|
||||
foreach ($fixResult['appliedFixers'] as $appliedFixer) {
|
||||
$failureContent .= "* {$appliedFixer}\n";
|
||||
}
|
||||
} else {
|
||||
$failureContent = "Wrong code style\n";
|
||||
}
|
||||
|
||||
if ('' !== $fixResult['diff']) {
|
||||
$failureContent .= "\nDiff:\n---------------\n\n".$fixResult['diff'];
|
||||
}
|
||||
|
||||
$failure->appendChild($dom->createCDATASection(trim($failureContent)));
|
||||
|
||||
return $testcase;
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ReportSummary
|
||||
{
|
||||
/**
|
||||
* @var array<string, array{appliedFixers: list<string>, diff: string}>
|
||||
*/
|
||||
private array $changed;
|
||||
|
||||
private int $filesCount;
|
||||
|
||||
private int $time;
|
||||
|
||||
private int $memory;
|
||||
|
||||
private bool $addAppliedFixers;
|
||||
|
||||
private bool $isDryRun;
|
||||
|
||||
private bool $isDecoratedOutput;
|
||||
|
||||
/**
|
||||
* @param array<string, array{appliedFixers: list<string>, diff: string}> $changed
|
||||
* @param int $time duration in milliseconds
|
||||
* @param int $memory memory usage in bytes
|
||||
*/
|
||||
public function __construct(
|
||||
array $changed,
|
||||
int $filesCount,
|
||||
int $time,
|
||||
int $memory,
|
||||
bool $addAppliedFixers,
|
||||
bool $isDryRun,
|
||||
bool $isDecoratedOutput
|
||||
) {
|
||||
$this->changed = $changed;
|
||||
$this->filesCount = $filesCount;
|
||||
$this->time = $time;
|
||||
$this->memory = $memory;
|
||||
$this->addAppliedFixers = $addAppliedFixers;
|
||||
$this->isDryRun = $isDryRun;
|
||||
$this->isDecoratedOutput = $isDecoratedOutput;
|
||||
}
|
||||
|
||||
public function isDecoratedOutput(): bool
|
||||
{
|
||||
return $this->isDecoratedOutput;
|
||||
}
|
||||
|
||||
public function isDryRun(): bool
|
||||
{
|
||||
return $this->isDryRun;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{appliedFixers: list<string>, diff: string}>
|
||||
*/
|
||||
public function getChanged(): array
|
||||
{
|
||||
return $this->changed;
|
||||
}
|
||||
|
||||
public function getMemory(): int
|
||||
{
|
||||
return $this->memory;
|
||||
}
|
||||
|
||||
public function getTime(): int
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
public function getFilesCount(): int
|
||||
{
|
||||
return $this->filesCount;
|
||||
}
|
||||
|
||||
public function shouldAddAppliedFixers(): bool
|
||||
{
|
||||
return $this->addAppliedFixers;
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use Symfony\Component\Finder\Finder as SymfonyFinder;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ReporterFactory
|
||||
{
|
||||
/** @var array<string, ReporterInterface> */
|
||||
private array $reporters = [];
|
||||
|
||||
public function registerBuiltInReporters(): self
|
||||
{
|
||||
/** @var null|list<class-string<ReporterInterface>> $builtInReporters */
|
||||
static $builtInReporters;
|
||||
|
||||
if (null === $builtInReporters) {
|
||||
$builtInReporters = [];
|
||||
|
||||
foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
|
||||
$relativeNamespace = $file->getRelativePath();
|
||||
|
||||
/** @var class-string<ReporterInterface> $class */
|
||||
$class = \sprintf(
|
||||
'%s\%s%s',
|
||||
__NAMESPACE__,
|
||||
'' !== $relativeNamespace ? $relativeNamespace.'\\' : '',
|
||||
$file->getBasename('.php'),
|
||||
);
|
||||
$builtInReporters[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($builtInReporters as $reporterClass) {
|
||||
$this->registerReporter(new $reporterClass());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerReporter(ReporterInterface $reporter): self
|
||||
{
|
||||
$format = $reporter->getFormat();
|
||||
|
||||
if (isset($this->reporters[$format])) {
|
||||
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is already registered.', $format));
|
||||
}
|
||||
|
||||
$this->reporters[$format] = $reporter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFormats(): array
|
||||
{
|
||||
$formats = array_keys($this->reporters);
|
||||
sort($formats);
|
||||
|
||||
return $formats;
|
||||
}
|
||||
|
||||
public function getReporter(string $format): ReporterInterface
|
||||
{
|
||||
if (!isset($this->reporters[$format])) {
|
||||
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is not registered.', $format));
|
||||
}
|
||||
|
||||
return $this->reporters[$format];
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface ReporterInterface
|
||||
{
|
||||
public function getFormat(): string;
|
||||
|
||||
/**
|
||||
* Process changed files array. Returns generated report.
|
||||
*/
|
||||
public function generate(ReportSummary $reportSummary): string;
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use PhpCsFixer\Differ\DiffConsoleFormatter;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class TextReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'txt';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$output = '';
|
||||
|
||||
$identifiedFiles = 0;
|
||||
foreach ($reportSummary->getChanged() as $file => $fixResult) {
|
||||
++$identifiedFiles;
|
||||
$output .= \sprintf('%4d) %s', $identifiedFiles, $file);
|
||||
|
||||
if ($reportSummary->shouldAddAppliedFixers()) {
|
||||
$output .= $this->getAppliedFixers(
|
||||
$reportSummary->isDecoratedOutput(),
|
||||
$fixResult['appliedFixers'],
|
||||
);
|
||||
}
|
||||
|
||||
$output .= $this->getDiff($reportSummary->isDecoratedOutput(), $fixResult['diff']);
|
||||
$output .= \PHP_EOL;
|
||||
}
|
||||
|
||||
return $output.$this->getFooter(
|
||||
$reportSummary->getTime(),
|
||||
$identifiedFiles,
|
||||
$reportSummary->getFilesCount(),
|
||||
$reportSummary->getMemory(),
|
||||
$reportSummary->isDryRun(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $appliedFixers
|
||||
*/
|
||||
private function getAppliedFixers(bool $isDecoratedOutput, array $appliedFixers): string
|
||||
{
|
||||
return \sprintf(
|
||||
$isDecoratedOutput ? ' (<comment>%s</comment>)' : ' (%s)',
|
||||
implode(', ', $appliedFixers),
|
||||
);
|
||||
}
|
||||
|
||||
private function getDiff(bool $isDecoratedOutput, string $diff): string
|
||||
{
|
||||
if ('' === $diff) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$diffFormatter = new DiffConsoleFormatter($isDecoratedOutput, \sprintf(
|
||||
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
|
||||
\PHP_EOL,
|
||||
\PHP_EOL,
|
||||
));
|
||||
|
||||
return \PHP_EOL.$diffFormatter->format($diff).\PHP_EOL;
|
||||
}
|
||||
|
||||
private function getFooter(int $time, int $identifiedFiles, int $files, int $memory, bool $isDryRun): string
|
||||
{
|
||||
return \PHP_EOL.\sprintf(
|
||||
'%s %d of %d %s in %.3f seconds, %.2f MB memory used'.\PHP_EOL,
|
||||
$isDryRun ? 'Found' : 'Fixed',
|
||||
$identifiedFiles,
|
||||
$files,
|
||||
$isDryRun ? 'files that can be fixed' : 'files',
|
||||
$time / 1_000,
|
||||
$memory / 1_024 / 1_024,
|
||||
);
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\FixReport;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class XmlReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'xml';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
if (!\extension_loaded('dom')) {
|
||||
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
|
||||
}
|
||||
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
// new nodes should be added to this or existing children
|
||||
$root = $dom->createElement('report');
|
||||
$dom->appendChild($root);
|
||||
|
||||
$root->appendChild($this->createAboutElement($dom, Application::getAbout()));
|
||||
|
||||
$filesXML = $dom->createElement('files');
|
||||
$root->appendChild($filesXML);
|
||||
|
||||
$i = 1;
|
||||
foreach ($reportSummary->getChanged() as $file => $fixResult) {
|
||||
$fileXML = $dom->createElement('file');
|
||||
$fileXML->setAttribute('id', (string) $i++);
|
||||
$fileXML->setAttribute('name', $file);
|
||||
$filesXML->appendChild($fileXML);
|
||||
|
||||
if ($reportSummary->shouldAddAppliedFixers()) {
|
||||
$fileXML->appendChild(
|
||||
$this->createAppliedFixersElement($dom, $fixResult['appliedFixers']),
|
||||
);
|
||||
}
|
||||
|
||||
if ('' !== $fixResult['diff']) {
|
||||
$fileXML->appendChild($this->createDiffElement($dom, $fixResult['diff']));
|
||||
}
|
||||
}
|
||||
|
||||
if (0 !== $reportSummary->getTime()) {
|
||||
$root->appendChild($this->createTimeElement($reportSummary->getTime(), $dom));
|
||||
}
|
||||
|
||||
if (0 !== $reportSummary->getMemory()) {
|
||||
$root->appendChild($this->createMemoryElement($reportSummary->getMemory(), $dom));
|
||||
}
|
||||
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$result = $dom->saveXML();
|
||||
if (false === $result) {
|
||||
throw new \RuntimeException('Failed to generate XML output');
|
||||
}
|
||||
|
||||
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($result) : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $appliedFixers
|
||||
*/
|
||||
private function createAppliedFixersElement(\DOMDocument $dom, array $appliedFixers): \DOMElement
|
||||
{
|
||||
$appliedFixersXML = $dom->createElement('applied_fixers');
|
||||
|
||||
foreach ($appliedFixers as $appliedFixer) {
|
||||
$appliedFixerXML = $dom->createElement('applied_fixer');
|
||||
$appliedFixerXML->setAttribute('name', $appliedFixer);
|
||||
$appliedFixersXML->appendChild($appliedFixerXML);
|
||||
}
|
||||
|
||||
return $appliedFixersXML;
|
||||
}
|
||||
|
||||
private function createDiffElement(\DOMDocument $dom, string $diff): \DOMElement
|
||||
{
|
||||
$diffXML = $dom->createElement('diff');
|
||||
$diffXML->appendChild($dom->createCDATASection($diff));
|
||||
|
||||
return $diffXML;
|
||||
}
|
||||
|
||||
private function createTimeElement(float $time, \DOMDocument $dom): \DOMElement
|
||||
{
|
||||
$time = round($time / 1_000, 3);
|
||||
|
||||
$timeXML = $dom->createElement('time');
|
||||
$timeXML->setAttribute('unit', 's');
|
||||
$timeTotalXML = $dom->createElement('total');
|
||||
$timeTotalXML->setAttribute('value', (string) $time);
|
||||
$timeXML->appendChild($timeTotalXML);
|
||||
|
||||
return $timeXML;
|
||||
}
|
||||
|
||||
private function createMemoryElement(float $memory, \DOMDocument $dom): \DOMElement
|
||||
{
|
||||
$memory = round($memory / 1_024 / 1_024, 3);
|
||||
|
||||
$memoryXML = $dom->createElement('memory');
|
||||
$memoryXML->setAttribute('value', (string) $memory);
|
||||
$memoryXML->setAttribute('unit', 'MB');
|
||||
|
||||
return $memoryXML;
|
||||
}
|
||||
|
||||
private function createAboutElement(\DOMDocument $dom, string $about): \DOMElement
|
||||
{
|
||||
$xml = $dom->createElement('about');
|
||||
$xml->setAttribute('value', $about);
|
||||
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListRulesReport;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class JsonReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'json';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$fixers = $reportSummary->getFixers();
|
||||
|
||||
usort($fixers, static fn (FixerInterface $a, FixerInterface $b): int => $a->getName() <=> $b->getName());
|
||||
|
||||
$json = ['rules' => []];
|
||||
|
||||
foreach ($fixers as $fixer) {
|
||||
$name = $fixer->getName();
|
||||
$json['rules'][$name] = [
|
||||
'isRisky' => $fixer->isRisky(),
|
||||
'name' => $name,
|
||||
'summary' => $fixer->getDefinition()->getSummary(),
|
||||
];
|
||||
}
|
||||
|
||||
return json_encode($json, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListRulesReport;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ReportSummary
|
||||
{
|
||||
/**
|
||||
* @var list<FixerInterface>
|
||||
*/
|
||||
private array $fixers;
|
||||
|
||||
/**
|
||||
* @param list<FixerInterface> $fixers
|
||||
*/
|
||||
public function __construct(array $fixers)
|
||||
{
|
||||
$this->fixers = $fixers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<FixerInterface>
|
||||
*/
|
||||
public function getFixers(): array
|
||||
{
|
||||
return $this->fixers;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListRulesReport;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ReporterFactory
|
||||
{
|
||||
/**
|
||||
* @var array<string, ReporterInterface>
|
||||
*/
|
||||
private array $reporters = [];
|
||||
|
||||
public function getReporter(string $format): ReporterInterface
|
||||
{
|
||||
if (!isset($this->reporters[$format])) {
|
||||
throw new \UnexpectedValueException(\sprintf('The format "%s" is not defined.', $format));
|
||||
}
|
||||
|
||||
return $this->reporters[$format];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFormats(): array
|
||||
{
|
||||
$formats = array_keys($this->reporters);
|
||||
sort($formats);
|
||||
|
||||
return $formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerBuiltInReporters(): self
|
||||
{
|
||||
$this
|
||||
->registerReporter(new JsonReporter())
|
||||
->registerReporter(new TextReporter())
|
||||
;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerReporter(ReporterInterface $reporter): self
|
||||
{
|
||||
$format = $reporter->getFormat();
|
||||
|
||||
if (isset($this->reporters[$format])) {
|
||||
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is already registered.', $format));
|
||||
}
|
||||
|
||||
$this->reporters[$format] = $reporter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListRulesReport;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface ReporterInterface
|
||||
{
|
||||
public function getFormat(): string;
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListRulesReport;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class TextReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'txt';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$fixers = $reportSummary->getFixers();
|
||||
|
||||
usort($fixers, static fn (FixerInterface $a, FixerInterface $b): int => $a->getName() <=> $b->getName());
|
||||
|
||||
$output = '';
|
||||
|
||||
foreach ($fixers as $i => $fixer) {
|
||||
$output .= \sprintf('%3d) %s', $i + 1, $fixer->getName()).\PHP_EOL.' '.$fixer->getDefinition()->getSummary().\PHP_EOL;
|
||||
|
||||
if ($fixer->isRisky()) {
|
||||
$output .= ' Rule is risky.'.\PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListSetsReport;
|
||||
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class JsonReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'json';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$sets = $reportSummary->getSets();
|
||||
|
||||
usort($sets, static fn (RuleSetDefinitionInterface $a, RuleSetDefinitionInterface $b): int => $a->getName() <=> $b->getName());
|
||||
|
||||
$json = ['sets' => []];
|
||||
|
||||
foreach ($sets as $set) {
|
||||
$setName = $set->getName();
|
||||
$json['sets'][$setName] = [
|
||||
'description' => $set->getDescription(),
|
||||
'isRisky' => $set->isRisky(),
|
||||
'name' => $setName,
|
||||
];
|
||||
}
|
||||
|
||||
return json_encode($json, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListSetsReport;
|
||||
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ReportSummary
|
||||
{
|
||||
/**
|
||||
* @var list<RuleSetDefinitionInterface>
|
||||
*/
|
||||
private array $sets;
|
||||
|
||||
/**
|
||||
* @param list<RuleSetDefinitionInterface> $sets
|
||||
*/
|
||||
public function __construct(array $sets)
|
||||
{
|
||||
$this->sets = $sets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<RuleSetDefinitionInterface>
|
||||
*/
|
||||
public function getSets(): array
|
||||
{
|
||||
return $this->sets;
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListSetsReport;
|
||||
|
||||
use Symfony\Component\Finder\Finder as SymfonyFinder;
|
||||
|
||||
/**
|
||||
* @author Boris Gorbylev <ekho@ekho.name>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ReporterFactory
|
||||
{
|
||||
/**
|
||||
* @var array<string, ReporterInterface>
|
||||
*/
|
||||
private array $reporters = [];
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerBuiltInReporters(): self
|
||||
{
|
||||
/** @var null|list<class-string<ReporterInterface>> $builtInReporters */
|
||||
static $builtInReporters;
|
||||
|
||||
if (null === $builtInReporters) {
|
||||
$builtInReporters = [];
|
||||
|
||||
foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
|
||||
$relativeNamespace = $file->getRelativePath();
|
||||
|
||||
/** @var class-string<ReporterInterface> $class */
|
||||
$class = \sprintf(
|
||||
'%s\%s%s',
|
||||
__NAMESPACE__,
|
||||
'' !== $relativeNamespace ? $relativeNamespace.'\\' : '',
|
||||
$file->getBasename('.php'),
|
||||
);
|
||||
$builtInReporters[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($builtInReporters as $reporterClass) {
|
||||
$this->registerReporter(new $reporterClass());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function registerReporter(ReporterInterface $reporter): self
|
||||
{
|
||||
$format = $reporter->getFormat();
|
||||
|
||||
if (isset($this->reporters[$format])) {
|
||||
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is already registered.', $format));
|
||||
}
|
||||
|
||||
$this->reporters[$format] = $reporter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFormats(): array
|
||||
{
|
||||
$formats = array_keys($this->reporters);
|
||||
sort($formats);
|
||||
|
||||
return $formats;
|
||||
}
|
||||
|
||||
public function getReporter(string $format): ReporterInterface
|
||||
{
|
||||
if (!isset($this->reporters[$format])) {
|
||||
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is not registered.', $format));
|
||||
}
|
||||
|
||||
return $this->reporters[$format];
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListSetsReport;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface ReporterInterface
|
||||
{
|
||||
public function getFormat(): string;
|
||||
|
||||
/**
|
||||
* Process changed files array. Returns generated report.
|
||||
*/
|
||||
public function generate(ReportSummary $reportSummary): string;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\Report\ListSetsReport;
|
||||
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class TextReporter implements ReporterInterface
|
||||
{
|
||||
public function getFormat(): string
|
||||
{
|
||||
return 'txt';
|
||||
}
|
||||
|
||||
public function generate(ReportSummary $reportSummary): string
|
||||
{
|
||||
$sets = $reportSummary->getSets();
|
||||
|
||||
usort($sets, static fn (RuleSetDefinitionInterface $a, RuleSetDefinitionInterface $b): int => $a->getName() <=> $b->getName());
|
||||
|
||||
$output = '';
|
||||
|
||||
foreach ($sets as $i => $set) {
|
||||
$output .= \sprintf('%2d) %s', $i + 1, $set->getName()).\PHP_EOL.' '.$set->getDescription().\PHP_EOL;
|
||||
|
||||
if ($set->isRisky()) {
|
||||
$output .= ' Set contains risky rules.'.\PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\SelfUpdate;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class GithubClient implements GithubClientInterface
|
||||
{
|
||||
private string $url;
|
||||
|
||||
public function __construct(string $url = 'https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/tags')
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function getTags(): array
|
||||
{
|
||||
$result = @file_get_contents(
|
||||
$this->url,
|
||||
false,
|
||||
stream_context_create([
|
||||
'http' => [
|
||||
'header' => 'User-Agent: PHP-CS-Fixer/PHP-CS-Fixer',
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
if (false === $result) {
|
||||
throw new \RuntimeException(\sprintf('Failed to load tags at "%s".', $this->url));
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* @var list<array{
|
||||
* name: string,
|
||||
* zipball_url: string,
|
||||
* tarball_url: string,
|
||||
* commit: array{sha: string, url: string},
|
||||
* }>
|
||||
*/
|
||||
$result = json_decode($result, true, 512, \JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \RuntimeException(\sprintf(
|
||||
'Failed to read response from "%s" as JSON: %s.',
|
||||
$this->url,
|
||||
$e->getMessage(),
|
||||
));
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn (array $tagData): string => $tagData['name'],
|
||||
$result,
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\SelfUpdate;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface GithubClientInterface
|
||||
{
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getTags(): array;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\SelfUpdate;
|
||||
|
||||
use Composer\Semver\Comparator;
|
||||
use Composer\Semver\Semver;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class NewVersionChecker implements NewVersionCheckerInterface
|
||||
{
|
||||
private GithubClientInterface $githubClient;
|
||||
|
||||
private VersionParser $versionParser;
|
||||
|
||||
/**
|
||||
* @var null|list<string>
|
||||
*/
|
||||
private ?array $availableVersions = null;
|
||||
|
||||
public function __construct(GithubClientInterface $githubClient)
|
||||
{
|
||||
$this->githubClient = $githubClient;
|
||||
$this->versionParser = new VersionParser();
|
||||
}
|
||||
|
||||
public function getLatestVersion(): string
|
||||
{
|
||||
$this->retrieveAvailableVersions();
|
||||
|
||||
\assert(isset($this->availableVersions[0]));
|
||||
|
||||
return $this->availableVersions[0];
|
||||
}
|
||||
|
||||
public function getLatestVersionOfMajor(int $majorVersion): ?string
|
||||
{
|
||||
$this->retrieveAvailableVersions();
|
||||
|
||||
$semverConstraint = '^'.$majorVersion;
|
||||
|
||||
foreach ($this->availableVersions as $availableVersion) {
|
||||
if (Semver::satisfies($availableVersion, $semverConstraint)) {
|
||||
return $availableVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function compareVersions(string $versionA, string $versionB): int
|
||||
{
|
||||
$versionA = $this->versionParser->normalize($versionA);
|
||||
$versionB = $this->versionParser->normalize($versionB);
|
||||
|
||||
if (Comparator::lessThan($versionA, $versionB)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Comparator::greaterThan($versionA, $versionB)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function retrieveAvailableVersions(): void
|
||||
{
|
||||
if (null !== $this->availableVersions) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->githubClient->getTags() as $version) {
|
||||
try {
|
||||
$this->versionParser->normalize($version);
|
||||
|
||||
if ('stable' === VersionParser::parseStability($version)) {
|
||||
$this->availableVersions[] = $version;
|
||||
}
|
||||
} catch (\UnexpectedValueException $exception) {
|
||||
// not a valid version tag
|
||||
}
|
||||
}
|
||||
|
||||
$versions = Semver::rsort($this->availableVersions);
|
||||
\assert(array_is_list($versions)); // Semver::rsort provides soft `array` type, let's validate and ensure proper type for SCA
|
||||
|
||||
$this->availableVersions = $versions;
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console\SelfUpdate;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface NewVersionCheckerInterface
|
||||
{
|
||||
/**
|
||||
* Returns the tag of the latest version.
|
||||
*/
|
||||
public function getLatestVersion(): string;
|
||||
|
||||
/**
|
||||
* Returns the tag of the latest minor/patch version of the given major version.
|
||||
*/
|
||||
public function getLatestVersionOfMajor(int $majorVersion): ?string;
|
||||
|
||||
/**
|
||||
* Returns -1, 0, or 1 if the first version is respectively less than,
|
||||
* equal to, or greater than the second.
|
||||
*/
|
||||
public function compareVersions(string $versionA, string $versionB): int;
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Console;
|
||||
|
||||
use PhpCsFixer\ComposerJsonReader;
|
||||
use PhpCsFixer\ToolInfo;
|
||||
use PhpCsFixer\ToolInfoInterface;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class WarningsDetector
|
||||
{
|
||||
private ToolInfoInterface $toolInfo;
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $warnings = [];
|
||||
|
||||
public function __construct(ToolInfoInterface $toolInfo)
|
||||
{
|
||||
$this->toolInfo = $toolInfo;
|
||||
}
|
||||
|
||||
public function detectOldMajor(): void
|
||||
{
|
||||
// @TODO 3.99 to be activated with new MAJOR release 4.0
|
||||
// $currentMajorVersion = \intval(explode('.', Application::VERSION)[0], 10);
|
||||
// $nextMajorVersion = $currentMajorVersion + 1;
|
||||
// $this->warnings[] = "You are running PHP CS Fixer v{$currentMajorVersion}, which is not maintained anymore. Please update to v{$nextMajorVersion}.";
|
||||
// $this->warnings[] = "You may find an UPGRADE guide at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v{$nextMajorVersion}.0.0/UPGRADE-v{$nextMajorVersion}.md .";
|
||||
}
|
||||
|
||||
public function detectOldVendor(): void
|
||||
{
|
||||
if ($this->toolInfo->isInstalledByComposer()) {
|
||||
$details = $this->toolInfo->getComposerInstallationDetails();
|
||||
if (ToolInfo::COMPOSER_LEGACY_PACKAGE_NAME === $details['name']) {
|
||||
$this->warnings[] = \sprintf(
|
||||
'You are running PHP CS Fixer installed with old vendor `%s`. Please update to `%s`.',
|
||||
ToolInfo::COMPOSER_LEGACY_PACKAGE_NAME,
|
||||
ToolInfo::COMPOSER_PACKAGE_NAME,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function detectNonMonolithic(): void
|
||||
{
|
||||
if (filter_var(getenv('PHP_CS_FIXER_NON_MONOLITHIC'), \FILTER_VALIDATE_BOOL)) {
|
||||
$this->warnings[] = 'Processing non-monolithic files enabled, because `PHP_CS_FIXER_NON_MONOLITHIC` is set. Execution result may be unpredictable - non-monolithic files are not officially supported.';
|
||||
}
|
||||
}
|
||||
|
||||
public function detectHigherPhpVersion(): void
|
||||
{
|
||||
try {
|
||||
$composerJsonReader = ComposerJsonReader::createSingleton();
|
||||
$minPhpVersion = $composerJsonReader->getPhp();
|
||||
|
||||
if (null === $minPhpVersion) {
|
||||
$this->warnings[] = 'No PHP version requirement found in composer.json. It is recommended to specify a minimum PHP version supported by your project.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentPhpVersion = \PHP_VERSION;
|
||||
$currentPhpMajorMinor = \sprintf('%d.%d', \PHP_MAJOR_VERSION, \PHP_MINOR_VERSION);
|
||||
|
||||
// Compare major.minor versions
|
||||
if (version_compare($currentPhpMajorMinor, $minPhpVersion, '>')) {
|
||||
$this->warnings[] = \sprintf(
|
||||
'You are running PHP CS Fixer on PHP %1$s, but the minimum PHP version supported by your project in composer.json is PHP %2$s. Executing PHP CS Fixer on newer PHP versions may introduce syntax or features not yet available in PHP %2$s, which could cause issues under that version. It is recommended to run PHP CS Fixer on PHP %2$s, to fit your project specifics.',
|
||||
$currentPhpVersion,
|
||||
$minPhpVersion,
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->warnings[] = \sprintf(
|
||||
'Unable to determine minimum PHP version supported by your project from composer.json: %s',
|
||||
$e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getWarnings(): array
|
||||
{
|
||||
if (0 === \count($this->warnings)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_merge(
|
||||
$this->warnings,
|
||||
['If you need help while solving warnings, ask at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/discussions/, we will help you!'],
|
||||
)));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer;
|
||||
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
|
||||
/**
|
||||
* @author Greg Korba <greg@codito.dev>
|
||||
*
|
||||
* @TODO 4.0 Include support for custom rulesets in main ConfigInterface
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface CustomRulesetsAwareConfigInterface extends ConfigInterface
|
||||
{
|
||||
/**
|
||||
* Registers custom rule sets to be used the same way as built-in rule sets.
|
||||
*
|
||||
* @param list<RuleSetDefinitionInterface> $ruleSets
|
||||
*
|
||||
* @todo v4 Introduce it in main ConfigInterface
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function registerCustomRuleSets(array $ruleSets): ConfigInterface;
|
||||
|
||||
/**
|
||||
* @return list<RuleSetDefinitionInterface>
|
||||
*/
|
||||
public function getCustomRuleSets(): array;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Differ;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DiffConsoleFormatter
|
||||
{
|
||||
private bool $isDecoratedOutput;
|
||||
|
||||
private string $template;
|
||||
|
||||
public function __construct(bool $isDecoratedOutput, string $template = '%s')
|
||||
{
|
||||
$this->isDecoratedOutput = $isDecoratedOutput;
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
public function format(string $diff, string $lineTemplate = '%s'): string
|
||||
{
|
||||
$isDecorated = $this->isDecoratedOutput;
|
||||
|
||||
$template = $isDecorated
|
||||
? $this->template
|
||||
: Preg::replace('/<[^<>]+>/', '', $this->template);
|
||||
|
||||
return \sprintf(
|
||||
$template,
|
||||
implode(
|
||||
\PHP_EOL,
|
||||
array_map(
|
||||
static function (string $line) use ($isDecorated, $lineTemplate): string {
|
||||
if ($isDecorated) {
|
||||
$count = 0;
|
||||
$line = Preg::replaceCallback(
|
||||
'/^([+\-@].*)/',
|
||||
static function (array $matches): string {
|
||||
\assert(isset($matches[0]));
|
||||
if ('+' === $matches[0][0]) {
|
||||
$colour = 'green';
|
||||
} elseif ('-' === $matches[0][0]) {
|
||||
$colour = 'red';
|
||||
} else {
|
||||
$colour = 'cyan';
|
||||
}
|
||||
|
||||
return \sprintf('<fg=%s>%s</fg=%s>', $colour, OutputFormatter::escape($matches[0]), $colour);
|
||||
},
|
||||
$line,
|
||||
1,
|
||||
$count,
|
||||
);
|
||||
|
||||
if (0 === $count) {
|
||||
$line = OutputFormatter::escape($line);
|
||||
}
|
||||
}
|
||||
|
||||
return \sprintf($lineTemplate, $line);
|
||||
},
|
||||
Preg::split('#\R#u', $diff),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Differ;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
interface DifferInterface
|
||||
{
|
||||
/**
|
||||
* Create diff.
|
||||
*/
|
||||
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string;
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Differ;
|
||||
|
||||
use SebastianBergmann\Diff\Differ;
|
||||
use SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class FullDiffer implements DifferInterface
|
||||
{
|
||||
private Differ $differ;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->differ = new Differ(new StrictUnifiedDiffOutputBuilder([
|
||||
'collapseRanges' => false,
|
||||
'commonLineThreshold' => 100,
|
||||
'contextLines' => 100,
|
||||
'fromFile' => 'Original',
|
||||
'toFile' => 'New',
|
||||
]));
|
||||
}
|
||||
|
||||
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string
|
||||
{
|
||||
return $this->differ->diff($old, $new);
|
||||
}
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Differ;
|
||||
|
||||
/**
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class NullDiffer implements DifferInterface
|
||||
{
|
||||
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Differ;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use SebastianBergmann\Diff\Differ;
|
||||
use SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class UnifiedDiffer implements DifferInterface
|
||||
{
|
||||
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string
|
||||
{
|
||||
if (null === $file) {
|
||||
$options = [
|
||||
'fromFile' => 'Original',
|
||||
'toFile' => 'New',
|
||||
];
|
||||
} else {
|
||||
$filePath = $file->getRealPath();
|
||||
|
||||
if (Preg::match('/\s/', $filePath)) {
|
||||
$filePath = '"'.$filePath.'"';
|
||||
}
|
||||
|
||||
$options = [
|
||||
'fromFile' => $filePath,
|
||||
'toFile' => $filePath,
|
||||
];
|
||||
}
|
||||
|
||||
$differ = new Differ(new StrictUnifiedDiffOutputBuilder($options));
|
||||
|
||||
return $differ->diff($old, $new);
|
||||
}
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
|
||||
|
||||
/**
|
||||
* This represents an entire annotation from a docblock.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Annotation implements \Stringable
|
||||
{
|
||||
/**
|
||||
* All the annotation tag names with types.
|
||||
*
|
||||
* @var non-empty-list<string>
|
||||
*/
|
||||
public const TAGS_WITH_TYPES = [
|
||||
'extends',
|
||||
'implements',
|
||||
'method',
|
||||
'param',
|
||||
'param-out',
|
||||
'phpstan-import-type',
|
||||
'phpstan-type',
|
||||
'phpstan-var',
|
||||
'property',
|
||||
'property-read',
|
||||
'property-write',
|
||||
'psalm-import-type',
|
||||
'psalm-type',
|
||||
'psalm-var',
|
||||
'return',
|
||||
'throws',
|
||||
'type',
|
||||
'var',
|
||||
];
|
||||
|
||||
/**
|
||||
* The lines that make up the annotation.
|
||||
*
|
||||
* @var non-empty-list<Line>
|
||||
*/
|
||||
private array $lines;
|
||||
|
||||
/**
|
||||
* The position of the first line of the annotation in the docblock.
|
||||
*/
|
||||
private int $start;
|
||||
|
||||
/**
|
||||
* The position of the last line of the annotation in the docblock.
|
||||
*/
|
||||
private int $end;
|
||||
|
||||
/**
|
||||
* The associated tag.
|
||||
*/
|
||||
private ?Tag $tag = null;
|
||||
|
||||
/**
|
||||
* Lazy loaded, cached types content.
|
||||
*/
|
||||
private ?string $typesContent = null;
|
||||
|
||||
/**
|
||||
* The cached types.
|
||||
*
|
||||
* @var null|list<string>
|
||||
*/
|
||||
private ?array $types = null;
|
||||
|
||||
private ?NamespaceAnalysis $namespace = null;
|
||||
|
||||
/**
|
||||
* @var list<NamespaceUseAnalysis>
|
||||
*/
|
||||
private array $namespaceUses;
|
||||
|
||||
/**
|
||||
* Create a new line instance.
|
||||
*
|
||||
* @param non-empty-array<int, Line> $lines
|
||||
* @param null|NamespaceAnalysis $namespace
|
||||
* @param list<NamespaceUseAnalysis> $namespaceUses
|
||||
*/
|
||||
public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
|
||||
{
|
||||
$this->lines = array_values($lines);
|
||||
$this->namespace = $namespace;
|
||||
$this->namespaceUses = $namespaceUses;
|
||||
|
||||
$this->start = array_key_first($lines);
|
||||
$this->end = array_key_last($lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of object.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the annotation tag names with types.
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*
|
||||
* @deprecated Use `Annotation::TAGS_WITH_TYPES` constant instead
|
||||
*
|
||||
* @TODO 4.0 remove me
|
||||
*/
|
||||
public static function getTagsWithTypes(): array
|
||||
{
|
||||
return self::TAGS_WITH_TYPES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start position of this annotation.
|
||||
*/
|
||||
public function getStart(): int
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end position of this annotation.
|
||||
*/
|
||||
public function getEnd(): int
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated tag.
|
||||
*/
|
||||
public function getTag(): Tag
|
||||
{
|
||||
if (null === $this->tag) {
|
||||
$this->tag = new Tag($this->lines[0]);
|
||||
}
|
||||
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getTypeExpression(): ?TypeExpression
|
||||
{
|
||||
$typesContent = $this->getTypesContent();
|
||||
|
||||
return null === $typesContent
|
||||
? null
|
||||
: new TypeExpression($typesContent, $this->namespace, $this->namespaceUses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getVariableName(): ?string
|
||||
{
|
||||
$type = preg_quote($this->getTypesContent() ?? '', '/');
|
||||
$regex = \sprintf(
|
||||
'/@%s\s+(%s\s*)?(&\s*)?(\.{3}\s*)?(?<variable>\$%s)(?:.*|$)/',
|
||||
$this->tag->getName(),
|
||||
$type,
|
||||
TypeExpression::REGEX_IDENTIFIER,
|
||||
);
|
||||
|
||||
if (Preg::match($regex, $this->getContent(), $matches)) {
|
||||
\assert(isset($matches['variable']));
|
||||
|
||||
return $matches['variable'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the types associated with this annotation.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
if (null === $this->types) {
|
||||
$typeExpression = $this->getTypeExpression();
|
||||
$this->types = null === $typeExpression
|
||||
? []
|
||||
: $typeExpression->getTypes();
|
||||
}
|
||||
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the types associated with this annotation.
|
||||
*
|
||||
* @param list<string> $types
|
||||
*/
|
||||
public function setTypes(array $types): void
|
||||
{
|
||||
$origTypesContent = $this->getTypesContent();
|
||||
$newTypesContent = implode(
|
||||
// Fallback to union type is provided for backward compatibility (previously glue was set to `|` by default even when type was not composite)
|
||||
// @TODO Better handling for cases where type is fixed (original type is not composite, but was made composite during fix)
|
||||
$this->getTypeExpression()->getTypesGlue() ?? '|',
|
||||
$types,
|
||||
);
|
||||
|
||||
if ($origTypesContent === $newTypesContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$originalTypesLines = Preg::split('/([^\n\r]+\R*)/', $origTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
|
||||
$newTypesLines = Preg::split('/([^\n\r]+\R*)/', $newTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
\assert(\count($originalTypesLines) === \count($newTypesLines));
|
||||
|
||||
foreach ($newTypesLines as $index => $line) {
|
||||
\assert(isset($originalTypesLines[$index]));
|
||||
$pattern = '/'.preg_quote($originalTypesLines[$index], '/').'/';
|
||||
|
||||
\assert(isset($this->lines[$index]));
|
||||
$this->lines[$index]->setContent(Preg::replace($pattern, $line, $this->lines[$index]->getContent(), 1));
|
||||
}
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the normalized types associated with this annotation, so they can easily be compared.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getNormalizedTypes(): array
|
||||
{
|
||||
$typeExpression = $this->getTypeExpression();
|
||||
if (null === $typeExpression) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalizedTypeExpression = $typeExpression
|
||||
->mapTypes(static fn (TypeExpression $v) => new TypeExpression(strtolower($v->toString()), null, []))
|
||||
->sortTypes(static fn (TypeExpression $a, TypeExpression $b) => $a->toString() <=> $b->toString())
|
||||
;
|
||||
|
||||
return $normalizedTypeExpression->getTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this annotation by removing all its lines.
|
||||
*/
|
||||
public function remove(): void
|
||||
{
|
||||
foreach ($this->lines as $line) {
|
||||
if ($line->isTheStart() && $line->isTheEnd()) {
|
||||
// Single line doc block, remove entirely
|
||||
$line->remove();
|
||||
} elseif ($line->isTheStart()) {
|
||||
// Multi line doc block, but start is on the same line as the first annotation, keep only the start
|
||||
$content = Preg::replace('#(\s*/\*\*).*#', '$1', $line->getContent());
|
||||
|
||||
$line->setContent($content);
|
||||
} elseif ($line->isTheEnd()) {
|
||||
// Multi line doc block, but end is on the same line as the last annotation, keep only the end
|
||||
$content = Preg::replace('#(\s*)\S.*(\*/.*)#', '$1$2', $line->getContent());
|
||||
|
||||
$line->setContent($content);
|
||||
} else {
|
||||
// Multi line doc block, neither start nor end on this line, can be removed safely
|
||||
$line->remove();
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the annotation content.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return implode('', $this->lines);
|
||||
}
|
||||
|
||||
public function supportTypes(): bool
|
||||
{
|
||||
return \in_array($this->getTag()->getName(), self::TAGS_WITH_TYPES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current types content.
|
||||
*
|
||||
* Be careful modifying the underlying line as that won't flush the cache.
|
||||
*/
|
||||
private function getTypesContent(): ?string
|
||||
{
|
||||
if (null === $this->typesContent) {
|
||||
$name = $this->getTag()->getName();
|
||||
|
||||
if (!$this->supportTypes()) {
|
||||
throw new \RuntimeException('This tag does not support types.');
|
||||
}
|
||||
|
||||
if (Preg::match(
|
||||
'{^(?:\h*\*|/\*\*)[\h*]*@'.$name.'\h+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&?[\.\$\s]).*)?\r?$}is',
|
||||
$this->getContent(),
|
||||
$matches,
|
||||
)) {
|
||||
\assert(isset($matches['types']));
|
||||
$this->typesContent = $matches['types'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->typesContent;
|
||||
}
|
||||
|
||||
private function clearCache(): void
|
||||
{
|
||||
$this->types = null;
|
||||
$this->typesContent = null;
|
||||
}
|
||||
}
|
||||
Vendored
+255
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
|
||||
|
||||
/**
|
||||
* This class represents a docblock.
|
||||
*
|
||||
* It internally splits it up into "lines" that we can manipulate.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DocBlock implements \Stringable
|
||||
{
|
||||
/**
|
||||
* @var list<Line>
|
||||
*/
|
||||
private array $lines = [];
|
||||
|
||||
/**
|
||||
* @var null|list<Annotation>
|
||||
*/
|
||||
private ?array $annotations = null;
|
||||
|
||||
private ?NamespaceAnalysis $namespace;
|
||||
|
||||
/**
|
||||
* @var list<NamespaceUseAnalysis>
|
||||
*/
|
||||
private array $namespaceUses;
|
||||
|
||||
/**
|
||||
* @param list<NamespaceUseAnalysis> $namespaceUses
|
||||
*/
|
||||
public function __construct(string $content, ?NamespaceAnalysis $namespace = null, array $namespaceUses = [])
|
||||
{
|
||||
foreach (Preg::split('/([^\n\r]+\R*)/', $content, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE) as $line) {
|
||||
$this->lines[] = new Line($line);
|
||||
}
|
||||
|
||||
$this->namespace = $namespace;
|
||||
$this->namespaceUses = $namespaceUses;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this docblock's lines.
|
||||
*
|
||||
* @return list<Line>
|
||||
*/
|
||||
public function getLines(): array
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single line.
|
||||
*/
|
||||
public function getLine(int $pos): ?Line
|
||||
{
|
||||
return $this->lines[$pos] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this docblock's annotations.
|
||||
*
|
||||
* @return list<Annotation>
|
||||
*/
|
||||
public function getAnnotations(): array
|
||||
{
|
||||
if (null !== $this->annotations) {
|
||||
return $this->annotations;
|
||||
}
|
||||
|
||||
$this->annotations = [];
|
||||
$total = \count($this->lines);
|
||||
|
||||
for ($index = 0; $index < $total; ++$index) {
|
||||
\assert(isset($this->lines[$index]));
|
||||
if ($this->lines[$index]->containsATag()) {
|
||||
// get all the lines that make up the annotation
|
||||
$lines = \array_slice($this->lines, $index, $this->findAnnotationLength($index), true);
|
||||
\assert([] !== $lines);
|
||||
$annotation = new Annotation($lines, $this->namespace, $this->namespaceUses);
|
||||
// move the index to the end of the annotation to avoid
|
||||
// checking it again because we know the lines inside the
|
||||
// current annotation cannot be part of another annotation
|
||||
$index = $annotation->getEnd();
|
||||
// add the current annotation to the list of annotations
|
||||
$this->annotations[] = $annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->annotations;
|
||||
}
|
||||
|
||||
public function isMultiLine(): bool
|
||||
{
|
||||
return 1 !== \count($this->lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a one line doc block, and turn it into a multi line doc block.
|
||||
*/
|
||||
public function makeMultiLine(string $indent, string $lineEnd): void
|
||||
{
|
||||
if ($this->isMultiLine()) {
|
||||
return;
|
||||
}
|
||||
|
||||
\assert(isset($this->lines[0]));
|
||||
$lineContent = $this->getSingleLineDocBlockEntry($this->lines[0]);
|
||||
|
||||
if ('' === $lineContent) {
|
||||
$this->lines = [
|
||||
new Line('/**'.$lineEnd),
|
||||
new Line($indent.' *'.$lineEnd),
|
||||
new Line($indent.' */'),
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lines = [
|
||||
new Line('/**'.$lineEnd),
|
||||
new Line($indent.' * '.$lineContent.$lineEnd),
|
||||
new Line($indent.' */'),
|
||||
];
|
||||
}
|
||||
|
||||
public function makeSingleLine(): void
|
||||
{
|
||||
if (!$this->isMultiLine()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$usefulLines = array_filter(
|
||||
$this->lines,
|
||||
static fn (Line $line): bool => $line->containsUsefulContent(),
|
||||
);
|
||||
|
||||
if (1 < \count($usefulLines)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lineContent = '';
|
||||
if (\count($usefulLines) > 0) {
|
||||
$lineContent = $this->getSingleLineDocBlockEntry(array_shift($usefulLines));
|
||||
}
|
||||
|
||||
$this->lines = [new Line('/** '.$lineContent.' */')];
|
||||
}
|
||||
|
||||
public function getAnnotation(int $pos): ?Annotation
|
||||
{
|
||||
$annotations = $this->getAnnotations();
|
||||
|
||||
return $annotations[$pos] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific types of annotations only.
|
||||
*
|
||||
* @param list<string>|string $types
|
||||
*
|
||||
* @return list<Annotation>
|
||||
*/
|
||||
public function getAnnotationsOfType($types): array
|
||||
{
|
||||
$typesToSearchFor = (array) $types;
|
||||
|
||||
$annotations = [];
|
||||
|
||||
foreach ($this->getAnnotations() as $annotation) {
|
||||
$tagName = $annotation->getTag()->getName();
|
||||
if (\in_array($tagName, $typesToSearchFor, true)) {
|
||||
$annotations[] = $annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual content of this docblock.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return implode('', $this->lines);
|
||||
}
|
||||
|
||||
private function findAnnotationLength(int $start): int
|
||||
{
|
||||
$index = $start;
|
||||
|
||||
while (($line = $this->getLine(++$index)) !== null) {
|
||||
if ($line->containsATag()) {
|
||||
// we've 100% reached the end of the description if we get here
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$line->containsUsefulContent()) {
|
||||
// if next line is also non-useful, or contains a tag, then we're done here
|
||||
$next = $this->getLine($index + 1);
|
||||
if (null === $next || !$next->containsUsefulContent() || $next->containsATag()) {
|
||||
break;
|
||||
}
|
||||
// otherwise, continue, the annotation must have contained a blank line in its description
|
||||
}
|
||||
}
|
||||
|
||||
return $index - $start;
|
||||
}
|
||||
|
||||
private function getSingleLineDocBlockEntry(Line $line): string
|
||||
{
|
||||
$lineString = $line->getContent();
|
||||
|
||||
if ('' === $lineString) {
|
||||
return $lineString;
|
||||
}
|
||||
|
||||
$lineString = str_replace('*/', '', $lineString);
|
||||
$lineString = trim($lineString);
|
||||
|
||||
if (str_starts_with($lineString, '/**')) {
|
||||
$lineString = substr($lineString, 3);
|
||||
} elseif (str_starts_with($lineString, '*')) {
|
||||
$lineString = substr($lineString, 1);
|
||||
}
|
||||
|
||||
return trim($lineString);
|
||||
}
|
||||
}
|
||||
Vendored
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
|
||||
/**
|
||||
* This represents a line of a docblock.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Line implements \Stringable
|
||||
{
|
||||
/**
|
||||
* The content of this line.
|
||||
*/
|
||||
private string $content;
|
||||
|
||||
/**
|
||||
* Create a new line instance.
|
||||
*/
|
||||
public function __construct(string $content)
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of object.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of this line.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this line contain useful content?
|
||||
*
|
||||
* If the line contains text or tags, then this is true.
|
||||
*/
|
||||
public function containsUsefulContent(): bool
|
||||
{
|
||||
return Preg::match('/\*\s*\S+/', $this->content) && '' !== trim(str_replace(['/', '*'], ' ', $this->content));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the line contain a tag?
|
||||
*
|
||||
* If this is true, then it must be the first line of an annotation.
|
||||
*/
|
||||
public function containsATag(): bool
|
||||
{
|
||||
return Preg::match('/\*\s*@/', $this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the line the start of a docblock?
|
||||
*/
|
||||
public function isTheStart(): bool
|
||||
{
|
||||
return str_contains($this->content, '/**');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the line the end of a docblock?
|
||||
*/
|
||||
public function isTheEnd(): bool
|
||||
{
|
||||
return str_contains($this->content, '*/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of this line.
|
||||
*/
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this line by clearing its contents.
|
||||
*
|
||||
* Note that this method technically brakes the internal state of the
|
||||
* docblock, but is useful when we need to retain the indices of lines
|
||||
* during the execution of an algorithm.
|
||||
*/
|
||||
public function remove(): void
|
||||
{
|
||||
$this->content = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a blank docblock line to this line's contents.
|
||||
*
|
||||
* Note that this method technically brakes the internal state of the
|
||||
* docblock, but is useful when we need to retain the indices of lines
|
||||
* during the execution of an algorithm.
|
||||
*/
|
||||
public function addBlank(): void
|
||||
{
|
||||
$matched = Preg::match('/^(\h*\*)[^\r\n]*(\r?\n)$/', $this->content, $matches);
|
||||
|
||||
if (!$matched) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->content .= $matches[1].$matches[2];
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
/**
|
||||
* This class represents a short description (aka summary) of a docblock.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class ShortDescription
|
||||
{
|
||||
/**
|
||||
* The docblock containing the short description.
|
||||
*/
|
||||
private DocBlock $doc;
|
||||
|
||||
public function __construct(DocBlock $doc)
|
||||
{
|
||||
$this->doc = $doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line index of the line containing the end of the short
|
||||
* description, if present.
|
||||
*/
|
||||
public function getEnd(): ?int
|
||||
{
|
||||
$reachedContent = false;
|
||||
|
||||
foreach ($this->doc->getLines() as $index => $line) {
|
||||
// we went past a description, then hit a tag or blank line, so
|
||||
// the last line of the description must be the one before this one
|
||||
if ($reachedContent && ($line->containsATag() || !$line->containsUsefulContent())) {
|
||||
return $index - 1;
|
||||
}
|
||||
|
||||
// no short description was found
|
||||
if ($line->containsATag()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we've reached content, but need to check the next lines too
|
||||
// in case the short description is multi-line
|
||||
if ($line->containsUsefulContent()) {
|
||||
$reachedContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
|
||||
/**
|
||||
* This represents a tag, as defined by the proposed PSR PHPDoc standard.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
* @author Jakub Kwaśniewski <jakub@zero-85.pl>
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Tag
|
||||
{
|
||||
/**
|
||||
* All the tags defined by the proposed PSR PHPDoc standard.
|
||||
*/
|
||||
public const PSR_STANDARD_TAGS = [
|
||||
'api', 'author', 'category', 'copyright', 'deprecated', 'example',
|
||||
'global', 'internal', 'license', 'link', 'method', 'package', 'param',
|
||||
'property', 'property-read', 'property-write', 'return', 'see',
|
||||
'since', 'subpackage', 'throws', 'todo', 'uses', 'var', 'version',
|
||||
];
|
||||
|
||||
/**
|
||||
* The line containing the tag.
|
||||
*/
|
||||
private Line $line;
|
||||
|
||||
/**
|
||||
* The cached tag name.
|
||||
*/
|
||||
private ?string $name = null;
|
||||
|
||||
/**
|
||||
* Create a new tag instance.
|
||||
*/
|
||||
public function __construct(Line $line)
|
||||
{
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag name.
|
||||
*
|
||||
* This may be "param", or "return", etc.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
if (null === $this->name) {
|
||||
Preg::matchAll('/@[a-zA-Z0-9_-]+(?=\s|$)/', $this->line->getContent(), $matches);
|
||||
|
||||
if (isset($matches[0][0])) {
|
||||
$this->name = ltrim($matches[0][0], '@');
|
||||
} else {
|
||||
$this->name = 'other';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tag name.
|
||||
*
|
||||
* This will also be persisted to the upstream line and annotation.
|
||||
*/
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$current = $this->getName();
|
||||
|
||||
if ('other' === $current) {
|
||||
throw new \RuntimeException('Cannot set name on unknown tag.');
|
||||
}
|
||||
|
||||
$this->line->setContent(Preg::replace("/@{$current}/", "@{$name}", $this->line->getContent(), 1));
|
||||
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the tag a known tag?
|
||||
*
|
||||
* This is defined by if it exists in the proposed PSR PHPDoc standard.
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return \in_array($this->getName(), self::PSR_STANDARD_TAGS, true);
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
/**
|
||||
* This class is responsible for comparing tags to see if they should be kept
|
||||
* together, or kept apart.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
* @author Jakub Kwaśniewski <jakub@zero-85.pl>
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class TagComparator
|
||||
{
|
||||
/**
|
||||
* Groups of tags that should be allowed to immediately follow each other.
|
||||
*
|
||||
* @var non-empty-list<non-empty-list<string>>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const DEFAULT_GROUPS = [
|
||||
['deprecated', 'link', 'see', 'since'],
|
||||
['author', 'copyright', 'license'],
|
||||
['category', 'package', 'subpackage'],
|
||||
['property', 'property-read', 'property-write'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Should the given tags be kept together, or kept apart?
|
||||
*
|
||||
* @param list<list<string>> $groups
|
||||
*/
|
||||
public static function shouldBeTogether(Tag $first, Tag $second, array $groups = self::DEFAULT_GROUPS): bool
|
||||
{
|
||||
@trigger_error('Method '.__METHOD__.' is deprecated and will be removed in version 4.0.', \E_USER_DEPRECATED);
|
||||
|
||||
$firstName = $first->getName();
|
||||
$secondName = $second->getName();
|
||||
|
||||
if ($firstName === $secondName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if (\in_array($firstName, $group, true) && \in_array($secondName, $group, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+920
@@ -0,0 +1,920 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\DocBlock;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
|
||||
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
|
||||
use PhpCsFixer\Utils;
|
||||
|
||||
/**
|
||||
* @author Michael Vorisek <https://github.com/mvorisek>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class TypeExpression
|
||||
{
|
||||
/**
|
||||
* Regex to match any PHP identifier.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const REGEX_IDENTIFIER = '(?:(?!(?<!\*)\d)[^\x00-\x2f\x3a-\x40\x5b-\x5e\x60\x7b-\x7f]++)';
|
||||
|
||||
/**
|
||||
* Regex to match any PHPDoc type.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const REGEX_TYPES = '(?<types>(?x) # one or several types separated by `|` or `&`
|
||||
'.self::REGEX_TYPE.'
|
||||
(?:
|
||||
\h*(?<glue>[|&])\h*
|
||||
(?&type)
|
||||
)*+
|
||||
)';
|
||||
|
||||
/**
|
||||
* Based on:
|
||||
* - https://github.com/phpstan/phpdoc-parser/blob/1.26.0/doc/grammars/type.abnf fuzzing grammar
|
||||
* - and https://github.com/phpstan/phpdoc-parser/blob/1.26.0/src/Parser/PhpDocParser.php parser impl.
|
||||
*/
|
||||
private const REGEX_TYPE = '(?<type>(?x) # single type
|
||||
(?:co(?:ntra)?variant\h+)?
|
||||
(?<nullable>\??\h*)
|
||||
(?:
|
||||
(?<array_shape>
|
||||
(?<array_shape_name>(?i)(?:array|list|object)(?-i))
|
||||
(?<array_shape_start>\h*\{[\h\v*]*)
|
||||
(?<array_shape_inners>
|
||||
(?<array_shape_inner>
|
||||
(?<array_shape_inner_key>(?:(?&constant)|(?&identifier)|(?&name))\h*\??\h*:\h*|)
|
||||
(?<array_shape_inner_value>(?&types_inner))
|
||||
)
|
||||
(?:
|
||||
\h*,[\h\v*]*
|
||||
(?&array_shape_inner)
|
||||
)*+
|
||||
(?:\h*,|(?!(?&array_shape_unsealed_variadic)))
|
||||
|)
|
||||
(?<array_shape_unsealed> # unsealed array shape, e.g. `...`. `...<string>`
|
||||
(?<array_shape_unsealed_variadic>\h*\.\.\.)
|
||||
(?<array_shape_unsealed_type>
|
||||
(?<array_shape_unsealed_type_start>\h*<\h*)
|
||||
(?<array_shape_unsealed_type_a>(?&types_inner))
|
||||
(?:
|
||||
(?<array_shape_unsealed_type_comma>\h*,\h*)
|
||||
(?<array_shape_unsealed_type_b>(?&array_shape_unsealed_type_a))
|
||||
|)
|
||||
\h*>
|
||||
|)
|
||||
|)
|
||||
[\h\v*]*\}
|
||||
)
|
||||
|
|
||||
(?<callable> # callable syntax, e.g. `callable(string, int...): bool`, `\Closure<T>(T, int): T`
|
||||
(?<callable_name>(?&name))
|
||||
(?<callable_template>
|
||||
(?<callable_template_start>\h*<\h*)
|
||||
(?<callable_template_inners>
|
||||
(?<callable_template_inner>
|
||||
(?<callable_template_inner_name>
|
||||
(?&identifier)
|
||||
)
|
||||
(?<callable_template_inner_b> # template bound
|
||||
\h+(?i)(?<callable_template_inner_b_kw>of|as)(?-i)\h+
|
||||
(?<callable_template_inner_b_types>(?&types_inner))
|
||||
|)
|
||||
(?<callable_template_inner_d> # template default
|
||||
\h*=\h*
|
||||
(?<callable_template_inner_d_types>(?&types_inner))
|
||||
|)
|
||||
)
|
||||
(?:
|
||||
\h*,\h*
|
||||
(?&callable_template_inner)
|
||||
)*+
|
||||
)
|
||||
\h*>
|
||||
(?=\h*\()
|
||||
|)
|
||||
(?<callable_start>\h*\(\h*)
|
||||
(?<callable_arguments>
|
||||
(?<callable_argument>
|
||||
(?<callable_argument_type>(?&types_inner))
|
||||
(?<callable_argument_is_reference>\h*&|)
|
||||
(?<callable_argument_is_variadic>\h*\.\.\.|)
|
||||
(?<callable_argument_name>\h*\$(?&identifier)|)
|
||||
(?<callable_argument_is_optional>\h*=|)
|
||||
)
|
||||
(?:
|
||||
\h*,\h*
|
||||
(?&callable_argument)
|
||||
)*+
|
||||
(?:\h*,)?
|
||||
|)
|
||||
\h*\)
|
||||
(?:
|
||||
\h*\:\h*
|
||||
(?<callable_return>(?&type))
|
||||
)?
|
||||
)
|
||||
|
|
||||
(?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
|
||||
(?<generic_name>(?&name))
|
||||
(?<generic_start>\h*<[\h\v*]*)
|
||||
(?<generic_types>
|
||||
(?&types_inner)
|
||||
(?:
|
||||
\h*,[\h\v*]*
|
||||
(?&types_inner)
|
||||
)*+
|
||||
(?:\h*,)?
|
||||
)
|
||||
[\h\v*]*>
|
||||
)
|
||||
|
|
||||
(?<class_constant> # class constants with optional wildcard, e.g.: `Foo::*`, `Foo::CONST_A`, `FOO::CONST_*`
|
||||
(?<class_constant_name>(?&name))
|
||||
::\*?(?:(?&identifier)\*?)*
|
||||
)
|
||||
|
|
||||
(?<constant> # single constant value (case insensitive), e.g.: 1, -1.8E+6, `\'a\'`
|
||||
(?i)
|
||||
# all sorts of numbers: with or without sign, supports literal separator and several numeric systems,
|
||||
# e.g.: 1, +1.1, 1., .1, -1, 123E+8, 123_456_789, 0x7Fb4, 0b0110, 0o777
|
||||
[+-]?(?:
|
||||
(?:0b[01]++(?:_[01]++)*+)
|
||||
| (?:0o[0-7]++(?:_[0-7]++)*+)
|
||||
| (?:0x[\da-f]++(?:_[\da-f]++)*+)
|
||||
| (?:(?<constant_digits>\d++(?:_\d++)*+)|(?=\.\d))
|
||||
(?:\.(?&constant_digits)|(?<=\d)\.)?+
|
||||
(?:e[+-]?(?&constant_digits))?+
|
||||
)
|
||||
| \'(?:[^\'\\\]|\\\.)*+\'
|
||||
| "(?:[^"\\\]|\\\.)*+"
|
||||
(?-i)
|
||||
)
|
||||
|
|
||||
(?<this> # self reference, e.g.: $this, $self, @static
|
||||
(?i)
|
||||
[@$](?:this | self | static)
|
||||
(?-i)
|
||||
)
|
||||
|
|
||||
(?<name> # full name, e.g.: `int`, `\DateTime`, `\Foo\Bar`, `positive-int`
|
||||
\\\?+
|
||||
(?<identifier>'.self::REGEX_IDENTIFIER.')
|
||||
(?:[\\\\\-](?&identifier))*+
|
||||
)
|
||||
|
|
||||
(?<parenthesized> # parenthesized type, e.g.: `(int)`, `(int|\stdClass)`
|
||||
(?<parenthesized_start>
|
||||
\(\h*
|
||||
)
|
||||
(?:
|
||||
(?<parenthesized_types>
|
||||
(?&types_inner)
|
||||
)
|
||||
|
|
||||
(?<conditional> # conditional type, e.g.: `$foo is \Throwable ? false : $foo`
|
||||
(?<conditional_cond_left>
|
||||
(?:\$(?&identifier))
|
||||
|
|
||||
(?<conditional_cond_left_types>(?&types_inner))
|
||||
)
|
||||
(?<conditional_cond_middle>
|
||||
\h+(?i)is(?:\h+not)?(?-i)\h+
|
||||
)
|
||||
(?<conditional_cond_right_types>(?&types_inner))
|
||||
(?<conditional_true_start>\h*\?\h*)
|
||||
(?<conditional_true_types>(?&types_inner))
|
||||
(?<conditional_false_start>\h*:\h*)
|
||||
(?<conditional_false_types>(?&types_inner))
|
||||
)
|
||||
)
|
||||
\h*\)
|
||||
)
|
||||
)
|
||||
(?<array> # array, e.g.: `string[]`, `array<int, string>[][]`
|
||||
(\h*\[\h*\])*
|
||||
)
|
||||
(?:(?=1)0
|
||||
(?<types_inner>(?>
|
||||
(?&type)
|
||||
(?:
|
||||
\h*[|&]\h*
|
||||
(?&type)
|
||||
)*+
|
||||
))
|
||||
|)
|
||||
)';
|
||||
|
||||
private const ALIASES = [
|
||||
'boolean' => 'bool',
|
||||
'callback' => 'callable',
|
||||
'double' => 'float',
|
||||
'false' => 'bool',
|
||||
'integer' => 'int',
|
||||
'list' => 'array',
|
||||
'real' => 'float',
|
||||
'true' => 'bool',
|
||||
];
|
||||
|
||||
private string $value;
|
||||
|
||||
private bool $isCompositeType;
|
||||
|
||||
/** @var null|'&'|'|' */
|
||||
private ?string $typesGlue = null;
|
||||
|
||||
/** @var list<array{start_index: int, expression: self}> */
|
||||
private array $innerTypeExpressions = [];
|
||||
|
||||
private ?NamespaceAnalysis $namespace;
|
||||
|
||||
/** @var list<NamespaceUseAnalysis> */
|
||||
private array $namespaceUses;
|
||||
|
||||
/**
|
||||
* @param list<NamespaceUseAnalysis> $namespaceUses
|
||||
*/
|
||||
public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->namespace = $namespace;
|
||||
$this->namespaceUses = $namespaceUses;
|
||||
|
||||
$this->parse();
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
if ($this->isCompositeType) {
|
||||
return array_map(
|
||||
static fn (array $type) => $type['expression']->toString(),
|
||||
$this->innerTypeExpressions,
|
||||
);
|
||||
}
|
||||
|
||||
return [$this->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if type expression is a composite type (union or intersection).
|
||||
*/
|
||||
public function isCompositeType(): bool
|
||||
{
|
||||
return $this->isCompositeType;
|
||||
}
|
||||
|
||||
public function isUnionType(): bool
|
||||
{
|
||||
return $this->isCompositeType && '|' === $this->typesGlue;
|
||||
}
|
||||
|
||||
public function isIntersectionType(): bool
|
||||
{
|
||||
return $this->isCompositeType && '&' === $this->typesGlue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|'&'|'|'
|
||||
*/
|
||||
public function getTypesGlue(): ?string
|
||||
{
|
||||
return $this->typesGlue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure(self): self $callback
|
||||
*/
|
||||
public function mapTypes(\Closure $callback): self
|
||||
{
|
||||
$value = $this->value;
|
||||
$startIndexOffset = 0;
|
||||
|
||||
foreach ($this->innerTypeExpressions as [
|
||||
'start_index' => $startIndexOrig,
|
||||
'expression' => $inner,
|
||||
]) {
|
||||
$innerValueOrig = $inner->value;
|
||||
|
||||
$inner = $inner->mapTypes($callback);
|
||||
|
||||
if ($inner->value !== $innerValueOrig) {
|
||||
$value = substr_replace(
|
||||
$value,
|
||||
$inner->value,
|
||||
$startIndexOrig + $startIndexOffset,
|
||||
\strlen($innerValueOrig),
|
||||
);
|
||||
|
||||
$startIndexOffset += \strlen($inner->value) - \strlen($innerValueOrig);
|
||||
}
|
||||
}
|
||||
|
||||
$type = $value === $this->value
|
||||
? $this
|
||||
: $this->inner($value);
|
||||
|
||||
return $callback($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure(self): void $callback
|
||||
*/
|
||||
public function walkTypes(\Closure $callback): void
|
||||
{
|
||||
$this->mapTypes(static function (self $type) use ($callback) {
|
||||
$valueOrig = $type->value;
|
||||
$callback($type);
|
||||
\assert($type->value === $valueOrig);
|
||||
|
||||
return $type;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure(self, self): (-1|0|1) $compareCallback
|
||||
*/
|
||||
public function sortTypes(\Closure $compareCallback): self
|
||||
{
|
||||
return $this->mapTypes(function (self $type) use ($compareCallback): self {
|
||||
if (!$type->isCompositeType) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
$innerTypeExpressions = Utils::stableSort(
|
||||
$type->innerTypeExpressions,
|
||||
static fn (array $v): self => $v['expression'],
|
||||
$compareCallback,
|
||||
);
|
||||
|
||||
if ($innerTypeExpressions !== $type->innerTypeExpressions) {
|
||||
$value = implode(
|
||||
$type->getTypesGlue(),
|
||||
array_map(static fn (array $v): string => $v['expression']->toString(), $innerTypeExpressions),
|
||||
);
|
||||
|
||||
return $this->inner($value);
|
||||
}
|
||||
|
||||
return $type;
|
||||
});
|
||||
}
|
||||
|
||||
public function removeDuplicateTypes(): self
|
||||
{
|
||||
return $this->mapTypes(function (self $type): self {
|
||||
if (!$type->isCompositeType) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
$seenNormalized = [];
|
||||
$uniqueTypeExpressions = [];
|
||||
|
||||
foreach ($type->innerTypeExpressions as $innerType) {
|
||||
$normalized = $innerType['expression']
|
||||
->sortTypes(static fn (self $a, self $b): int => $a->toString() <=> $b->toString())
|
||||
->toString()
|
||||
;
|
||||
|
||||
if (!\in_array($normalized, $seenNormalized, true)) {
|
||||
$seenNormalized[] = $normalized;
|
||||
$uniqueTypeExpressions[] = $innerType['expression'];
|
||||
}
|
||||
}
|
||||
|
||||
$value = implode(
|
||||
$type->getTypesGlue(),
|
||||
array_map(static fn (self $expr): string => $expr->toString(), $uniqueTypeExpressions),
|
||||
);
|
||||
|
||||
return $this->inner($value);
|
||||
});
|
||||
}
|
||||
|
||||
public function getCommonType(): ?string
|
||||
{
|
||||
$mainType = null;
|
||||
|
||||
foreach ($this->getTypes() as $type) {
|
||||
if ('null' === $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($type, '?')) {
|
||||
$type = substr($type, 1);
|
||||
}
|
||||
|
||||
if (Preg::match('/\[\h*\]$/', $type)) {
|
||||
$type = 'array';
|
||||
} elseif (Preg::match('/^(.+?)\h*[<{(]/', $type, $matches)) {
|
||||
$type = $matches[1];
|
||||
}
|
||||
|
||||
if (isset(self::ALIASES[$type])) {
|
||||
$type = self::ALIASES[$type];
|
||||
}
|
||||
|
||||
if (null === $mainType || $type === $mainType) {
|
||||
$mainType = $type;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$mainType = $this->getParentType($type, $mainType);
|
||||
|
||||
if (null === $mainType) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $mainType;
|
||||
}
|
||||
|
||||
public function allowsNull(): bool
|
||||
{
|
||||
foreach ($this->getTypes() as $type) {
|
||||
if (\in_array($type, ['null', 'mixed'], true) || str_starts_with($type, '?')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function parse(): void
|
||||
{
|
||||
$seenGlues = null;
|
||||
$innerValues = [];
|
||||
|
||||
$index = 0;
|
||||
while (true) {
|
||||
Preg::match(
|
||||
'{\G'.self::REGEX_TYPE.'(?<glue_raw>\h*(?<glue>[|&])\h*(?!$)|$)}',
|
||||
$this->value,
|
||||
$matches,
|
||||
\PREG_OFFSET_CAPTURE,
|
||||
$index,
|
||||
);
|
||||
|
||||
if ([] === $matches) {
|
||||
throw new \Exception('Unable to parse phpdoc type '.var_export($this->value, true));
|
||||
}
|
||||
|
||||
\assert(isset($matches[0], $matches['type']));
|
||||
|
||||
if (null === $seenGlues) {
|
||||
if (($matches['glue'][0] ?? '') === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
$seenGlues = ['|' => false, '&' => false];
|
||||
}
|
||||
|
||||
if (($matches['glue'][0] ?? '') !== '') {
|
||||
\assert(isset($seenGlues[$matches['glue'][0]]));
|
||||
$seenGlues[$matches['glue'][0]] = true;
|
||||
}
|
||||
|
||||
$innerValues[] = [
|
||||
'start_index' => $index,
|
||||
'value' => $matches['type'][0],
|
||||
'next_glue' => $matches['glue'][0] ?? null,
|
||||
'next_glue_raw' => $matches['glue_raw'][0] ?? null,
|
||||
];
|
||||
|
||||
$consumedValueLength = \strlen($matches[0][0]);
|
||||
$index += $consumedValueLength;
|
||||
|
||||
if (\strlen($this->value) <= $index) {
|
||||
\assert(\strlen($this->value) === $index);
|
||||
|
||||
$seenGlues = array_filter($seenGlues);
|
||||
\assert([] !== $seenGlues);
|
||||
|
||||
$this->isCompositeType = true;
|
||||
$this->typesGlue = array_key_first($seenGlues);
|
||||
|
||||
if (1 === \count($seenGlues)) {
|
||||
foreach ($innerValues as $innerValue) {
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $innerValue['start_index'],
|
||||
'expression' => $this->inner($innerValue['value']),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
for ($i = 0; $i < \count($innerValues); ++$i) {
|
||||
$innerStartIndex = $innerValues[$i]['start_index'];
|
||||
$innerValue = '';
|
||||
while (true) {
|
||||
$innerValue .= $innerValues[$i]['value'];
|
||||
|
||||
if (($innerValues[$i]['next_glue'] ?? $this->typesGlue) === $this->typesGlue) {
|
||||
break;
|
||||
}
|
||||
|
||||
$innerValue .= $innerValues[$i]['next_glue_raw'];
|
||||
|
||||
++$i;
|
||||
\assert(isset($innerValues[$i])); // for PHPStan
|
||||
}
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $innerStartIndex,
|
||||
'expression' => $this->inner($innerValue),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->isCompositeType = false;
|
||||
|
||||
\assert(isset($matches['nullable'], $matches['array'], $matches['class_constant']));
|
||||
|
||||
if ('' !== $matches['nullable'][0]) {
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => \strlen($matches['nullable'][0]),
|
||||
'expression' => $this->inner(substr($matches['type'][0], \strlen($matches['nullable'][0]))),
|
||||
];
|
||||
} elseif ('' !== $matches['array'][0]) {
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => 0,
|
||||
'expression' => $this->inner(substr($matches['type'][0], 0, -\strlen($matches['array'][0]))),
|
||||
];
|
||||
} elseif ('' !== ($matches['generic'][0] ?? '') && 0 === $matches['generic'][1]) {
|
||||
\assert(isset($matches['generic_name'], $matches['generic_start'], $matches['generic_types']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => 0,
|
||||
'expression' => $this->inner($matches['generic_name'][0]),
|
||||
];
|
||||
|
||||
$this->parseCommaSeparatedInnerTypes(
|
||||
\strlen($matches['generic_name'][0]) + \strlen($matches['generic_start'][0]),
|
||||
$matches['generic_types'][0],
|
||||
);
|
||||
} elseif ('' !== ($matches['callable'][0] ?? '') && 0 === $matches['callable'][1]) {
|
||||
\assert(isset($matches['callable_name'], $matches['callable_template'], $matches['callable_start'], $matches['callable_arguments'], $matches['callable_template_start'], $matches['callable_template_inners']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => 0,
|
||||
'expression' => $this->inner($matches['callable_name'][0]),
|
||||
];
|
||||
|
||||
$this->parseCallableTemplateInnerTypes(
|
||||
\strlen($matches['callable_name'][0])
|
||||
+ \strlen($matches['callable_template_start'][0]),
|
||||
$matches['callable_template_inners'][0],
|
||||
);
|
||||
|
||||
$this->parseCallableArgumentTypes(
|
||||
\strlen($matches['callable_name'][0])
|
||||
+ \strlen($matches['callable_template'][0])
|
||||
+ \strlen($matches['callable_start'][0]),
|
||||
$matches['callable_arguments'][0],
|
||||
);
|
||||
|
||||
if ('' !== ($matches['callable_return'][0] ?? '')) {
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => \strlen($this->value) - \strlen($matches['callable_return'][0]),
|
||||
'expression' => $this->inner($matches['callable_return'][0]),
|
||||
];
|
||||
}
|
||||
} elseif ('' !== ($matches['array_shape'][0] ?? '') && 0 === $matches['array_shape'][1]) {
|
||||
\assert(isset($matches['array_shape_name'], $matches['array_shape_start'], $matches['array_shape_inners']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => 0,
|
||||
'expression' => $this->inner($matches['array_shape_name'][0]),
|
||||
];
|
||||
|
||||
$nextIndex = \strlen($matches['array_shape_name'][0]) + \strlen($matches['array_shape_start'][0]);
|
||||
|
||||
$this->parseArrayShapeInnerTypes(
|
||||
$nextIndex,
|
||||
$matches['array_shape_inners'][0],
|
||||
);
|
||||
|
||||
if ('' !== ($matches['array_shape_unsealed_type'][0] ?? '')) {
|
||||
\assert(isset($matches['array_shape_unsealed_variadic'], $matches['array_shape_unsealed_type_start'], $matches['array_shape_unsealed_type_a']));
|
||||
|
||||
$nextIndex += \strlen($matches['array_shape_inners'][0])
|
||||
+ \strlen($matches['array_shape_unsealed_variadic'][0])
|
||||
+ \strlen($matches['array_shape_unsealed_type_start'][0]);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $nextIndex,
|
||||
'expression' => $this->inner($matches['array_shape_unsealed_type_a'][0]),
|
||||
];
|
||||
|
||||
if ('' !== ($matches['array_shape_unsealed_type_b'][0] ?? '')) {
|
||||
\assert(isset($matches['array_shape_unsealed_type_comma']));
|
||||
|
||||
$nextIndex += \strlen($matches['array_shape_unsealed_type_a'][0])
|
||||
+ \strlen($matches['array_shape_unsealed_type_comma'][0]);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $nextIndex,
|
||||
'expression' => $this->inner($matches['array_shape_unsealed_type_b'][0]),
|
||||
];
|
||||
}
|
||||
}
|
||||
} elseif ('' !== ($matches['parenthesized'][0] ?? '') && 0 === $matches['parenthesized'][1]) {
|
||||
\assert(isset($matches['parenthesized_start']));
|
||||
$index = \strlen($matches['parenthesized_start'][0]);
|
||||
|
||||
if ('' !== ($matches['conditional'][0] ?? '')) {
|
||||
\assert(isset($matches['conditional_cond_left'], $matches['conditional_cond_middle'], $matches['conditional_cond_right_types'], $matches['conditional_true_start'], $matches['conditional_true_types'], $matches['conditional_false_start'], $matches['conditional_false_types']));
|
||||
|
||||
if ('' !== ($matches['conditional_cond_left_types'][0] ?? '')) {
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $index,
|
||||
'expression' => $this->inner($matches['conditional_cond_left_types'][0]),
|
||||
];
|
||||
}
|
||||
|
||||
$index += \strlen($matches['conditional_cond_left'][0]) + \strlen($matches['conditional_cond_middle'][0]);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $index,
|
||||
'expression' => $this->inner($matches['conditional_cond_right_types'][0]),
|
||||
];
|
||||
|
||||
$index += \strlen($matches['conditional_cond_right_types'][0]) + \strlen($matches['conditional_true_start'][0]);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $index,
|
||||
'expression' => $this->inner($matches['conditional_true_types'][0]),
|
||||
];
|
||||
|
||||
$index += \strlen($matches['conditional_true_types'][0]) + \strlen($matches['conditional_false_start'][0]);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $index,
|
||||
'expression' => $this->inner($matches['conditional_false_types'][0]),
|
||||
];
|
||||
} else {
|
||||
\assert(isset($matches['parenthesized_types']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $index,
|
||||
'expression' => $this->inner($matches['parenthesized_types'][0]),
|
||||
];
|
||||
}
|
||||
} elseif ('' !== $matches['class_constant'][0]) {
|
||||
\assert(isset($matches['class_constant_name']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => 0,
|
||||
'expression' => $this->inner($matches['class_constant_name'][0]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
|
||||
{
|
||||
$index = 0;
|
||||
while (\strlen($value) !== $index) {
|
||||
Preg::match(
|
||||
'{\G'.self::REGEX_TYPES.'(?:\h*,[\h\v*]*|$)}',
|
||||
$value,
|
||||
$matches,
|
||||
0,
|
||||
$index,
|
||||
);
|
||||
|
||||
\assert(isset($matches[0], $matches['types']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex + $index,
|
||||
'expression' => $this->inner($matches['types']),
|
||||
];
|
||||
|
||||
$index += \strlen($matches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCallableTemplateInnerTypes(int $startIndex, string $value): void
|
||||
{
|
||||
$index = 0;
|
||||
while (\strlen($value) !== $index) {
|
||||
Preg::match(
|
||||
'{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_template_inner>(?&callable_template_inner))(?:\h*,\h*|$))}',
|
||||
$value,
|
||||
$prematches,
|
||||
0,
|
||||
$index,
|
||||
);
|
||||
\assert(isset($prematches[0], $prematches['_callable_template_inner']));
|
||||
$consumedValue = $prematches['_callable_template_inner'];
|
||||
$consumedValueLength = \strlen($consumedValue);
|
||||
$consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
|
||||
|
||||
$addedPrefix = 'Closure<';
|
||||
Preg::match(
|
||||
'{^'.self::REGEX_TYPES.'$}',
|
||||
$addedPrefix.$consumedValue.'>(): void',
|
||||
$matches,
|
||||
\PREG_OFFSET_CAPTURE,
|
||||
);
|
||||
|
||||
\assert(isset($matches['callable_template_inner_b'], $matches['callable_template_inner_d']));
|
||||
|
||||
if ('' !== $matches['callable_template_inner_b'][0]) {
|
||||
\assert(isset($matches['callable_template_inner_b_types']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex + $index + $matches['callable_template_inner_b_types'][1]
|
||||
- \strlen($addedPrefix),
|
||||
'expression' => $this->inner($matches['callable_template_inner_b_types'][0]),
|
||||
];
|
||||
}
|
||||
|
||||
if ('' !== $matches['callable_template_inner_d'][0]) {
|
||||
\assert(isset($matches['callable_template_inner_d_types']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex + $index + $matches['callable_template_inner_d_types'][1]
|
||||
- \strlen($addedPrefix),
|
||||
'expression' => $this->inner($matches['callable_template_inner_d_types'][0]),
|
||||
];
|
||||
}
|
||||
|
||||
$index += $consumedValueLength + $consumedCommaLength;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCallableArgumentTypes(int $startIndex, string $value): void
|
||||
{
|
||||
$index = 0;
|
||||
while (\strlen($value) !== $index) {
|
||||
Preg::match(
|
||||
'{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_argument>(?&callable_argument))(?:\h*,\h*|$))}',
|
||||
$value,
|
||||
$prematches,
|
||||
0,
|
||||
$index,
|
||||
);
|
||||
\assert(isset($prematches[0], $prematches['_callable_argument']));
|
||||
$consumedValue = $prematches['_callable_argument'];
|
||||
$consumedValueLength = \strlen($consumedValue);
|
||||
$consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
|
||||
|
||||
$addedPrefix = 'Closure(';
|
||||
Preg::match(
|
||||
'{^'.self::REGEX_TYPES.'$}',
|
||||
$addedPrefix.$consumedValue.'): void',
|
||||
$matches,
|
||||
\PREG_OFFSET_CAPTURE,
|
||||
);
|
||||
|
||||
\assert(isset($matches['callable_argument_type']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex + $index,
|
||||
'expression' => $this->inner($matches['callable_argument_type'][0]),
|
||||
];
|
||||
|
||||
$index += $consumedValueLength + $consumedCommaLength;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseArrayShapeInnerTypes(int $startIndex, string $value): void
|
||||
{
|
||||
$index = 0;
|
||||
while (\strlen($value) !== $index) {
|
||||
Preg::match(
|
||||
'{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_array_shape_inner>(?&array_shape_inner))(?:\h*,[\h\v*]*|$))}',
|
||||
$value,
|
||||
$prematches,
|
||||
0,
|
||||
$index,
|
||||
);
|
||||
\assert(isset($prematches[0], $prematches['_array_shape_inner']));
|
||||
$consumedValue = $prematches['_array_shape_inner'];
|
||||
$consumedValueLength = \strlen($consumedValue);
|
||||
$consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
|
||||
|
||||
$addedPrefix = 'array{';
|
||||
Preg::match(
|
||||
'{^'.self::REGEX_TYPES.'$}',
|
||||
$addedPrefix.$consumedValue.'}',
|
||||
$matches,
|
||||
\PREG_OFFSET_CAPTURE,
|
||||
);
|
||||
|
||||
\assert(isset($matches['array_shape_inner_value']));
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1]
|
||||
- \strlen($addedPrefix),
|
||||
'expression' => $this->inner($matches['array_shape_inner_value'][0]),
|
||||
];
|
||||
|
||||
$index += $consumedValueLength + $consumedCommaLength;
|
||||
}
|
||||
}
|
||||
|
||||
private function inner(string $value): self
|
||||
{
|
||||
return new self($value, $this->namespace, $this->namespaceUses);
|
||||
}
|
||||
|
||||
private function getParentType(string $type1, string $type2): ?string
|
||||
{
|
||||
$types = [
|
||||
$this->normalize($type1),
|
||||
$this->normalize($type2),
|
||||
];
|
||||
natcasesort($types);
|
||||
$types = implode('|', $types);
|
||||
|
||||
$parents = [
|
||||
'array|Traversable' => 'iterable',
|
||||
'array|iterable' => 'iterable',
|
||||
'iterable|Traversable' => 'iterable',
|
||||
'self|static' => 'self',
|
||||
];
|
||||
|
||||
return $parents[$types] ?? null;
|
||||
}
|
||||
|
||||
private function normalize(string $type): string
|
||||
{
|
||||
if (isset(self::ALIASES[$type])) {
|
||||
return self::ALIASES[$type];
|
||||
}
|
||||
|
||||
if (\in_array($type, [
|
||||
'array',
|
||||
'bool',
|
||||
'callable',
|
||||
'false',
|
||||
'float',
|
||||
'int',
|
||||
'iterable',
|
||||
'mixed',
|
||||
'never',
|
||||
'null',
|
||||
'object',
|
||||
'resource',
|
||||
'string',
|
||||
'true',
|
||||
'void',
|
||||
], true)) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (Preg::match('/\[\]$/', $type)) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
if (Preg::match('/^(.+?)</', $type, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if (str_starts_with($type, '\\')) {
|
||||
return substr($type, 1);
|
||||
}
|
||||
|
||||
foreach ($this->namespaceUses as $namespaceUse) {
|
||||
if ($namespaceUse->getShortName() === $type) {
|
||||
return $namespaceUse->getFullName();
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return "{$this->namespace->getFullName()}\\{$type}";
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Doctrine\Annotation;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2006-2013 Doctrine Project.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DocLexer
|
||||
{
|
||||
public const T_NONE = 1;
|
||||
public const T_INTEGER = 2;
|
||||
public const T_STRING = 3;
|
||||
public const T_FLOAT = 4;
|
||||
|
||||
// All tokens that are also identifiers should be >= 100
|
||||
public const T_IDENTIFIER = 100;
|
||||
public const T_AT = 101;
|
||||
public const T_CLOSE_CURLY_BRACES = 102;
|
||||
public const T_CLOSE_PARENTHESIS = 103;
|
||||
public const T_COMMA = 104;
|
||||
public const T_EQUALS = 105;
|
||||
public const T_NAMESPACE_SEPARATOR = 107;
|
||||
public const T_OPEN_CURLY_BRACES = 108;
|
||||
public const T_OPEN_PARENTHESIS = 109;
|
||||
public const T_COLON = 112;
|
||||
public const T_MINUS = 113;
|
||||
|
||||
private const CATCHABLE_PATTERNS = [
|
||||
'[a-z_\\\][a-z0-9_\:\\\]*[a-z_][a-z0-9_]*',
|
||||
'(?:[+-]?[0-9]+(?:[\.][0-9]+)*)(?:[eE][+-]?[0-9]+)?',
|
||||
'"(?:""|[^"])*+"',
|
||||
];
|
||||
|
||||
private const NON_CATCHABLE_PATTERNS = ['\s+', '\*+', '(.)'];
|
||||
|
||||
/** @var array<string, self::T_*> */
|
||||
private array $noCase = [
|
||||
'@' => self::T_AT,
|
||||
',' => self::T_COMMA,
|
||||
'(' => self::T_OPEN_PARENTHESIS,
|
||||
')' => self::T_CLOSE_PARENTHESIS,
|
||||
'{' => self::T_OPEN_CURLY_BRACES,
|
||||
'}' => self::T_CLOSE_CURLY_BRACES,
|
||||
'=' => self::T_EQUALS,
|
||||
':' => self::T_COLON,
|
||||
'-' => self::T_MINUS,
|
||||
'\\' => self::T_NAMESPACE_SEPARATOR,
|
||||
];
|
||||
|
||||
/** @var list<Token> */
|
||||
private array $tokens = [];
|
||||
|
||||
private int $position = 0;
|
||||
|
||||
private int $peek = 0;
|
||||
|
||||
private ?string $regex = null;
|
||||
|
||||
public function setInput(string $input): void
|
||||
{
|
||||
$this->tokens = [];
|
||||
$this->reset();
|
||||
$this->scan($input);
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->peek = 0;
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function peek(): ?Token
|
||||
{
|
||||
return $this->tokens[$this->position + $this->peek++] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self::T_*
|
||||
*/
|
||||
private function getType(string &$value): int
|
||||
{
|
||||
$type = self::T_NONE;
|
||||
|
||||
if ('"' === $value[0]) {
|
||||
$value = str_replace('""', '"', substr($value, 1, \strlen($value) - 2));
|
||||
|
||||
return self::T_STRING;
|
||||
}
|
||||
|
||||
if (isset($this->noCase[$value])) {
|
||||
return $this->noCase[$value];
|
||||
}
|
||||
|
||||
if ('_' === $value[0] || '\\' === $value[0] || !Preg::match('/[^A-Za-z]/', $value[0])) {
|
||||
return self::T_IDENTIFIER;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return str_contains($value, '.') || str_contains(strtolower($value), 'e')
|
||||
? self::T_FLOAT : self::T_INTEGER;
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function scan(string $input): void
|
||||
{
|
||||
$this->regex ??= \sprintf(
|
||||
'/(%s)|%s/%s',
|
||||
implode(')|(', self::CATCHABLE_PATTERNS),
|
||||
implode('|', self::NON_CATCHABLE_PATTERNS),
|
||||
'iu',
|
||||
);
|
||||
|
||||
$flags = \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_OFFSET_CAPTURE;
|
||||
$matches = Preg::split($this->regex, $input, -1, $flags);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
// Must remain before 'value' assignment since it can change content
|
||||
$firstMatch = $match[0];
|
||||
$type = $this->getType($firstMatch);
|
||||
|
||||
$this->tokens[] = new Token($type, $firstMatch, (int) $match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Doctrine\Annotation;
|
||||
|
||||
/**
|
||||
* A Doctrine annotation token.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Token
|
||||
{
|
||||
private int $type;
|
||||
|
||||
private string $content;
|
||||
|
||||
private int $position;
|
||||
|
||||
/**
|
||||
* @param int $type The type
|
||||
* @param string $content The content
|
||||
*/
|
||||
public function __construct(int $type = DocLexer::T_NONE, string $content = '', int $position = 0)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->content = $content;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(int $type): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the token type is one of the given types.
|
||||
*
|
||||
* @param int|list<int> $types
|
||||
*/
|
||||
public function isType($types): bool
|
||||
{
|
||||
if (!\is_array($types)) {
|
||||
$types = [$types];
|
||||
}
|
||||
|
||||
return \in_array($this->getType(), $types, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the content with an empty string.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->setContent('');
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Doctrine\Annotation;
|
||||
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\Tokenizer\Token as PhpToken;
|
||||
|
||||
/**
|
||||
* A list of Doctrine annotation tokens.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @extends \SplFixedArray<Token>
|
||||
*
|
||||
* `SplFixedArray` uses `T|null` in return types because value can be null if an offset is unset or if the size does not match the number of elements.
|
||||
* But our class takes care of it and always ensures correct size and indexes, so that these methods never return `null` instead of `Token`.
|
||||
*
|
||||
* @method Token offsetGet($offset)
|
||||
* @method \Traversable<int, Token> getIterator()
|
||||
* @method array<int, Token> toArray()
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class Tokens extends \SplFixedArray
|
||||
{
|
||||
/**
|
||||
* @param list<string> $ignoredTags
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function createFromDocComment(PhpToken $input, array $ignoredTags = []): self
|
||||
{
|
||||
if (!$input->isGivenKind(\T_DOC_COMMENT)) {
|
||||
throw new \InvalidArgumentException('Input must be a T_DOC_COMMENT token.');
|
||||
}
|
||||
|
||||
$tokens = [];
|
||||
|
||||
$content = $input->getContent();
|
||||
$ignoredTextPosition = 0;
|
||||
$currentPosition = 0;
|
||||
$token = null;
|
||||
while (false !== $nextAtPosition = strpos($content, '@', $currentPosition)) {
|
||||
if (0 !== $nextAtPosition && !Preg::match('/\s/', $content[$nextAtPosition - 1])) {
|
||||
$currentPosition = $nextAtPosition + 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$lexer = new DocLexer();
|
||||
$lexer->setInput(substr($content, $nextAtPosition));
|
||||
|
||||
$scannedTokens = [];
|
||||
$index = 0;
|
||||
$nbScannedTokensToUse = 0;
|
||||
$nbScopes = 0;
|
||||
while (null !== $token = $lexer->peek()) {
|
||||
if (0 === $index && !$token->isType(DocLexer::T_AT)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (1 === $index) {
|
||||
if (!$token->isType(DocLexer::T_IDENTIFIER) || \in_array($token->getContent(), $ignoredTags, true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$nbScannedTokensToUse = 2;
|
||||
}
|
||||
|
||||
if ($index >= 2 && 0 === $nbScopes && !$token->isType([DocLexer::T_NONE, DocLexer::T_OPEN_PARENTHESIS])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$scannedTokens[] = $token;
|
||||
|
||||
if ($token->isType(DocLexer::T_OPEN_PARENTHESIS)) {
|
||||
++$nbScopes;
|
||||
} elseif ($token->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
|
||||
if (0 === --$nbScopes) {
|
||||
$nbScannedTokensToUse = \count($scannedTokens);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
++$index;
|
||||
}
|
||||
|
||||
if (0 !== $nbScopes) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (0 !== $nbScannedTokensToUse) {
|
||||
$ignoredTextLength = $nextAtPosition - $ignoredTextPosition;
|
||||
if (0 !== $ignoredTextLength) {
|
||||
$tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition, $ignoredTextLength));
|
||||
}
|
||||
|
||||
$lastTokenEndIndex = 0;
|
||||
foreach (\array_slice($scannedTokens, 0, $nbScannedTokensToUse) as $scannedToken) {
|
||||
$token = $scannedToken->isType(DocLexer::T_STRING)
|
||||
? new Token(
|
||||
$scannedToken->getType(),
|
||||
'"'.str_replace('"', '""', $scannedToken->getContent()).'"',
|
||||
$scannedToken->getPosition(),
|
||||
)
|
||||
: $scannedToken;
|
||||
|
||||
$missingTextLength = $token->getPosition() - $lastTokenEndIndex;
|
||||
if ($missingTextLength > 0) {
|
||||
$tokens[] = new Token(DocLexer::T_NONE, substr(
|
||||
$content,
|
||||
$nextAtPosition + $lastTokenEndIndex,
|
||||
$missingTextLength,
|
||||
));
|
||||
}
|
||||
|
||||
$tokens[] = new Token($token->getType(), $token->getContent());
|
||||
$lastTokenEndIndex = $token->getPosition() + \strlen($token->getContent());
|
||||
}
|
||||
|
||||
$currentPosition = $ignoredTextPosition = $nextAtPosition + $token->getPosition() + \strlen($token->getContent());
|
||||
} else {
|
||||
$currentPosition = $nextAtPosition + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ignoredTextPosition < \strlen($content)) {
|
||||
$tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition));
|
||||
}
|
||||
|
||||
return self::fromArray($tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create token collection from array.
|
||||
*
|
||||
* @param array<int, Token> $array the array to import
|
||||
* @param ?bool $saveIndices save the numeric indices used in the original array, default is yes
|
||||
*/
|
||||
public static function fromArray($array, $saveIndices = null): self
|
||||
{
|
||||
$tokens = new self(\count($array));
|
||||
|
||||
if (null === $saveIndices || $saveIndices) {
|
||||
foreach ($array as $key => $val) {
|
||||
$tokens[$key] = $val;
|
||||
}
|
||||
} else {
|
||||
$index = 0;
|
||||
|
||||
foreach ($array as $val) {
|
||||
$tokens[$index++] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the closest next token that is neither a comment nor a whitespace token.
|
||||
*/
|
||||
public function getNextMeaningfulToken(int $index): ?int
|
||||
{
|
||||
return $this->getMeaningfulTokenSibling($index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the last token that is part of the annotation at the given index.
|
||||
*/
|
||||
public function getAnnotationEnd(int $index): ?int
|
||||
{
|
||||
$currentIndex = null;
|
||||
|
||||
if (isset($this[$index + 2])) {
|
||||
if ($this[$index + 2]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
|
||||
$currentIndex = $index + 2;
|
||||
} elseif (
|
||||
isset($this[$index + 3])
|
||||
&& $this[$index + 2]->isType(DocLexer::T_NONE)
|
||||
&& $this[$index + 3]->isType(DocLexer::T_OPEN_PARENTHESIS)
|
||||
&& Preg::match('/^(\R\s*\*\s*)*\s*$/', $this[$index + 2]->getContent())
|
||||
) {
|
||||
$currentIndex = $index + 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $currentIndex) {
|
||||
$level = 0;
|
||||
for ($max = \count($this); $currentIndex < $max; ++$currentIndex) {
|
||||
if ($this[$currentIndex]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
|
||||
++$level;
|
||||
} elseif ($this[$currentIndex]->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
|
||||
--$level;
|
||||
}
|
||||
|
||||
if (0 === $level) {
|
||||
return $currentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $index + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the code from the tokens.
|
||||
*/
|
||||
public function getCode(): string
|
||||
{
|
||||
$code = '';
|
||||
foreach ($this as $token) {
|
||||
$code .= $token->getContent();
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a token at the given index.
|
||||
*/
|
||||
public function insertAt(int $index, Token $token): void
|
||||
{
|
||||
$this->setSize($this->getSize() + 1);
|
||||
|
||||
for ($i = $this->getSize() - 1; $i > $index; --$i) {
|
||||
$this[$i] = $this[$i - 1] ?? new Token();
|
||||
}
|
||||
|
||||
$this[$index] = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|int $index
|
||||
* @param null|Token $token
|
||||
*/
|
||||
public function offsetSet($index, $token): void
|
||||
{
|
||||
if (!$token instanceof Token) {
|
||||
throw new \InvalidArgumentException(\sprintf('Token must be an instance of %s, "%s" given.', Token::class, get_debug_type($token)));
|
||||
}
|
||||
|
||||
parent::offsetSet($index, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $index
|
||||
*
|
||||
* @throws \OutOfBoundsException
|
||||
*/
|
||||
public function offsetUnset($index): void
|
||||
{
|
||||
if (!isset($this[$index])) {
|
||||
throw new \OutOfBoundsException(\sprintf('Index "%s" is invalid or does not exist.', $index));
|
||||
}
|
||||
|
||||
$max = \count($this) - 1;
|
||||
while ($index < $max) {
|
||||
$this[$index] = $this[$index + 1];
|
||||
++$index;
|
||||
}
|
||||
|
||||
parent::offsetUnset($index);
|
||||
|
||||
$this->setSize($max);
|
||||
}
|
||||
|
||||
private function getMeaningfulTokenSibling(int $index, int $direction): ?int
|
||||
{
|
||||
while (true) {
|
||||
$index += $direction;
|
||||
|
||||
if (!$this->offsetExists($index)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$this[$index]->isType(DocLexer::T_NONE)) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Documentation;
|
||||
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\Preg;
|
||||
use PhpCsFixer\Utils;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DocumentationLocator
|
||||
{
|
||||
private string $path;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->path = \dirname(__DIR__, 2).'/doc';
|
||||
}
|
||||
|
||||
public function getFixersDocumentationDirectoryPath(): string
|
||||
{
|
||||
return $this->path.'/rules';
|
||||
}
|
||||
|
||||
public function getFixersDocumentationIndexFilePath(): string
|
||||
{
|
||||
return $this->getFixersDocumentationDirectoryPath().'/index.rst';
|
||||
}
|
||||
|
||||
public function getFixerDocumentationFilePath(FixerInterface $fixer): string
|
||||
{
|
||||
return $this->getFixersDocumentationDirectoryPath().'/'.Preg::replaceCallback(
|
||||
'/^.*\\\(.+)\\\(.+)Fixer$/',
|
||||
static fn (array $matches): string => Utils::camelCaseToUnderscore($matches[1]).'/'.Utils::camelCaseToUnderscore($matches[2]),
|
||||
\get_class($fixer),
|
||||
).'.rst';
|
||||
}
|
||||
|
||||
public function getFixerDocumentationFileRelativePath(FixerInterface $fixer): string
|
||||
{
|
||||
return Preg::replace(
|
||||
'#^'.preg_quote($this->getFixersDocumentationDirectoryPath(), '#').'/#',
|
||||
'',
|
||||
$this->getFixerDocumentationFilePath($fixer),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRuleSetsDocumentationDirectoryPath(): string
|
||||
{
|
||||
return $this->path.'/ruleSets';
|
||||
}
|
||||
|
||||
public function getRuleSetsDocumentationIndexFilePath(): string
|
||||
{
|
||||
return $this->getRuleSetsDocumentationDirectoryPath().'/index.rst';
|
||||
}
|
||||
|
||||
public function getRuleSetsDocumentationFilePath(string $name): string
|
||||
{
|
||||
return $this->getRuleSetsDocumentationDirectoryPath().'/'.str_replace(':risky', 'Risky', ucfirst(substr($name, 1))).'.rst';
|
||||
}
|
||||
|
||||
public function getUsageFilePath(): string
|
||||
{
|
||||
return $this->path.'/usage.rst';
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Documentation;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DocumentationTag
|
||||
{
|
||||
/**
|
||||
* @var DocumentationTagType::*
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
public string $type;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @param DocumentationTagType::* $type
|
||||
*/
|
||||
public function __construct(
|
||||
string $type,
|
||||
string $title,
|
||||
?string $description = null
|
||||
) {
|
||||
$this->type = $type;
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace PhpCsFixer\Documentation;
|
||||
|
||||
use PhpCsFixer\Console\Application;
|
||||
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
|
||||
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
|
||||
use PhpCsFixer\Fixer\ExperimentalFixerInterface;
|
||||
use PhpCsFixer\Fixer\FixerInterface;
|
||||
use PhpCsFixer\Fixer\InternalFixerInterface;
|
||||
use PhpCsFixer\FixerConfiguration\FixerOptionInterface;
|
||||
use PhpCsFixer\RuleSet\AutomaticRuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\DeprecatedRuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\InternalRuleSetDefinitionInterface;
|
||||
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
|
||||
use PhpCsFixer\Utils;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
|
||||
*/
|
||||
final class DocumentationTagGenerator
|
||||
{
|
||||
/**
|
||||
* @return list<DocumentationTag>
|
||||
*/
|
||||
public static function analyseRuleSet(RuleSetDefinitionInterface $ruleSetDefinition): array
|
||||
{
|
||||
$tags = [];
|
||||
|
||||
// not possible for set to be DocumentationTagType::EXPERIMENTAL
|
||||
|
||||
if ($ruleSetDefinition instanceof InternalRuleSetDefinitionInterface) {
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::INTERNAL,
|
||||
'This rule set is INTERNAL',
|
||||
'Set is expected to be used only on PHP CS Fixer project itself.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($ruleSetDefinition instanceof DeprecatedRuleSetDefinitionInterface) {
|
||||
$alternatives = $ruleSetDefinition->getSuccessorsNames();
|
||||
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::DEPRECATED,
|
||||
\sprintf('This rule set is DEPRECATED and will be removed in the next major version %d.0', Application::getMajorVersion() + 1),
|
||||
0 !== \count($alternatives)
|
||||
? \sprintf(
|
||||
'You should use %s instead.',
|
||||
Utils::naturalLanguageJoinWithBackticks($alternatives),
|
||||
)
|
||||
: 'No replacement available.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($ruleSetDefinition->isRisky()) {
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::RISKY,
|
||||
'This rule set is RISKY',
|
||||
'This set contains rules that are risky. Using it may lead to changes in your code\'s logic and behaviour. Use it with caution and review changes before incorporating them into your code base.',
|
||||
);
|
||||
}
|
||||
|
||||
// not possible for set to be DocumentationTagType::CONFIGURABLE
|
||||
|
||||
if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) {
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::AUTOMATIC,
|
||||
'This rule set is AUTOMATIC',
|
||||
'⚡ '.strip_tags(AutomaticRuleSetDefinitionInterface::WARNING_MESSAGE_DECORATED),
|
||||
);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<DocumentationTag>
|
||||
*/
|
||||
public static function analyseRule(FixerInterface $fixer): array
|
||||
{
|
||||
$tags = [];
|
||||
|
||||
if ($fixer instanceof ExperimentalFixerInterface) {
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::EXPERIMENTAL,
|
||||
'This rule is EXPERIMENTAL',
|
||||
'Rule is not covered with backward compatibility promise and may produce unstable or unexpected results, use it at your own risk. Rule\'s behaviour may be changed at any point, including rule\'s name; its options\' names, availability and allowed values; its default configuration. Rule may be even removed without prior notice. Feel free to provide feedback and help with determining final state of the rule.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($fixer instanceof InternalFixerInterface) {
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::INTERNAL,
|
||||
'This rule is INTERNAL',
|
||||
'Rule is expected to be used only on PHP CS Fixer project itself.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($fixer instanceof DeprecatedFixerInterface) {
|
||||
$alternatives = $fixer->getSuccessorsNames();
|
||||
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::DEPRECATED,
|
||||
\sprintf('This rule is DEPRECATED and will be removed in the next major version %d.0', Application::getMajorVersion() + 1),
|
||||
0 !== \count($alternatives)
|
||||
? \sprintf(
|
||||
'You should use %s instead.',
|
||||
Utils::naturalLanguageJoinWithBackticks($alternatives),
|
||||
)
|
||||
: 'No replacement available.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($fixer->isRisky()) {
|
||||
$riskyDescription = $fixer->getDefinition()->getRiskyDescription();
|
||||
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::RISKY,
|
||||
'This rule is RISKY',
|
||||
// @TODO - FRS enable me
|
||||
// 'Using it may lead to changes in your code\'s logic and behaviour. Use it with caution and review changes before incorporating them into your code base.'
|
||||
// \n\n
|
||||
''
|
||||
.(null !== $riskyDescription ? "{$riskyDescription}" : ''),
|
||||
);
|
||||
}
|
||||
|
||||
if ($fixer instanceof ConfigurableFixerInterface) {
|
||||
$options = array_map(
|
||||
static fn (FixerOptionInterface $option): string => '`'.$option->getName().'`',
|
||||
$fixer->getConfigurationDefinition()->getOptions(),
|
||||
);
|
||||
$tags[] = new DocumentationTag(
|
||||
DocumentationTagType::CONFIGURABLE,
|
||||
'This rule is CONFIGURABLE',
|
||||
\sprintf(
|
||||
'You can configure this rule using the following option%s: %s.',
|
||||
1 === \count($options) ? '' : 's',
|
||||
implode(', ', $options),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// not possible for set to be DocumentationTagType::AUTOMATIC
|
||||
|
||||
return $tags;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user