Skip to content

Commit 712b503

Browse files
committed
Added rule for splitting multiple methods arguments into multiline if there is more than one
1 parent d686fc6 commit 712b503

File tree

8 files changed

+301
-3
lines changed

8 files changed

+301
-3
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\CodeStyle\PhpCsFixer\Rule;
10+
11+
use PhpCsFixer\AbstractFixer;
12+
use PhpCsFixer\FixerDefinition\CodeSample;
13+
use PhpCsFixer\FixerDefinition\FixerDefinition;
14+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
15+
use PhpCsFixer\Tokenizer\Token;
16+
use PhpCsFixer\Tokenizer\Tokens;
17+
18+
final class MultilineParametersFixer extends AbstractFixer
19+
{
20+
public function getDefinition(): FixerDefinitionInterface
21+
{
22+
return new FixerDefinition(
23+
'Methods and functions with 2+ parameters must use multiline format',
24+
[
25+
new CodeSample(
26+
'<?php function foo(string $a, int $b): void {}'
27+
),
28+
]
29+
);
30+
}
31+
32+
public function isCandidate(Tokens $tokens): bool
33+
{
34+
return $tokens->isTokenKindFound(T_FUNCTION);
35+
}
36+
37+
protected function applyFix(
38+
\SplFileInfo $file,
39+
Tokens $tokens
40+
): void {
41+
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
42+
if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
43+
continue;
44+
}
45+
46+
$openParenIndex = $tokens->getNextTokenOfKind($index, ['(']);
47+
$closeParenIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenIndex);
48+
49+
// Count commas to determine parameter count
50+
$paramCount = $this->countParameters($tokens, $openParenIndex, $closeParenIndex);
51+
52+
// Only process if 2+ parameters
53+
if ($paramCount < 2) {
54+
continue;
55+
}
56+
57+
// Check if already multiline
58+
if ($this->isMultiline($tokens, $openParenIndex, $closeParenIndex)) {
59+
continue;
60+
}
61+
62+
// Apply multiline formatting
63+
$this->makeMultiline($tokens, $openParenIndex, $closeParenIndex);
64+
}
65+
}
66+
67+
private function countParameters(
68+
Tokens $tokens,
69+
int $start,
70+
int $end
71+
): int {
72+
$count = 0;
73+
$depth = 0;
74+
75+
for ($i = $start + 1; $i < $end; ++$i) {
76+
if ($tokens[$i]->equals('(') || $tokens[$i]->equals('[')) {
77+
++$depth;
78+
} elseif ($tokens[$i]->equals(')') || $tokens[$i]->equals(']')) {
79+
--$depth;
80+
} elseif ($depth === 0 && $tokens[$i]->equals(',')) {
81+
++$count;
82+
}
83+
}
84+
85+
// If we found any commas, param count is commas + 1
86+
// If no commas but there's content, it's 1 param
87+
if ($count > 0) {
88+
return $count + 1;
89+
}
90+
91+
// Check if there's any non-whitespace content
92+
for ($i = $start + 1; $i < $end; ++$i) {
93+
if (!$tokens[$i]->isWhitespace()) {
94+
return 1;
95+
}
96+
}
97+
98+
return 0;
99+
}
100+
101+
private function isMultiline(
102+
Tokens $tokens,
103+
int $start,
104+
int $end
105+
): bool {
106+
for ($i = $start; $i <= $end; ++$i) {
107+
if ($tokens[$i]->isGivenKind(T_WHITESPACE) && str_contains($tokens[$i]->getContent(), "\n")) {
108+
return true;
109+
}
110+
}
111+
112+
return false;
113+
}
114+
115+
private function makeMultiline(
116+
Tokens $tokens,
117+
int $openParenIndex,
118+
int $closeParenIndex
119+
): void {
120+
$indent = $this->detectIndent($tokens, $openParenIndex);
121+
$lineIndent = str_repeat(' ', 4); // 4 spaces for parameters
122+
123+
// Add newline after opening parenthesis
124+
$tokens->insertAt($openParenIndex + 1, new Token([T_WHITESPACE, "\n" . $indent . $lineIndent]));
125+
++$closeParenIndex;
126+
127+
// Find all commas and add newlines after them
128+
$depth = 0;
129+
for ($i = $openParenIndex + 1; $i < $closeParenIndex; ++$i) {
130+
if ($tokens[$i]->equals('(') || $tokens[$i]->equals('[')) {
131+
++$depth;
132+
} elseif ($tokens[$i]->equals(')') || $tokens[$i]->equals(']')) {
133+
--$depth;
134+
} elseif ($depth === 0 && $tokens[$i]->equals(',')) {
135+
// Remove any whitespace after comma
136+
$nextIndex = $i + 1;
137+
while ($nextIndex < $closeParenIndex && $tokens[$nextIndex]->isWhitespace()) {
138+
$tokens->clearAt($nextIndex);
139+
++$nextIndex;
140+
}
141+
142+
// Insert newline with proper indentation
143+
$tokens->insertAt($i + 1, new Token([T_WHITESPACE, "\n" . $indent . $lineIndent]));
144+
$closeParenIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenIndex);
145+
}
146+
}
147+
148+
// Add newline before closing parenthesis
149+
$tokens->insertAt($closeParenIndex, new Token([T_WHITESPACE, "\n" . $indent]));
150+
151+
// Handle the opening brace
152+
$nextNonWhitespace = $tokens->getNextNonWhitespace($closeParenIndex);
153+
if ($nextNonWhitespace !== null && $tokens[$nextNonWhitespace]->equals(T_CURLY_OPEN)) {
154+
$tokens->ensureWhitespaceAtIndex($nextNonWhitespace - 1, 1, ' ');
155+
}
156+
}
157+
158+
private function detectIndent(
159+
Tokens $tokens,
160+
int $index
161+
): string {
162+
// Look backwards to find the indentation of the current line
163+
for ($i = $index - 1; $i >= 0; --$i) {
164+
if ($tokens[$i]->isGivenKind(T_WHITESPACE) && str_contains($tokens[$i]->getContent(), "\n")) {
165+
$lines = explode("\n", $tokens[$i]->getContent());
166+
167+
return end($lines);
168+
}
169+
}
170+
171+
return '';
172+
}
173+
174+
public function getName(): string
175+
{
176+
return 'Ibexa/multiline_parameters';
177+
}
178+
}

