13 Commits
0.4.0 ... 0.5.0

Author SHA1 Message Date
ErickSkrauch
84dd1184b8 Prepare 0.5.0 release 2023-04-08 21:37:10 +02:00
ErickSkrauch
7d36ce912c Fixes #13. Implemented Ely\multiline_if_statement_braces fixer 2023-04-08 21:07:39 +02:00
ErickSkrauch
e9ad4a94ab Fix Unreleased changelog link 2023-03-28 22:15:20 +02:00
ErickSkrauch
6d572ae421 Add fixer to place promoted properties on a new line when there is more than 1 parameter 2023-03-24 01:13:24 +01:00
ErickSkrauch
050dfe6399 Revisit BlackLineAroundClassBodyFixer 2023-03-24 00:40:48 +01:00
ErickSkrauch
b85d0d5c01 Cover properties promotion cases for Ely\align_multiple_parameters fixer 2023-03-23 17:09:01 +01:00
ErickSkrauch
5ae374f6cd Fix build shield 2023-03-23 03:44:14 +01:00
ErickSkrauch
6956e0271e Fixes #12. Implemented Ely\align_multiline_parameters 2023-03-23 03:36:47 +01:00
ErickSkrauch
4a4f556d7b Update LICENSE year 2023-03-22 20:41:26 +01:00
ErickSkrauch
6e9d815a1f Fixes #9. Add space before next meaningful line of code and skip comments 2023-03-22 20:41:04 +01:00
ErickSkrauch
22dcb418fb Update CHANGELOG 2023-03-22 20:39:19 +01:00
ErickSkrauch
2d215dc761 Fixes #10. Don't count string interpolation as a scope beginning 2023-03-22 18:42:39 +01:00
ErickSkrauch
6f14beec28 Fix PHPUnit configuration 2023-03-22 14:03:19 +01:00
17 changed files with 946 additions and 47 deletions

View File

