• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

j-schumann / symfony-addons / 15680692345

16 Jun 2025 12:22PM UTC coverage: 53.664%. First build
15680692345

push

github

j-schumann
Merge branch 'develop'

# Conflicts:
#	.php-cs-fixer.dist.php
#	composer.json
#	src/PHPUnit/ApiPlatformTestCase.php
#	src/PHPUnit/AuthenticatedClientTrait.php

103 of 382 new or added lines in 10 files covered. (26.96%)

476 of 887 relevant lines covered (53.66%)

3.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/PhpCsFixer/WrapNamedMethodArgumentsFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Vrok\SymfonyAddons\PhpCsFixer;
6

7
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
8
use PhpCsFixer\Fixer\FixerInterface;
9
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
10
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
11
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
12
use PhpCsFixer\FixerDefinition\CodeSample;
13
use PhpCsFixer\FixerDefinition\FixerDefinition;
14
use PhpCsFixer\Tokenizer\Token;
15
use PhpCsFixer\Tokenizer\Tokens;
16

17
final class WrapNamedMethodArgumentsFixer implements FixerInterface, ConfigurableFixerInterface
18
{
19
    private const int DEFAULT_MAX_ARGUMENTS = 3;
20
    private const array NESTING_OPEN_TOKENS = ['(', '[', '{'];
21
    private const array NESTING_CLOSE_TOKENS = [')', ']', '}'];
22

23
    private int $maxArguments = self::DEFAULT_MAX_ARGUMENTS;
24

25
    public function getDefinition(): FixerDefinition
26
    {
NEW
27
        return new FixerDefinition(
×
NEW
28
            'Wrap method arguments to separate lines when they are named and exceed the maximum argument count (default 3).',
×
NEW
29
            [
×
NEW
30
                new CodeSample(
×
NEW
31
                    '<?php
×
32
$this->method(arg1: $value1, arg2: $value2, arg3: $value3);
33
// will be changed to:
34
$this->method(
35
    arg1: $value1,
36
    arg2: $value2,
37
    arg3: $value3
38
);
39

40
// will stay unchanged:
NEW
41
$this->method(arg1: $value1, arg2: $value2);',
×
NEW
42
                    ['max_arguments' => 2]
×
NEW
43
                ),
×
NEW
44
            ]
×
NEW
45
        );
×
46
    }
47

48
    public function getName(): string
49
    {
NEW
50
        return 'VrokSymfonyAddons/wrap_named_method_arguments';
×
51
    }
52

53
    public function getPriority(): int
54
    {
NEW
55
        return 100;
×
56
    }
57

58
    public function supports(\SplFileInfo $file): bool
59
    {
NEW
60
        return true;
×
61
    }
62

63
    /**
64
     * @param Tokens<Token> $tokens
65
     */
66
    public function isCandidate(Tokens $tokens): bool
67
    {
NEW
68
        return $tokens->isTokenKindFound(T_STRING);
×
69
    }
70

71
    public function isRisky(): bool
72
    {
NEW
73
        return false;
×
74
    }
75

76
    public function getConfigurationDefinition(): FixerConfigurationResolverInterface
77
    {
NEW
78
        return new FixerConfigurationResolver([
×
NEW
79
            (new FixerOptionBuilder(
×
NEW
80
                'max_arguments',
×
NEW
81
                'Maximum number of arguments before formatting is applied.'
×
NEW
82
            ))
×
NEW
83
                ->setAllowedTypes(['int'])
×
NEW
84
                ->setDefault(self::DEFAULT_MAX_ARGUMENTS)
×
NEW
85
                ->getOption(),
×
NEW
86
        ]);
×
87
    }
88

89
    public function configure(array $configuration): void
90
    {
NEW
91
        $this->maxArguments = $configuration['max_arguments'] ?? self::DEFAULT_MAX_ARGUMENTS;
×
92
    }
93

94
    /**
95
     * @param Tokens<Token> $tokens
96
     */
97
    public function fix(\SplFileInfo $file, Tokens $tokens): void
98
    {
NEW
99
        for ($i = 0, $tokenCount = $tokens->count(); $i < $tokenCount; ++$i) {
×
NEW
100
            if (!$tokens[$i]->isGivenKind(T_STRING)) {
×
NEW
101
                continue;
×
102
            }
103

NEW
104
            $openParenIndex = $tokens->getNextMeaningfulToken($i);
×
105
            if (
NEW
106
                null === $openParenIndex
×
NEW
107
                || !$tokens[$openParenIndex]->equals('(')
×
108
            ) {
NEW
109
                continue;
×
110
            }
111

NEW
112
            $closeParenIndex = $tokens->findBlockEnd(
×
NEW
113
                Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
×
NEW
114
                $openParenIndex
×
NEW
115
            );
×
116

NEW
117
            if ($this->shouldFormatMethodCall($tokens, $openParenIndex, $closeParenIndex)) {
×
NEW
118
                $indentation = $this->detectIndentation($tokens, $i);
×
NEW
119
                $this->formatMethodCall($tokens, $openParenIndex, $closeParenIndex, $indentation);
×
120
            }
121
        }
122
    }
123

124
    /**
125
     * @param Tokens<Token> $tokens
126
     */
127
    private function shouldFormatMethodCall(
128
        Tokens $tokens,
129
        int $openParenIndex,
130
        int $closeParenIndex,
131
    ): bool {
NEW
132
        $analysisResult = $this->analyzeArguments($tokens, $openParenIndex, $closeParenIndex);
×
133

134
        // Only format if we have named args and exceed the threshold
NEW
135
        if (!$analysisResult['hasNamedArgs'] || $analysisResult['argumentCount'] <= $this->maxArguments) {
×
NEW
136
            return false;
×
137
        }
138

139
        // Check if arguments are already on separate lines
NEW
140
        if ($this->areArgumentsAlreadyFormatted($tokens, $openParenIndex, $closeParenIndex, $analysisResult['topLevelCommas'])) {
×
NEW
141
            return false;
×
142
        }
143

NEW
144
        return true;
×
145
    }
146

147
    private function areArgumentsAlreadyFormatted(Tokens $tokens, int $openParenIndex, int $closeParenIndex, array $topLevelCommas): bool
148
    {
149
        // Check if there's a newline after the opening parenthesis
NEW
150
        $nextIndex = $openParenIndex + 1;
×
NEW
151
        if ($nextIndex < $closeParenIndex && $tokens[$nextIndex]->isWhitespace()) {
×
NEW
152
            if (false !== strpos($tokens[$nextIndex]->getContent(), "\n")) {
×
153
                // There's a newline after opening paren, likely already formatted
NEW
154
                return true;
×
155
            }
156
        }
157

158
        // Check if there are newlines after commas
NEW
159
        foreach ($topLevelCommas as $commaIndex) {
×
NEW
160
            $nextIndex = $commaIndex + 1;
×
NEW
161
            if ($nextIndex < $closeParenIndex && $tokens[$nextIndex]->isWhitespace()) {
×
NEW
162
                if (false !== strpos($tokens[$nextIndex]->getContent(), "\n")) {
×
163
                    // Found newline after comma, likely already formatted
NEW
164
                    return true;
×
165
                }
166
            }
167
        }
168

NEW
169
        return false;
×
170
    }
171

172
    /**
173
     * @param Tokens<Token> $tokens
174
     */
175
    private function detectIndentation(Tokens $tokens, int $functionNameIndex): array
176
    {
177
        // Find the start of the line containing the function call
NEW
178
        $lineStartIndex = $functionNameIndex;
×
NEW
179
        while ($lineStartIndex > 0) {
×
NEW
180
            $prevIndex = $lineStartIndex - 1;
×
181
            if (
NEW
182
                $tokens[$prevIndex]->isWhitespace()
×
NEW
183
                && str_contains($tokens[$prevIndex]->getContent(), "\n")
×
184
            ) {
NEW
185
                break;
×
186
            }
NEW
187
            --$lineStartIndex;
×
188
        }
189

190
        // Detect current line indentation
NEW
191
        $baseIndent = '';
×
NEW
192
        if ($lineStartIndex > 0 && $tokens[$lineStartIndex]->isWhitespace()) {
×
NEW
193
            $whitespace = $tokens[$lineStartIndex]->getContent();
×
NEW
194
            $lines = explode("\n", $whitespace);
×
NEW
195
            $baseIndent = end($lines); // Get indentation after the last newline
×
196
        }
197

198
        // Detect indentation unit (try to find consistent indentation in the file)
NEW
199
        $indentUnit = $this->detectIndentationUnit($tokens);
×
200

NEW
201
        return [
×
NEW
202
            'base'     => $baseIndent,
×
NEW
203
            'unit'     => $indentUnit,
×
NEW
204
            'argument' => $baseIndent.$indentUnit,
×
NEW
205
        ];
×
206
    }
207

208
    /**
209
     * @param Tokens<Token> $tokens
210
     */
211
    private function detectIndentationUnit(Tokens $tokens): string
212
    {
NEW
213
        $indentations = [];
×
214

215
        // Sample some whitespace tokens to detect indentation pattern
NEW
216
        for ($i = 0, $count = min(100, $tokens->count()); $i < $count; ++$i) {
×
NEW
217
            if (!$tokens[$i]->isWhitespace()) {
×
NEW
218
                continue;
×
219
            }
220

NEW
221
            $content = $tokens[$i]->getContent();
×
NEW
222
            if (!str_contains($content, "\n")) {
×
NEW
223
                continue;
×
224
            }
225

NEW
226
            $lines = explode("\n", $content);
×
NEW
227
            foreach ($lines as $line) {
×
NEW
228
                if ('' === $line) {
×
NEW
229
                    continue;
×
230
                }
231

232
                // Count leading spaces/tabs
NEW
233
                $indent = '';
×
NEW
234
                $len = \strlen($line);
×
NEW
235
                for ($j = 0; $j < $len; ++$j) {
×
NEW
236
                    if (' ' === $line[$j] || "\t" === $line[$j]) {
×
NEW
237
                        $indent .= $line[$j];
×
238
                    } else {
NEW
239
                        break;
×
240
                    }
241
                }
242

NEW
243
                if ('' !== $indent) {
×
NEW
244
                    $indentations[] = $indent;
×
245
                }
246
            }
247
        }
248

249
        // Analyze indentations to find the unit
NEW
250
        if ([] === $indentations) {
×
NEW
251
            return '    '; // Default to 4 spaces
×
252
        }
253

254
        // Check if using tabs
NEW
255
        foreach ($indentations as $indent) {
×
NEW
256
            if (str_contains($indent, "\t")) {
×
NEW
257
                return "\t";
×
258
            }
259
        }
260

261
        // Count spaces - find the smallest non-zero indentation
NEW
262
        $spaceCounts = array_map('strlen', $indentations);
×
NEW
263
        $spaceCounts = array_filter($spaceCounts, static fn ($count) => $count > 0);
×
264

NEW
265
        if ([] === $spaceCounts) {
×
NEW
266
            return '    '; // Default to 4 spaces
×
267
        }
268

NEW
269
        $minSpaces = min($spaceCounts);
×
270

NEW
271
        return str_repeat(' ', $minSpaces);
×
272
    }
273

274
    /**
275
     * @param Tokens<Token> $tokens
276
     */
277
    private function analyzeArguments(
278
        Tokens $tokens,
279
        int $openParenIndex,
280
        int $closeParenIndex,
281
    ): array {
NEW
282
        $topLevelCommas = [];
×
NEW
283
        $nestingLevel = 0;
×
NEW
284
        $hasContent = false;
×
NEW
285
        $hasNamedArgs = false;
×
286

NEW
287
        for ($i = $openParenIndex + 1; $i < $closeParenIndex; ++$i) {
×
NEW
288
            $token = $tokens[$i];
×
NEW
289
            $content = $token->getContent();
×
290

NEW
291
            if (\in_array($content, self::NESTING_OPEN_TOKENS, true)) {
×
NEW
292
                ++$nestingLevel;
×
NEW
293
            } elseif (\in_array($content, self::NESTING_CLOSE_TOKENS, true)) {
×
NEW
294
                --$nestingLevel;
×
NEW
295
            } elseif (',' === $content && 0 === $nestingLevel) {
×
NEW
296
                $topLevelCommas[] = $i;
×
NEW
297
            } elseif (':' === $content) {
×
NEW
298
                $hasNamedArgs = true;
×
299
            }
300

NEW
301
            if (!$token->isWhitespace()) {
×
NEW
302
                $hasContent = true;
×
303
            }
304
        }
305

NEW
306
        return [
×
NEW
307
            'argumentCount'  => $hasContent ? \count($topLevelCommas) + 1 : 0,
×
NEW
308
            'hasNamedArgs'   => $hasNamedArgs,
×
NEW
309
            'topLevelCommas' => $topLevelCommas,
×
NEW
310
        ];
×
311
    }
312

313
    /**
314
     * @param Tokens<Token> $tokens
315
     */
316
    private function formatMethodCall(Tokens $tokens, int $openParenIndex, int $closeParenIndex, array $indentation): void
317
    {
NEW
318
        $analysisResult = $this->analyzeArguments($tokens, $openParenIndex, $closeParenIndex);
×
NEW
319
        $topLevelCommas = $analysisResult['topLevelCommas'];
×
320

321
        // Work backwards to avoid index shifts
NEW
322
        $this->addNewlineBeforeClosingParenthesis($tokens, $closeParenIndex, $indentation['base']);
×
NEW
323
        $this->addNewlinesAfterCommas($tokens, $topLevelCommas, $indentation['argument']);
×
NEW
324
        $this->addNewlineAfterOpeningParenthesis($tokens, $openParenIndex, $indentation['argument']);
×
325
    }
326

327
    /**
328
     * @param Tokens<Token> $tokens
329
     */
330
    private function addNewlineBeforeClosingParenthesis(Tokens $tokens, int $closeParenIndex, string $baseIndent): void
331
    {
NEW
332
        $prevIndex = $tokens->getPrevMeaningfulToken($closeParenIndex);
×
NEW
333
        if (null === $prevIndex) {
×
NEW
334
            return;
×
335
        }
336

NEW
337
        if ($prevIndex + 1 === $closeParenIndex) {
×
NEW
338
            $tokens->insertAt($closeParenIndex, new Token([T_WHITESPACE, "\n".$baseIndent]));
×
339
        } else {
NEW
340
            $this->replaceWhitespaceWithNewline($tokens, $prevIndex + 1, $baseIndent);
×
341
        }
342
    }
343

344
    /**
345
     * @param Tokens<Token> $tokens
346
     */
347
    private function addNewlinesAfterCommas(Tokens $tokens, array $topLevelCommas, string $argumentIndent): void
348
    {
NEW
349
        foreach (array_reverse($topLevelCommas) as $commaIndex) {
×
NEW
350
            $nextTokenIndex = $commaIndex + 1;
×
NEW
351
            if ($nextTokenIndex < \count($tokens) && $tokens[$nextTokenIndex]->isWhitespace()) {
×
NEW
352
                $tokens[$nextTokenIndex] = new Token([T_WHITESPACE, "\n".$argumentIndent]);
×
353
            } else {
NEW
354
                $tokens->insertAt($commaIndex + 1, new Token([T_WHITESPACE, "\n".$argumentIndent]));
×
355
            }
356
        }
357
    }
358

359
    /**
360
     * @param Tokens<Token> $tokens
361
     */
362
    private function addNewlineAfterOpeningParenthesis(Tokens $tokens, int $openParenIndex, string $argumentIndent): void
363
    {
NEW
364
        $nextTokenIndex = $openParenIndex + 1;
×
NEW
365
        if ($nextTokenIndex < \count($tokens) && $tokens[$nextTokenIndex]->isWhitespace()) {
×
NEW
366
            $tokens[$nextTokenIndex] = new Token([T_WHITESPACE, "\n".$argumentIndent]);
×
367
        } else {
NEW
368
            $tokens->insertAt($openParenIndex + 1, new Token([T_WHITESPACE, "\n".$argumentIndent]));
×
369
        }
370
    }
371

372
    /**
373
     * @param Tokens<Token> $tokens
374
     */
375
    private function replaceWhitespaceWithNewline(Tokens $tokens, int $whitespaceIndex, string $indent): void
376
    {
NEW
377
        if ($tokens[$whitespaceIndex]->isWhitespace()) {
×
NEW
378
            $content = $tokens[$whitespaceIndex]->getContent();
×
NEW
379
            if (!str_contains($content, "\n")) {
×
NEW
380
                $tokens[$whitespaceIndex] = new Token([T_WHITESPACE, "\n".$indent]);
×
381
            }
382
        }
383
    }
384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc