Skip to content

Commit

Permalink
Add the ChainedMethodBlockFixer
Browse files Browse the repository at this point in the history
  • Loading branch information
leofeyer committed Sep 13, 2023
1 parent 669e442 commit c5310c6
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 0 deletions.
1 change: 1 addition & 0 deletions config/contao.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
// Custom fixers
$ecsConfig->rule(\Contao\EasyCodingStandard\Fixer\AssertEqualsFixer::class);
$ecsConfig->rule(\Contao\EasyCodingStandard\Fixer\CaseCommentIndentationFixer::class);
$ecsConfig->rule(\Contao\EasyCodingStandard\Fixer\ChainedMethodBlockFixer::class);
$ecsConfig->rule(\Contao\EasyCodingStandard\Sniffs\ContaoFrameworkClassAliasSniff::class);
$ecsConfig->rule(\Contao\EasyCodingStandard\Fixer\ExpectsWithCallbackFixer::class);
$ecsConfig->rule(\Contao\EasyCodingStandard\Fixer\FunctionCallWithMultilineArrayFixer::class);
Expand Down
133 changes: 133 additions & 0 deletions src/Fixer/ChainedMethodBlockFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\EasyCodingStandard\Fixer;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class ChainedMethodBlockFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'A block of chained method calls must be followed by an empty line.',
[
new CodeSample(
<<<'EOT'
<?php
use PHPUnit\Framework\TestCase;
class SomeTest extends TestCase
{
public function testFoo(): void
{
$mock = $this->createMock(Foo::class);
$mock
->method('isFoo')
->willReturn(true)
;
$mock
->method('isBar')
->willReturn(false)
;
$mock->isFoo();
}
EOT,
),
],
);
}

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_OBJECT_OPERATOR);
}

/**
* Must run before StatementIndentationFixer.
*/
public function getPriority(): int
{
return -4;
}

protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = 1, $count = \count($tokens); $index < $count; ++$index) {
if (!$tokens[$index]->isGivenKind(T_OBJECT_OPERATOR)) {
continue;
}

// Not a chained call
if (!$tokens[$index - 1]->isWhitespace()) {
continue;
}

$nextMeaningful = $tokens->getNextMeaningfulToken($index);

// Not a method call
if (!$tokens[$nextMeaningful + 1]->equals('(')) {
continue;
}

$end = $tokens->getNextTokenOfKind($index, [';']);

if (!$tokens[$end - 1]->isWhitespace()) {
$index = $end;
continue;
}

$start = $tokens->getPrevTokenOfKind($index, [';', '{']);
$nextMeaningful = $tokens->getNextMeaningfulToken($start);

if ($tokens[$nextMeaningful]->equals('}')) {
$index = $end;
continue;
}

$chainedCalls = 0;
$operators = $tokens->findGivenKind(T_OBJECT_OPERATOR, $start, $end);

foreach (array_keys($operators) as $pos) {
if ($tokens[$pos - 1]->isWhitespace()) {
++$chainedCalls;
}
}

if ($chainedCalls < 1) {
$index = $end;
continue;
}

$nextMeaningful = $tokens->getNextMeaningfulToken($end);

if ($tokens[$nextMeaningful]->equals('}')) {
$index = $end;
continue;
}

if (substr_count($tokens[$end + 1]->getContent(), "\n") < 2) {
$tokens->insertAt($end + 1, new Token([T_WHITESPACE, "\n"]));
}

$index = $end;
}
}
}
135 changes: 135 additions & 0 deletions tests/Fixer/ChainedMethodBlockFixerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\EasyCodingStandard\Tests\Fixer;

use Contao\EasyCodingStandard\Fixer\ChainedMethodBlockFixer;
use PhpCsFixer\Tokenizer\Tokens;
use PHPUnit\Framework\TestCase;

class ChainedMethodBlockFixerTest extends TestCase
{
/**
* @dataProvider getCodeSamples
*/
public function testFixesTheCode(string $code, string $expected): void
{
$tokens = Tokens::fromCode($code);

$fixer = new ChainedMethodBlockFixer();
$fixer->fix($this->createMock('SplFileInfo'), $tokens);

$this->assertSame($expected, $tokens->generateCode());
}

public function getCodeSamples(): \Generator
{
yield [
<<<'EOT'
<?php
use PHPUnit\Framework\TestCase;
class SomeTest extends TestCase
{
public function testFoo(): void
{
$mock = $this->createMock(Foo::class);
$mock
->method("isFoo")
->willReturn(true)
;
$mock
->method("isBar")
->willReturn(false)
;
$mock->isFoo();
if (true) {
$mock
->method("isBaz")
->willReturn(false)
;
}
$mock
->method("isBat")
->willReturnCallback(
function () {
$bar = $this->createMock(Bar::class);
$bar
->method("isFoo")
->willReturn(false)
;
$bar
->method("isBar")
->willReturn(true)
;
}
)
;
}
}
EOT,
<<<'EOT'
<?php
use PHPUnit\Framework\TestCase;
class SomeTest extends TestCase
{
public function testFoo(): void
{
$mock = $this->createMock(Foo::class);
$mock
->method("isFoo")
->willReturn(true)
;
$mock
->method("isBar")
->willReturn(false)
;
$mock->isFoo();
if (true) {
$mock
->method("isBaz")
->willReturn(false)
;
}
$mock
->method("isBat")
->willReturnCallback(
function () {
$bar = $this->createMock(Bar::class);
$bar
->method("isFoo")
->willReturn(false)
;
$bar
->method("isBar")
->willReturn(true)
;
}
)
;
}
}
EOT,
];
}
}

0 comments on commit c5310c6

Please sign in to comment.