@@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased] ## [Unreleased]
## [0.5.0] - 2023-04-08
### Added
- Enh #12: Implemented `Ely\align_multiline_parameters` fixer.
- Enh #13: Implemented `Ely\multiline_if_statement_braces` fixer.
- Enabled `Ely\align_multiline_parameters` for Ely.by codestyle in `['types' => false, 'defaults' => false]` mode.
- Enabled `Ely\multiline_if_statement_braces` for Ely.by codestyle in `['keep_on_own_line' => true]` mode.
- Enabled
[`PhpCsFixerCustomFixers/multiline_promoted_properties`](https://github.com/kubawerlos/php-cs-fixer-custom-fixers#multilinepromotedpropertiesfixer)
fixer for Ely.by codestyle in 2+ parameters mode.
### Fixed
- Bug #10: `Ely/blank_line_before_return` don't treat interpolation curly bracket as beginning of the scope.
- Bug #9: `Ely/line_break_after_statements` add space before next meaningful line of code and skip comments.
## [0.4.0] - 2022-12-06 ## [0.4.0] - 2022-12-06
### Added ### Added
- `simple_to_complex_string_variable` fixer. - `simple_to_complex_string_variable` fixer.
@@ -135,7 +149,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added ### Added
- First release - First release
[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/0.4.0...HEAD [Unreleased]: https://github.com/elyby/php-code-style/compare/0.5.0...HEAD
[0.5.0]: https://github.com/elyby/php-code-style/compare/0.4.0...0.5.0
[0.4.0]: https://github.com/elyby/php-code-style/compare/0.3.0...0.4.0 [0.4.0]: https://github.com/elyby/php-code-style/compare/0.3.0...0.4.0
[0.3.0]: https://github.com/elyby/php-code-style/compare/0.2.1...0.3.0 [0.3.0]: https://github.com/elyby/php-code-style/compare/0.2.1...0.3.0
[0.2.1]: https://github.com/elyby/php-code-style/compare/0.2.0...0.2.1 [0.2.1]: https://github.com/elyby/php-code-style/compare/0.2.0...0.2.1

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2018 Ely.by (http://ely.by) Copyright 2023 Ely.by (http://ely.by)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -43,12 +43,14 @@ vendor/bin/php-cs-fixer fix
### Configuration ### Configuration
You can pass a custom set of rules to the `\Ely\CS\Config::create()` call. For example, it can be used to validate a You can pass a custom set of rules to the `\Ely\CS\Config::create()` call. For example, it can be used to validate a
project with PHP 7.0 compatibility: project with PHP 7.4 compatibility:
```php ```php
<?php <?php
return \Ely\CS\Config::create([ return \Ely\CS\Config::create([
'visibility_required' => ['property', 'method'], 'trailing_comma_in_multiline' => [
'elements' => ['arrays', 'arguments'],
],
])->setFinder($finder); ])->setFinder($finder);
``` ```
@@ -76,12 +78,15 @@ class Foo extends Bar implements FooInterface {
private const SAMPLE_1 = 123; private const SAMPLE_1 = 123;
private const SAMPLE_2 = 321; private const SAMPLE_2 = 321;
public $field1; public Typed $field1;
public $field2; public $field2;
public function sampleFunction(int $a, int $b = null): array { public function sampleFunction(
if ($a === $b) { int $a,
private readonly int $b = null,
): array {
if ($a === $this->b) {
$result = bar(); $result = bar();
} else { } else {
$result = BazClass::bar($this->field1, $this->field2); $result = BazClass::bar($this->field1, $this->field2);
@@ -89,7 +94,7 @@ class Foo extends Bar implements FooInterface {
return $result; return $result;
} }
public function setToNull(): self { public function setToNull(): self {
$this->field1 = null; $this->field1 = null;
return $this; return $this;
@@ -154,6 +159,16 @@ class Foo extends Bar implements FooInterface {
echo 'the next statement is here'; echo 'the next statement is here';
``` ```
* There MUST be no alignment around multiline function parameters.
```php
<?php
function foo(
string $input,
int $key = 0,
): void {}
```
## Using our fixers ## Using our fixers
First of all, you must install Ely.by PHP-CS-Fixer package as described in the [installation chapter](#installation). First of all, you must install Ely.by PHP-CS-Fixer package as described in the [installation chapter](#installation).
@@ -169,6 +184,31 @@ return \PhpCsFixer\Config::create()
And then you'll be able to use our custom rules. And then you'll be able to use our custom rules.
### `Ely/align_multiline_parameters`
Forces aligned or not aligned multiline function parameters:
```diff
--- Original
+++ New
@@ @@
function foo(
string $string,
- int $index = 0,
- $arg = 'no type',
+ int $index = 0,
+ $arg = 'no type',
): void {}
```
**Configuration:**
* `variables` - when set to `true`, forces variables alignment. On `false` forces strictly no alignment.
You can set it to `null` to disable touching of variables. **Default**: `true`.
* `defaults` - when set to `true`, forces defaults alignment. On `false` forces strictly no alignment.
You can set it to `null` to disable touching of defaults. **Default**: `false`.
### `Ely/blank_line_around_class_body` ### `Ely/blank_line_around_class_body`
Ensure that a class body contains one blank line after its definition and before its end: Ensure that a class body contains one blank line after its definition and before its end:
@@ -246,6 +286,28 @@ and `do-while`.
$c = 'next statement'; $c = 'next statement';
``` ```
### `Ely/multiline_if_statement_braces`
Ensures that multiline if statement body curly brace placed on the right line.
```diff
--- Original
+++ New
@@ @@
<?php
if ($condition1 === 123
- && $condition2 = 321) {
+ && $condition2 = 321
+) {
// Do something here
}
```
**Configuration:**
* `keep_on_own_line` - should this place closing bracket on its own line? If it's set to `false`, than
curly bracket will be placed right after the last condition statement. **Default**: `true`.
### `Ely/remove_class_name_method_usages` (Yii2) ### `Ely/remove_class_name_method_usages` (Yii2)
Replaces Yii2 [`BaseObject::className()`](https://github.com/yiisoft/yii2/blob/e53fc0ded1/framework/base/BaseObject.php#L84) Replaces Yii2 [`BaseObject::className()`](https://github.com/yiisoft/yii2/blob/e53fc0ded1/framework/base/BaseObject.php#L84)
@@ -265,7 +327,7 @@ usages with native `::class` keyword, introduced in PHP 5.5.
[ico-version]: https://img.shields.io/packagist/v/ely/php-code-style.svg?style=flat-square [ico-version]: https://img.shields.io/packagist/v/ely/php-code-style.svg?style=flat-square
[ico-license]: https://img.shields.io/badge/license-Apache-green.svg?style=flat-square [ico-license]: https://img.shields.io/badge/license-Apache-green.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/ely/php-code-style.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/ely/php-code-style.svg?style=flat-square
[ico-build-status]: https://img.shields.io/github/workflow/status/elyby/php-code-style/CI.svg?style=flat-square [ico-build-status]: https://img.shields.io/github/actions/workflow/status/elyby/php-code-style/ci.yml?branch=master&style=flat-square
[link-packagist]: https://packagist.org/packages/ely/php-code-style [link-packagist]: https://packagist.org/packages/ely/php-code-style
[link-contributors]: ../../contributors [link-contributors]: ../../contributors

View File

@@ -20,7 +20,9 @@
"homepage": "https://github.com/elyby/php-code-style", "homepage": "https://github.com/elyby/php-code-style",
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"friendsofphp/php-cs-fixer": "^3.13" "friendsofphp/php-cs-fixer": "^3.13",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.13",
"symfony/polyfill-php80": "^1.15"
}, },
"require-dev": { "require-dev": {
"ergebnis/composer-normalize": "^2.28", "ergebnis/composer-normalize": "^2.28",

62
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "984ffa466fb6bc9281a36b8701147fee", "content-hash": "8f031e395244dc0c0de0135b5c34ceda",
"packages": [ "packages": [
{ {
"name": "composer/pcre", "name": "composer/pcre",
@@ -462,6 +462,52 @@
], ],
"time": "2022-10-31T19:28:50+00:00" "time": "2022-10-31T19:28:50+00:00"
}, },
{
"name": "kubawerlos/php-cs-fixer-custom-fixers",
"version": "v3.13.0",
"source": {
"type": "git",
"url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git",
"reference": "fb53d8bbe2224383a84c71f451d76eb7bc6c8e33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/fb53d8bbe2224383a84c71f451d76eb7bc6c8e33",
"reference": "fb53d8bbe2224383a84c71f451d76eb7bc6c8e33",
"shasum": ""
},
"require": {
"ext-filter": "*",
"ext-tokenizer": "*",
"friendsofphp/php-cs-fixer": "^3.6.0",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.20"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpCsFixerCustomFixers\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kuba Werłos",
"email": "werlos@gmail.com"
}
],
"description": "A set of custom fixers for PHP CS Fixer",
"support": {
"issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues",
"source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.13.0"
},
"time": "2023-02-15T18:51:16+00:00"
},
{ {
"name": "psr/cache", "name": "psr/cache",
"version": "1.0.1", "version": "1.0.1",
@@ -1662,16 +1708,16 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.26.0", "version": "v1.27.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1680,7 +1726,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.26-dev" "dev-main": "1.27-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -1725,7 +1771,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
}, },
"funding": [ "funding": [
{ {
@@ -1741,7 +1787,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-05-10T07:21:04+00:00" "time": "2022-11-03T14:55:06+00:00"
}, },
{ {
"name": "symfony/polyfill-php81", "name": "symfony/polyfill-php81",

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit <phpunit
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
beStrictAboutChangesToGlobalState="true" beStrictAboutChangesToGlobalState="true"
beStrictAboutOutputDuringTests="true" beStrictAboutOutputDuringTests="true"

View File

@@ -3,14 +3,18 @@ declare(strict_types=1);
namespace Ely\CS; namespace Ely\CS;
use Ely\CS\Fixers as ElyFixers;
use PhpCsFixer\Config as PhpCsFixerConfig; use PhpCsFixer\Config as PhpCsFixerConfig;
use PhpCsFixer\ConfigInterface as PhpCsFixerConfigInterface;
use PhpCsFixerCustomFixers\Fixers as KubawerlosFixers;
class Config { class Config {
public static function create(array $overwrittenRules = []): PhpCsFixerConfig { public static function create(array $overwrittenRules = []): PhpCsFixerConfigInterface {
return (new PhpCsFixerConfig()) return (new PhpCsFixerConfig())
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->registerCustomFixers(new Fixers()) ->registerCustomFixers(new ElyFixers())
->registerCustomFixers(new KubawerlosFixers())
->setRules(Rules::create($overwrittenRules)); ->setRules(Rules::create($overwrittenRules));
} }

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Ely\CS\Fixer\FunctionNotation;
use Ely\CS\Fixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use SplFileInfo;
final class AlignMultilineParametersFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface {
/**
* @internal
*/
public const C_VARIABLES = 'variables';
/**
* @internal
*/
public const C_DEFAULTS = 'defaults';
private array $parameterModifiers;
public function __construct() {
parent::__construct();
$this->parameterModifiers = [
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
];
if (defined('T_READONLY')) {
$this->parameterModifiers[] = T_READONLY;
}
}
public function getDefinition(): FixerDefinitionInterface {
return new FixerDefinition(
'Aligns parameters in multiline function declaration.',
[
new CodeSample(
'<?php
function test(
string $a,
int $b = 0
): void {};
',
),
new CodeSample(
'<?php
function test(
string $string,
int $int = 0
): void {};
',
[self::C_VARIABLES => false, self::C_DEFAULTS => false],
),
],
);
}
public function isCandidate(Tokens $tokens): bool {
return $tokens->isAnyTokenKindsFound([T_FUNCTION, T_FN]);
}
/**
* Must run after StatementIndentationFixer, MethodArgumentSpaceFixer
*/
public function getPriority(): int {
return -10;
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface {
return new FixerConfigurationResolver([
(new FixerOptionBuilder(self::C_VARIABLES, 'on null no value alignment, on bool forces alignment'))
->setAllowedTypes(['bool', 'null'])
->setDefault(true)
->getOption(),
(new FixerOptionBuilder(self::C_DEFAULTS, 'on null no value alignment, on bool forces alignment'))
->setAllowedTypes(['bool', 'null'])
->setDefault(null)
->getOption(),
]);
}
protected function applyFix(SplFileInfo $file, Tokens $tokens): void {
// There is nothing to do
if ($this->configuration[self::C_VARIABLES] === null && $this->configuration[self::C_DEFAULTS] === null) {
return;
}
$tokensAnalyzer = new TokensAnalyzer($tokens);
$functionsAnalyzer = new FunctionsAnalyzer();
/** @var \PhpCsFixer\Tokenizer\Token $functionToken */
foreach ($tokens as $i => $functionToken) {
if (!$functionToken->isGivenKind([T_FUNCTION, T_FN])) {
continue;
}
$openBraceIndex = $tokens->getNextTokenOfKind($i, ['(']);
$isMultiline = $tokensAnalyzer->isBlockMultiline($tokens, $openBraceIndex);
if (!$isMultiline) {
continue;
}
/** @var \PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis[] $arguments */
$arguments = $functionsAnalyzer->getFunctionArguments($tokens, $i);
if (empty($arguments)) {
continue;
}
$longestType = 0;
$longestVariableName = 0;
$hasAtLeastOneTypedArgument = false;
foreach ($arguments as $argument) {
$typeAnalysis = $argument->getTypeAnalysis();
if ($typeAnalysis) {
$hasAtLeastOneTypedArgument = true;
$typeLength = $this->getFullTypeLength($tokens, $typeAnalysis->getStartIndex());
if ($typeLength > $longestType) {
$longestType = $typeLength;
}
}
$variableNameLength = strlen($argument->getName());
if ($variableNameLength > $longestVariableName) {
$longestVariableName = $variableNameLength;
}
}
$argsIndent = WhitespacesAnalyzer::detectIndent($tokens, $i) . $this->whitespacesConfig->getIndent();
foreach ($arguments as $argument) {
if ($this->configuration[self::C_VARIABLES] !== null) {
$whitespaceIndex = $argument->getNameIndex() - 1;
if ($this->configuration[self::C_VARIABLES] === true) {
$typeLen = 0;
if ($argument->getTypeAnalysis() !== null) {
$typeLen = $this->getFullTypeLength($tokens, $argument->getTypeAnalysis()->getStartIndex());
}
$appendix = str_repeat(' ', $longestType - $typeLen + (int)$hasAtLeastOneTypedArgument);
if ($argument->hasTypeAnalysis()) {
$whitespaceToken = $appendix;
} else {
$whitespaceToken = $this->whitespacesConfig->getLineEnding() . $argsIndent . $appendix;
}
} else {
if ($argument->hasTypeAnalysis()) {
$whitespaceToken = ' ';
} else {
$whitespaceToken = $this->whitespacesConfig->getLineEnding() . $argsIndent;
}
}
$tokens->ensureWhitespaceAtIndex($whitespaceIndex, 0, $whitespaceToken);
}
if ($this->configuration[self::C_DEFAULTS] !== null) {
// Can't use $argument->hasDefault() because it's null when it's default for a type (e.g. 0 for int)
/** @var \PhpCsFixer\Tokenizer\Token $equalToken */
$equalToken = $tokens[$tokens->getNextMeaningfulToken($argument->getNameIndex())];
if ($equalToken->getContent() === '=') {
$nameLen = strlen($argument->getName());
$whitespaceIndex = $argument->getNameIndex() + 1;
if ($this->configuration[self::C_DEFAULTS] === true) {
$tokens->ensureWhitespaceAtIndex($whitespaceIndex, 0, str_repeat(' ', $longestVariableName - $nameLen + 1));
} else {
$tokens->ensureWhitespaceAtIndex($whitespaceIndex, 0, ' ');
}
}
}
}
}
}
private function getFullTypeLength(Tokens $tokens, int $typeIndex): int {
/** @var \PhpCsFixer\Tokenizer\Token $typeToken */
$typeToken = $tokens[$typeIndex];
$typeLength = strlen($typeToken->getContent());
/** @var \PhpCsFixer\Tokenizer\Token $possiblyReadonlyToken */
$possiblyReadonlyToken = $tokens[$typeIndex - 2];
if ($possiblyReadonlyToken->isGivenKind($this->parameterModifiers)) {
/** @var \PhpCsFixer\Tokenizer\Token $whitespaceToken */
$whitespaceToken = $tokens[$typeIndex - 1];
$typeLength += strlen($possiblyReadonlyToken->getContent() . $whitespaceToken->getContent());
}
/** @var \PhpCsFixer\Tokenizer\Token $possiblyPromotionToken */
$possiblyPromotionToken = $tokens[$typeIndex - 4];
if ($possiblyPromotionToken->isGivenKind($this->parameterModifiers)) {
/** @var \PhpCsFixer\Tokenizer\Token $whitespaceToken */
$whitespaceToken = $tokens[$typeIndex - 3];
$typeLength += strlen($possiblyPromotionToken->getContent() . $whitespaceToken->getContent());
}
return $typeLength;
}
}

View File

@@ -16,14 +16,26 @@ use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer; use PhpCsFixer\Tokenizer\TokensAnalyzer;
use SplFileInfo;
/** /**
* This is copy of the PR https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/3688 * This fixer conflicts with the CurlyBracesPositionFixer (which is part of the BracesFixer),
* because CurlyBracesPositionFixer always tries to remove any new lines between class beginning
* and the first meaningful statement. And then this fixer restores those spaces back.
* *
* @author ErickSkrauch <erickskrauch@ely.by> * That is the reason, why you always see a "braces, Ely/blank_line_around_class_body" in verbose output.
*/ */
final class BlankLineAroundClassBodyFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface { final class BlankLineAroundClassBodyFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface {
/**
* @internal
*/
public const C_BLANK_LINES_COUNT = 'blank_lines_count';
/**
* @internal
*/
public const C_APPLY_TO_ANONYMOUS_CLASSES = 'apply_to_anonymous_classes';
public function getDefinition(): FixerDefinitionInterface { public function getDefinition(): FixerDefinitionInterface {
return new FixerDefinition( return new FixerDefinition(
'Ensure that class body contains one blank line after class definition and before its end.', 'Ensure that class body contains one blank line after class definition and before its end.',
@@ -48,7 +60,7 @@ new class extends Foo {
}; };
', ',
['apply_to_anonymous_classes' => false], [self::C_APPLY_TO_ANONYMOUS_CLASSES => false],
), ),
new CodeSample( new CodeSample(
'<?php '<?php
@@ -58,74 +70,75 @@ new class extends Foo {
} }
}; };
', ',
['apply_to_anonymous_classes' => true], [self::C_APPLY_TO_ANONYMOUS_CLASSES => true],
), ),
], ],
); );
} }
public function getPriority(): int {
// should be run after the BracesFixer
return -26;
}
public function isCandidate(Tokens $tokens): bool { public function isCandidate(Tokens $tokens): bool {
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()); return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
} }
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { protected function applyFix(SplFileInfo $file, Tokens $tokens): void {
$analyzer = new TokensAnalyzer($tokens); $analyzer = new TokensAnalyzer($tokens);
/** @var Token $token */
foreach ($tokens as $index => $token) { foreach ($tokens as $index => $token) {
if (!$token->isClassy()) { if (!$token->isClassy()) {
continue; continue;
} }
$countLines = $this->configuration['blank_lines_count']; $countLines = $this->configuration[self::C_BLANK_LINES_COUNT];
if (!$this->configuration['apply_to_anonymous_classes'] && $analyzer->isAnonymousClass($index)) { if (!$this->configuration[self::C_APPLY_TO_ANONYMOUS_CLASSES] && $analyzer->isAnonymousClass($index)) {
$countLines = 0; $countLines = 0;
} }
$startBraceIndex = $tokens->getNextTokenOfKind($index, ['{']); $startBraceIndex = $tokens->getNextTokenOfKind($index, ['{']);
if ($tokens[$startBraceIndex + 1]->isWhitespace()) { /** @var Token $nextAfterBraceToken */
$nextAfterBraceToken = $tokens[$startBraceIndex + 1];
if ($nextAfterBraceToken->isWhitespace()) {
$nextStatementIndex = $tokens->getNextMeaningfulToken($startBraceIndex); $nextStatementIndex = $tokens->getNextMeaningfulToken($startBraceIndex);
// Traits should be placed right after a class opening brace, /** @var Token $nextStatementToken */
if ($tokens[$nextStatementIndex]->getContent() !== 'use') { $nextStatementToken = $tokens[$nextStatementIndex];
$this->fixBlankLines($tokens, $startBraceIndex + 1, $countLines); // Traits should be placed right after a class opening brace
if ($nextStatementToken->getContent() !== 'use') {
$this->ensureBlankLines($tokens, $startBraceIndex + 1, $countLines);
} }
} }
$endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startBraceIndex); $endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startBraceIndex);
if ($tokens[$endBraceIndex - 1]->isWhitespace()) { if ($tokens[$endBraceIndex - 1]->isWhitespace()) {
$this->fixBlankLines($tokens, $endBraceIndex - 1, $countLines); $this->ensureBlankLines($tokens, $endBraceIndex - 1, $countLines);
} }
} }
} }
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { protected function createConfigurationDefinition(): FixerConfigurationResolverInterface {
return new FixerConfigurationResolver([ return new FixerConfigurationResolver([
(new FixerOptionBuilder('blank_lines_count', 'adjusts an amount of the blank lines.')) (new FixerOptionBuilder(self::C_BLANK_LINES_COUNT, 'adjusts the number of blank lines.'))
->setAllowedTypes(['int']) ->setAllowedTypes(['int'])
->setDefault(1) ->setDefault(1)
->getOption(), ->getOption(),
(new FixerOptionBuilder('apply_to_anonymous_classes', 'whether this fixer should be applied to anonymous classes.')) (new FixerOptionBuilder(self::C_APPLY_TO_ANONYMOUS_CLASSES, 'whether this fixer should be applied to anonymous classes.'))
->setAllowedTypes(['bool']) ->setAllowedTypes(['bool'])
->setDefault(true) ->setDefault(true)
->getOption(), ->getOption(),
]); ]);
} }
private function fixBlankLines(Tokens $tokens, int $index, int $countLines): void { private function ensureBlankLines(Tokens $tokens, int $index, int $countLines): void {
$content = $tokens[$index]->getContent(); $content = $tokens[$index]->getContent();
// Apply fix only in the case when the count lines do not equals to expected // Apply fix only when the lines count doesn't equal to expected
// Don't check for \r\n sequence since it's still contains \n part
if (substr_count($content, "\n") === $countLines + 1) { if (substr_count($content, "\n") === $countLines + 1) {
return; return;
} }
// The final bit of the whitespace must be the next statement's indentation // Use regexp to extract contents between line breaks
Preg::matchAll('/[^\n\r]+[\r\n]*/', $content, $matches); Preg::matchAll('/[^\n\r]+[\r\n]*/', $content, $matches);
$lines = $matches[0]; $lines = $matches[0];
$eol = $this->whitespacesConfig->getLineEnding(); $eol = $this->whitespacesConfig->getLineEnding();
$tokens[$index] = new Token([T_WHITESPACE, str_repeat($eol, $countLines + 1) . end($lines)]); $tokens->ensureWhitespaceAtIndex($index, 0, str_repeat($eol, $countLines + 1) . end($lines));
} }
} }

View File

@@ -67,8 +67,12 @@ final class BlankLineBeforeReturnFixer extends AbstractFixer implements Whitespa
break; break;
} }
/** @var Token $backwardToken */
$backwardToken = $tokens[$backwardIndex]; $backwardToken = $tokens[$backwardIndex];
if ($backwardToken->getContent() === '{') { /** @var Token $nextToken */
$nextToken = $tokens[$backwardIndex + 1];
// Exclude string interpolation: "str {$var}"
if ($backwardToken->getContent() === '{' && !$nextToken->isGivenKind(T_VARIABLE)) {
break; break;
} }

View File

@@ -26,7 +26,7 @@ final class LineBreakAfterStatementsFixer extends AbstractFixer implements White
/** /**
* There is no 'do', 'cause the processing of the 'while' also includes do {} while (); construction * There is no 'do', 'cause the processing of the 'while' also includes do {} while (); construction
*/ */
public const STATEMENTS = [ private const STATEMENTS = [
T_IF, T_IF,
T_SWITCH, T_SWITCH,
T_FOR, T_FOR,
@@ -93,7 +93,7 @@ class Foo
} }
$endStatementIndex = $this->findStatementEnd($tokens, $index); $endStatementIndex = $this->findStatementEnd($tokens, $index);
$nextStatementIndex = $tokens->getNextNonWhitespace($endStatementIndex); $nextStatementIndex = $tokens->getNextMeaningfulToken($endStatementIndex);
if ($nextStatementIndex === null) { if ($nextStatementIndex === null) {
continue; continue;
} }

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Ely\CS\Fixer\Whitespace;
use Ely\CS\Fixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use SplFileInfo;
final class MultilineIfStatementBracesFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface {
/**
* @internal
*/
public const C_KEEP_ON_OWN_LINE = 'keep_on_own_line';
public function getDefinition(): FixerDefinitionInterface {
return new FixerDefinition(
'Ensures that multiline if statement body curly brace placed on the right line.',
[
new CodeSample(
'<?php
if ($condition1 == true
&& $condition2 === false) {}
',
),
new CodeSample(
'<?php
if ($condition1 == true
&& $condition2 === false
) {}
',
[self::C_KEEP_ON_OWN_LINE => false],
),
],
);
}
public function isCandidate(Tokens $tokens): bool {
return $tokens->isTokenKindFound(T_IF);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface {
return new FixerConfigurationResolver([
(new FixerOptionBuilder(self::C_KEEP_ON_OWN_LINE, 'adjusts the position of condition closing brace.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
]);
}
protected function applyFix(SplFileInfo $file, Tokens $tokens): void {
$keepOnOwnLine = $this->configuration[self::C_KEEP_ON_OWN_LINE];
$tokensAnalyzer = new TokensAnalyzer($tokens);
$eol = $this->whitespacesConfig->getLineEnding();
foreach ($tokens as $i => $token) {
if (!$token->isGivenKind(T_IF)) {
continue;
}
$openBraceIndex = $tokens->getNextTokenOfKind($i, ['(']);
if (!$tokensAnalyzer->isBlockMultiline($tokens, $openBraceIndex)) {
continue;
}
$closingBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openBraceIndex);
/** @var \PhpCsFixer\Tokenizer\Token $statementBeforeClosingBrace */
$statementBeforeClosingBrace = $tokens[$closingBraceIndex - 1];
if ($keepOnOwnLine) {
if (!$statementBeforeClosingBrace->isWhitespace()
|| !str_contains($statementBeforeClosingBrace->getContent(), $eol)
) {
$indent = WhitespacesAnalyzer::detectIndent($tokens, $i);
$tokens->ensureWhitespaceAtIndex($closingBraceIndex, 0, $eol . $indent);
}
} else {
$tokens->removeLeadingWhitespace($closingBraceIndex);
}
}
}
}

View File

@@ -213,12 +213,22 @@ class Rules {
'space_multiple_catch' => 'none', 'space_multiple_catch' => 'none',
], ],
// kubawerlos fixers
'PhpCsFixerCustomFixers/multiline_promoted_properties' => [
'minimum_number_of_parameters' => 2,
],
// Our custom or extended fixers // Our custom or extended fixers
'Ely/align_multiline_parameters' => [
'variables' => false,
'defaults' => false,
],
'Ely/blank_line_around_class_body' => [ 'Ely/blank_line_around_class_body' => [
'apply_to_anonymous_classes' => false, 'apply_to_anonymous_classes' => false,
], ],
'Ely/blank_line_before_return' => true, 'Ely/blank_line_before_return' => true,
'Ely/line_break_after_statements' => true, 'Ely/line_break_after_statements' => true,
'Ely/multiline_if_statement_braces' => true,
'Ely/remove_class_name_method_usages' => true, 'Ely/remove_class_name_method_usages' => true,
], $overwrittenRules); ], $overwrittenRules);
} }

