diff --git a/phpstan.neon b/phpstan.neon index f5da971..8a310a5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,3 +4,4 @@ parameters: checkMissingCallableSignature: true paths: - src + - tests diff --git a/src/lib/PhpCsFixer/Rule/MultilineParametersFixer.php b/src/lib/PhpCsFixer/Rule/MultilineParametersFixer.php new file mode 100644 index 0000000..cbe75ba --- /dev/null +++ b/src/lib/PhpCsFixer/Rule/MultilineParametersFixer.php @@ -0,0 +1,182 @@ +isTokenKindFound(T_FUNCTION); + } + + protected function applyFix( + \SplFileInfo $file, + Tokens $tokens + ): void { + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + if (!$tokens[$index]->isGivenKind(T_FUNCTION)) { + continue; + } + + $openParentIndex = $tokens->getNextTokenOfKind($index, ['(']); + if ($openParentIndex === null) { + continue; + } + + $closeParentIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParentIndex); + + // Count commas to determine parameter count + $paramCount = $this->countParameters($tokens, $openParentIndex, $closeParentIndex); + + // Only process if 2+ parameters + if ($paramCount < 2) { + continue; + } + + // Check if already multiline + if ($this->isMultiline($tokens, $openParentIndex, $closeParentIndex)) { + continue; + } + + // Apply multiline formatting + $this->makeMultiline($tokens, $openParentIndex, $closeParentIndex); + } + } + + private function countParameters( + Tokens $tokens, + int $start, + int $end + ): int { + $count = 0; + $depth = 0; + + for ($i = $start + 1; $i < $end; ++$i) { + if ($tokens[$i]->equals('(') || $tokens[$i]->equals('[')) { + ++$depth; + } elseif ($tokens[$i]->equals(')') || $tokens[$i]->equals(']')) { + --$depth; + } elseif ($depth === 0 && $tokens[$i]->equals(',')) { + ++$count; + } + } + + // If we found any commas, param count is commas + 1 + // If no commas but there's content, it's 1 param + if ($count > 0) { + return $count + 1; + } + + // Check if there's any non-whitespace content + for ($i = $start + 1; $i < $end; ++$i) { + if (!$tokens[$i]->isWhitespace()) { + return 1; + } + } + + return 0; + } + + private function isMultiline( + Tokens $tokens, + int $start, + int $end + ): bool { + for ($i = $start; $i <= $end; ++$i) { + if ($tokens[$i]->isGivenKind(T_WHITESPACE) && str_contains($tokens[$i]->getContent(), "\n")) { + return true; + } + } + + return false; + } + + private function makeMultiline( + Tokens $tokens, + int $openParentIndex, + int $closeParentIndex + ): void { + $indent = $this->detectIndent($tokens, $openParentIndex); + $lineIndent = str_repeat(' ', 4); // 4 spaces for parameters + + // Add newline after opening parenthesis + $tokens->insertAt($openParentIndex + 1, new Token([T_WHITESPACE, "\n" . $indent . $lineIndent])); + ++$closeParentIndex; + + // Find all commas and add newlines after them + $depth = 0; + for ($i = $openParentIndex + 1; $i < $closeParentIndex; ++$i) { + if ($tokens[$i]->equals('(') || $tokens[$i]->equals('[')) { + ++$depth; + } elseif ($tokens[$i]->equals(')') || $tokens[$i]->equals(']')) { + --$depth; + } elseif ($depth === 0 && $tokens[$i]->equals(',')) { + // Remove any whitespace after comma + $nextIndex = $i + 1; + while ($nextIndex < $closeParentIndex && $tokens[$nextIndex]->isWhitespace()) { + $tokens->clearAt($nextIndex); + ++$nextIndex; + } + + // Insert newline with proper indentation + $tokens->insertAt($i + 1, new Token([T_WHITESPACE, "\n" . $indent . $lineIndent])); + $closeParentIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParentIndex); + } + } + + // Add newline before closing parenthesis + $tokens->insertAt($closeParentIndex, new Token([T_WHITESPACE, "\n" . $indent])); + + // Handle the opening brace + $nextNonWhitespace = $tokens->getNextNonWhitespace($closeParentIndex); + if ($nextNonWhitespace !== null && $tokens[$nextNonWhitespace]->equals('{')) { + $tokens->ensureWhitespaceAtIndex($nextNonWhitespace - 1, 1, ' '); + } + } + + private function detectIndent( + Tokens $tokens, + int $index + ): string { + // Look backwards to find the indentation of the current line + for ($i = $index - 1; $i >= 0; --$i) { + if ($tokens[$i]->isGivenKind(T_WHITESPACE) && str_contains($tokens[$i]->getContent(), "\n")) { + $lines = explode("\n", $tokens[$i]->getContent()); + + return end($lines); + } + } + + return ''; + } + + public function getName(): string + { + return 'Ibexa/multiline_parameters'; + } +} diff --git a/src/lib/PhpCsFixer/Sets/AbstractIbexaRuleSet.php b/src/lib/PhpCsFixer/Sets/AbstractIbexaRuleSet.php index 4ccf3f6..079b8fb 100644 --- a/src/lib/PhpCsFixer/Sets/AbstractIbexaRuleSet.php +++ b/src/lib/PhpCsFixer/Sets/AbstractIbexaRuleSet.php @@ -9,6 +9,7 @@ namespace Ibexa\CodeStyle\PhpCsFixer\Sets; use AdamWojs\PhpCsFixerPhpdocForceFQCN\Fixer\Phpdoc\ForceFQCNFixer; +use Ibexa\CodeStyle\PhpCsFixer\Rule\MultilineParametersFixer; use PhpCsFixer\Config; abstract class AbstractIbexaRuleSet implements RuleSetInterface @@ -26,6 +27,7 @@ public function getRules(): array return [ '@PSR12' => false, 'AdamWojs/phpdoc_force_fqcn_fixer' => true, + 'Ibexa/multiline_parameters' => true, 'array_syntax' => [ 'syntax' => 'short', ], @@ -204,7 +206,7 @@ public function getRules(): array public function buildConfig(): Config { $config = new Config(); - $config->registerCustomFixers([new ForceFQCNFixer()]); + $config->registerCustomFixers([new ForceFQCNFixer(), new MultilineParametersFixer()]); $config->setRules(array_merge( $config->getRules(), diff --git a/tests/lib/PhpCsFixer/InternalConfigFactoryTest.php b/tests/lib/PhpCsFixer/InternalConfigFactoryTest.php index 71ef115..e85f80c 100644 --- a/tests/lib/PhpCsFixer/InternalConfigFactoryTest.php +++ b/tests/lib/PhpCsFixer/InternalConfigFactoryTest.php @@ -37,14 +37,22 @@ protected function setUp(): void /** * @dataProvider provideRuleSetTestCases + * + * @param array{name: string, version: string, pretty_version?: string} $package + * @param class-string $expectedRuleSetClass */ - public function testVersionBasedRuleSetSelection(array $package, string $expectedRuleSetClass): void - { + public function testVersionBasedRuleSetSelection( + array $package, + string $expectedRuleSetClass + ): void { $ruleSet = $this->createRuleSetFromPackage->invoke($this->factory, $package); self::assertInstanceOf($expectedRuleSetClass, $ruleSet); } + /** + * @return array + */ public function provideRuleSetTestCases(): array { return [ diff --git a/tests/lib/PhpCsFixer/Rule/MultilineParametersFixerTest.php b/tests/lib/PhpCsFixer/Rule/MultilineParametersFixerTest.php new file mode 100644 index 0000000..61f536c --- /dev/null +++ b/tests/lib/PhpCsFixer/Rule/MultilineParametersFixerTest.php @@ -0,0 +1,116 @@ +fixer = new MultilineParametersFixer(); + } + + /** + * @dataProvider provideFixCases + */ + public function testFix( + string $input, + string $expected + ): void { + $tokens = Tokens::fromCode($input); + $this->fixer->fix(new SplFileInfo(__FILE__), $tokens); + + self::assertSame($expected, $tokens->generateCode()); + } + + /** + * @return iterable + */ + public static function provideFixCases(): iterable + { + yield 'single parameter should not be modified' => [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' true, 'whitespace_after_comma_in_array' => true, 'yoda_style' => false, + 'Ibexa/multiline_parameters' => true, ]; diff --git a/tests/lib/PhpCsFixer/Sets/expected_rules/4_6_rule_set/php_cs_fixer_rules.php b/tests/lib/PhpCsFixer/Sets/expected_rules/4_6_rule_set/php_cs_fixer_rules.php index ca860a8..700bc04 100644 --- a/tests/lib/PhpCsFixer/Sets/expected_rules/4_6_rule_set/php_cs_fixer_rules.php +++ b/tests/lib/PhpCsFixer/Sets/expected_rules/4_6_rule_set/php_cs_fixer_rules.php @@ -179,4 +179,5 @@ 'no_trailing_comma_in_singleline_array' => true, 'no_unneeded_curly_braces' => true, 'single_blank_line_before_namespace' => true, + 'Ibexa/multiline_parameters' => true, ]; diff --git a/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/local_rules.php b/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/local_rules.php index 37e51fb..f0685d5 100644 --- a/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/local_rules.php +++ b/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/local_rules.php @@ -218,4 +218,5 @@ 'visibility_required' => true, 'whitespace_after_comma_in_array' => true, 'yoda_style' => false, + 'Ibexa/multiline_parameters' => true, ]; diff --git a/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/php_cs_fixer_rules.php b/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/php_cs_fixer_rules.php index a50d32f..e4b9f51 100644 --- a/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/php_cs_fixer_rules.php +++ b/tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/php_cs_fixer_rules.php @@ -204,4 +204,5 @@ 'types_spaces' => [ 'space' => 'single', ], + 'Ibexa/multiline_parameters' => true, ];