diff --git a/CHANGELOG.md b/CHANGELOG.md index e24325b..d492a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Enh #12: Implemented `Ely\align_multiline_parameters` fixer. +- Enabled `Ely\align_multiline_parameters` for Ely.by codestyle in `['types' => false, 'defaults' => false]` 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. diff --git a/README.md b/README.md index a18de64..a944b34 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,14 @@ vendor/bin/php-cs-fixer fix ### 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 -project with PHP 7.0 compatibility: +project with PHP 7.4 compatibility: ```php ['property', 'method'], + 'trailing_comma_in_multiline' => [ + 'elements' => ['arrays', 'arguments'], + ], ])->setFinder($finder); ``` @@ -76,12 +78,15 @@ class Foo extends Bar implements FooInterface { private const SAMPLE_1 = 123; private const SAMPLE_2 = 321; - public $field1; + public Typed $field1; public $field2; - public function sampleFunction(int $a, int $b = null): array { - if ($a === $b) { + public function sampleFunction( + int $a, + private readonly int $b = null, + ): array { + if ($a === $this->b) { $result = bar(); } else { $result = BazClass::bar($this->field1, $this->field2); @@ -89,7 +94,7 @@ class Foo extends Bar implements FooInterface { return $result; } - + public function setToNull(): self { $this->field1 = null; return $this; @@ -154,6 +159,16 @@ class Foo extends Bar implements FooInterface { echo 'the next statement is here'; ``` +* There MUST be no alignment around multiline function parameters. + + ```php + 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 = strlen($typeAnalysis->getName()); + 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 = strlen($argument->getTypeAnalysis()->getName()); + } + + $appendix = str_repeat(' ', $longestType - $typeLen + (int)$hasAtLeastOneTypedArgument); + if ($argument->hasTypeAnalysis()) { + $whitespace = $appendix; + } else { + $whitespace = $this->whitespacesConfig->getLineEnding() . $argsIndent . $appendix; + } + } else { + if ($argument->hasTypeAnalysis()) { + $whitespace = ' '; + } else { + $whitespace = $this->whitespacesConfig->getLineEnding() . $argsIndent; + } + } + + $tokens->ensureWhitespaceAtIndex($whitespaceIndex, 0, $whitespace); + } + + 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, ' '); + } + } + } + } + } + } + +} diff --git a/src/Rules.php b/src/Rules.php index 6f66f27..b964a44 100644 --- a/src/Rules.php +++ b/src/Rules.php @@ -214,6 +214,10 @@ class Rules { ], // Our custom or extended fixers + 'Ely/align_multiline_parameters' => [ + 'variables' => false, + 'defaults' => false, + ], 'Ely/blank_line_around_class_body' => [ 'apply_to_anonymous_classes' => false, ], diff --git a/tests/Fixer/FunctionNotation/AlignMultilineParametersFixerTest.php b/tests/Fixer/FunctionNotation/AlignMultilineParametersFixerTest.php new file mode 100644 index 0000000..85a65ba --- /dev/null +++ b/tests/Fixer/FunctionNotation/AlignMultilineParametersFixerTest.php @@ -0,0 +1,219 @@ +fixer->configure([ + 'variables' => true, + 'defaults' => true, + ]); + $this->doTest($expected, $input); + } + + public function provideTrueCases(): iterable { + yield 'empty function' => [ + ' [ + ' [ + ' [ + ' $b; + ', + ]; + + yield 'function, no defaults' => [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' $int; + ', + ' $int; + ', + ]; + } + + /** + * @dataProvider provideFalseCases + */ + public function testBothFalse(string $expected, ?string $input = null): void { + $this->fixer->configure([ + 'variables' => false, + '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([ + 'variables' => null, + 'defaults' => null, + ]); + $this->doTest($expected, $input); + } + + public function provideNullCases(): iterable { + foreach ($this->provideFalseCases() as $key => $case) { + yield $key => [$case[0]]; + } + } + + protected function createFixer(): AbstractFixer { + return new AlignMultilineParametersFixer(); + } + +}