View File

@@ -0,0 +1,351 @@
<?php
declare(strict_types=1);
namespace Ely\CS\Test\Fixer\FunctionNotation;
use Ely\CS\Fixer\FunctionNotation\AlignMultilineParametersFixer;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
/**
* @covers \Ely\CS\Fixer\FunctionNotation\AlignMultilineParametersFixer
*/
final class AlignMultilineParametersFixerTest extends AbstractFixerTestCase {
/**
* @dataProvider provideTrueCases
*/
public function testBothTrue(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => true,
AlignMultilineParametersFixer::C_DEFAULTS => true,
]);
$this->doTest($expected, $input);
}
public function provideTrueCases(): iterable {
yield 'empty function' => [
'<?php
function test(): void {}
',
];
yield 'empty multiline function' => [
'<?php
function test(
): void {}
',
];
yield 'single line function' => [
'<?php
function test(string $a, int $b): void {}
',
];
yield 'single line fn' => [
'<?php
fn(string $a, int $b) => $b;
',
];
yield 'function, no defaults' => [
'<?php
function test(
string $a,
int $b
): void {}
',
'<?php
function test(
string $a,
int $b
): void {}
',
];
yield 'function, one has default' => [
'<?php
function test(
string $a,
int $b = 0
): void {}
',
'<?php
function test(
string $a,
int $b = 0
): void {}
',
];
yield 'function, one has no type' => [
'<?php
function test(
string $a,
$b
): void {}
',
'<?php
function test(
string $a,
$b
): void {}
',
];
yield 'function, one has no type, but has default' => [
'<?php
function test(
string $a,
$b = 0
): void {}
',
'<?php
function test(
string $a,
$b = 0
): void {}
',
];
yield 'function, no types at all' => [
'<?php
function test(
$string = "string",
$int = 0
): void {}
',
'<?php
function test(
$string = "string",
$int = 0
): void {}
',
];
yield 'function, defaults' => [
'<?php
function test(
string $string = "string",
int $int = 0
): void {}
',
'<?php
function test(
string $string = "string",
int $int = 0
): void {}
',
];
yield 'class method, defaults' => [
'<?php
class Test {
public function foo(
string $string = "string",
int $int = 0
): void {}
}
',
'<?php
class Test {
public function foo(
string $string = "string",
int $int = 0
): void {}
}
',
];
yield 'fn, defaults' => [
'<?php
fn(
string $string = "string",
int $int = 0
) => $int;
',
'<?php
fn(
string $string = "string",
int $int = 0
) => $int;
',
];
}
/**
* @dataProvider provideFalseCases
*/
public function testBothFalse(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => false,
AlignMultilineParametersFixer::C_DEFAULTS => false,
]);
$this->doTest($expected, $input);
}
public function provideFalseCases(): iterable {
foreach ($this->provideTrueCases() as $key => $case) {
if (isset($case[1])) {
yield $key => [$case[1], $case[0]];
} else {
yield $key => $case;
}
}
}
/**
* @dataProvider provideNullCases
*/
public function testBothNull(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => null,
AlignMultilineParametersFixer::C_DEFAULTS => null,
]);
$this->doTest($expected, $input);
}
public function provideNullCases(): iterable {
foreach ($this->provideFalseCases() as $key => $case) {
yield $key => [$case[0]];
}
}
/**
* @dataProvider provide80TrueCases
* @requires PHP 8.0
*/
public function test80BothTrue(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => true,
AlignMultilineParametersFixer::C_DEFAULTS => true,
]);
$this->doTest($expected, $input);
}
public function provide80TrueCases(): iterable {
yield 'constructor promotion, defaults' => [
'<?php
class Test {
public function __construct(
public string $string = "string",
protected bool $bool = true
) {}
}
',
'<?php
class Test {
public function __construct(
public string $string = "string",
protected bool $bool = true
) {}
}
',
];
}
/**
* @dataProvider provideFalse80Cases
* @requires PHP 8.0
*/
public function test80BothFalse(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => false,
AlignMultilineParametersFixer::C_DEFAULTS => false,
]);
$this->doTest($expected, $input);
}
public function provideFalse80Cases(): iterable {
foreach ($this->provide80TrueCases() as $key => $case) {
if (isset($case[1])) {
yield $key => [$case[1], $case[0]];
} else {
yield $key => $case;
}
}
}
/**
* @dataProvider provide81TrueCases
* @requires PHP 8.1
*/
public function test81BothTrue(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => true,
AlignMultilineParametersFixer::C_DEFAULTS => true,
]);
$this->doTest($expected, $input);
}
public function provide81TrueCases(): iterable {
yield 'constructor promotion, readonly, defaults' => [
'<?php
class Test {
public function __construct(
public readonly string $string = "string",
protected readonly bool $bool = true
) {}
}
',
'<?php
class Test {
public function __construct(
public readonly string $string = "string",
protected readonly bool $bool = true
) {}
}
',
];
yield 'partial constructor promotion, readonly, defaults' => [
'<?php
class Test {
public function __construct(
readonly string $string = "string",
int $int = 0,
protected bool $bool = true,
$float = 0.0,
) {}
}
',
'<?php
class Test {
public function __construct(
readonly string $string = "string",
int $int = 0,
protected bool $bool = true,
$float = 0.0,
) {}
}
',
];
}
/**
* @dataProvider provideFalse81Cases
* @requires PHP 8.1
*/
public function test81BothFalse(string $expected, ?string $input = null): void {
$this->fixer->configure([
AlignMultilineParametersFixer::C_VARIABLES => false,
AlignMultilineParametersFixer::C_DEFAULTS => false,
]);
$this->doTest($expected, $input);
}
public function provideFalse81Cases(): iterable {
foreach ($this->provide81TrueCases() as $key => $case) {
if (isset($case[1])) {
yield $key => [$case[1], $case[0]];
} else {
yield $key => $case;
}
}
}
protected function createFixer(): AbstractFixer {
return new AlignMultilineParametersFixer();
}
}