src/lib/PhpCsFixer/Sets/AbstractIbexaRuleSet.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Ibexa\CodeStyle\PhpCsFixer\Sets;
1010

1111
use AdamWojs\PhpCsFixerPhpdocForceFQCN\Fixer\Phpdoc\ForceFQCNFixer;
12+
use Ibexa\CodeStyle\PhpCsFixer\Rule\MultilineParametersFixer;
1213
use PhpCsFixer\Config;
1314

1415
abstract class AbstractIbexaRuleSet implements RuleSetInterface
@@ -26,6 +27,7 @@ public function getRules(): array
2627
return [
2728
'@PSR12' => false,
2829
'AdamWojs/phpdoc_force_fqcn_fixer' => true,
30+
'Ibexa/multiline_parameters' => true,
2931
'array_syntax' => [
3032
'syntax' => 'short',
3133
],
@@ -204,7 +206,7 @@ public function getRules(): array
204206
public function buildConfig(): Config
205207
{
206208
$config = new Config();
207-
$config->registerCustomFixers([new ForceFQCNFixer()]);
209+
$config->registerCustomFixers([new ForceFQCNFixer(), new MultilineParametersFixer()]);
208210

209211
$config->setRules(array_merge(
210212
$config->getRules(),

tests/lib/PhpCsFixer/InternalConfigFactoryTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ protected function setUp(): void
3838
/**
3939
* @dataProvider provideRuleSetTestCases
4040
*/
41-
public function testVersionBasedRuleSetSelection(array $package, string $expectedRuleSetClass): void
42-
{
41+
public function testVersionBasedRuleSetSelection(
42+
array $package,
43+
string $expectedRuleSetClass
44+
): void {
4345
$ruleSet = $this->createRuleSetFromPackage->invoke($this->factory, $package);
4446

4547
self::assertInstanceOf($expectedRuleSetClass, $ruleSet);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\CodeStyle\PhpCsFixer\Rule;
10+
11+
use Ibexa\CodeStyle\PhpCsFixer\Rule\MultilineParametersFixer;
12+
use PhpCsFixer\Tokenizer\Tokens;
13+
use PHPUnit\Framework\TestCase;
14+
15+
final class MultilineParametersFixerTest extends TestCase
16+
{
17+
private MultilineParametersFixer $fixer;
18+
19+
protected function setUp(): void
20+
{
21+
$this->fixer = new MultilineParametersFixer();
22+
}
23+
24+
/**
25+
* @dataProvider provideFixCases
26+
*/
27+
public function testFix(
28+
string $input,
29+
string $expected
30+
): void {
31+
$tokens = Tokens::fromCode($input);
32+
$this->fixer->fix(new \SplFileInfo(__FILE__), $tokens);
33+
34+
self::assertSame($expected, $tokens->generateCode());
35+
}
36+
37+
public static function provideFixCases(): iterable
38+
{
39+
yield 'single parameter should not be modified' => [
40+
'<?php
41+
function bar(array $package): void {
42+
}',
43+
'<?php
44+
function bar(array $package): void {
45+
}',
46+
];
47+
48+
yield 'single parameter with type hints should not be modified' => [
49+
'<?php
50+
function bar(?string $package = null): void {
51+
}',
52+
'<?php
53+
function bar(?string $package = null): void {
54+
}',
55+
];
56+
57+
yield 'multiple parameters should be split' => [
58+
'<?php
59+
function foo(array $package, string $expectedRuleSetClass): void {
60+
}',
61+
'<?php
62+
function foo(
63+
array $package,
64+
string $expectedRuleSetClass
65+
): void {
66+
}',
67+
];
68+
69+
yield 'multiple parameters with type hints should be split' => [
70+
'<?php
71+
function test(?string $foo = null, int $bar = 42): string {
72+
}',
73+
'<?php
74+
function test(
75+
?string $foo = null,
76+
int $bar = 42
77+
): string {
78+
}',
79+
];
80+
81+
yield 'constructor with properties should be split' => [
82+
'<?php
83+
class Test {
84+
public function __construct(private string $foo, private int $bar) {
85+
}
86+
}',
87+
'<?php
88+
class Test {
89+
public function __construct(
90+
private string $foo,
91+
private int $bar
92+
) {
93+
}
94+
}',
95+
];
96+
97+
yield 'already multiline should not be modified' => [
98+
'<?php
99+
function test(
100+
string $foo,
101+
int $bar
102+
): void {
103+
}',
104+
'<?php
105+
function test(
106+
string $foo,
107+
int $bar
108+
): void {
109+
}',
110+
];
111+
}
112+
}

tests/lib/PhpCsFixer/Sets/expected_rules/4_6_rule_set/local_rules.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,5 @@
195195
'visibility_required' => true,
196196
'whitespace_after_comma_in_array' => true,
197197
'yoda_style' => false,
198+
'Ibexa/multiline_parameters' => true,
198199
];

tests/lib/PhpCsFixer/Sets/expected_rules/4_6_rule_set/php_cs_fixer_rules.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,5 @@
179179
'no_trailing_comma_in_singleline_array' => true,
180180
'no_unneeded_curly_braces' => true,
181181
'single_blank_line_before_namespace' => true,
182+
'Ibexa/multiline_parameters' => true,
182183
];

tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/local_rules.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,5 @@
218218
'visibility_required' => true,
219219
'whitespace_after_comma_in_array' => true,
220220
'yoda_style' => false,
221+
'Ibexa/multiline_parameters' => true,
221222
];

tests/lib/PhpCsFixer/Sets/expected_rules/5_0_rule_set/php_cs_fixer_rules.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,5 @@
204204
'types_spaces' => [
205205
'space' => 'single',
206206
],
207+
'Ibexa/multiline_parameters' => true,
207208
];

0 commit comments

Comments
 (0)