View File

@@ -180,6 +180,20 @@ return $c;',
} }
', ',
]; ];
yield [
'<?php
if ($condition) {
$a = "Interpolation {$var}.";
return true;
}',
];
yield [
'<?php
if ($condition) {
$a = "Deprecated interpolation ${var}.";
return true;
}',
];
} }
/** /**

View File

@@ -575,6 +575,14 @@ class Foo
} }
}', }',
]; ];
yield [
'<?php
do {
$a = 123;
} while ($value > 10); // comment here
',
];
} }
protected function createFixer(): AbstractFixer { protected function createFixer(): AbstractFixer {

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Ely\CS\Test\Fixer\Whitespace;
use Ely\CS\Fixer\Whitespace\MultilineIfStatementBracesFixer;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
/**
* @covers \Ely\CS\Fixer\Whitespace\MultilineIfStatementBracesFixer
*/
class MultilineIfStatementBracesFixerTest extends AbstractFixerTestCase {
/**
* @dataProvider provideFixCases
*/
public function testFixOnNewLine(string $expected, ?string $input = null): void {
$this->doTest($expected, $input);
}
public function provideFixCases(): iterable {
yield 'simple' => [
'<?php
if ($condition1
&& $condition2
) {}',
'<?php
if ($condition1
&& $condition2) {}',
];
yield 'nested' => [
'<?php
function foo() {
if ($condition1
&& $condition2
) {}
}',
'<?php
function foo() {
if ($condition1
&& $condition2) {}
}',
];
}
/**
* @dataProvider provideInvertedFixCases
*/
public function testFixOnSameLine(string $expected, ?string $input = null): void {
$this->fixer->configure([
MultilineIfStatementBracesFixer::C_KEEP_ON_OWN_LINE => false,
]);
$this->doTest($expected, $input);
}
public function provideInvertedFixCases(): iterable {
foreach ($this->provideFixCases() as $name => $case) {
yield $name => [$case[1], $case[0]];
}
}
protected function createFixer(): AbstractFixer {
return new MultilineIfStatementBracesFixer();
}
}