Skip to content

Commit

Permalink
Add HeredocIndentationFixer
Browse files Browse the repository at this point in the history
  • Loading branch information
gharlan committed Sep 7, 2018
1 parent 7136aa4 commit ecd0db7
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,10 @@ Choose from the list of available rules:
- ``separate`` (``'both'``, ``'bottom'``, ``'none'``, ``'top'``): whether the header should be
separated from the file content with a new line; defaults to ``'both'``

* **heredoc_indentation**

Heredoc/nowdoc content must be properly indented. Requires PHP >= 7.3.

* **heredoc_to_nowdoc**

Convert ``heredoc`` to ``nowdoc`` where possible.
Expand Down
152 changes: 152 additions & 0 deletions src/Fixer/Whitespace/HeredocIndentationFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\Whitespace;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @author Gregor Harlan
*/
final class HeredocIndentationFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
/**
* {@inheritdoc}
*/
public function getDefinition()
{
return new FixerDefinition(
'Heredoc/nowdoc content must be properly indented. Requires PHP >= 7.3.',
[
new VersionSpecificCodeSample(
<<<'SAMPLE'
<?php
$a = <<<EOD
abc
def
EOD;
SAMPLE
,
new VersionSpecification(70300)
),
new VersionSpecificCodeSample(
<<<'SAMPLE'
<?php
$a = <<<'EOD'
abc
def
EOD;
SAMPLE
,
new VersionSpecification(70300)
),
]
);
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens)
{
return \PHP_VERSION_ID >= 70300 && $tokens->isTokenKindFound(T_START_HEREDOC);
}

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

$end = $index;

for (--$index; !$tokens[$index]->isGivenKind(T_START_HEREDOC); --$index);

$this->fixIndentation($tokens, $index, $end);
}
}

/**
* @param Tokens $tokens
* @param int $start
* @param int $end
*/
private function fixIndentation(Tokens $tokens, $start, $end)
{
$indent = $this->getIndentAt($tokens, $start).$this->whitespacesConfig->getIndent();

Preg::match('/^[ \t]*/', $tokens[$end]->getContent(), $matches);
$currentIndent = $matches[0];

$content = $indent.substr($tokens[$end]->getContent(), \strlen($currentIndent));
$tokens[$end] = new Token([T_END_HEREDOC, $content]);

if ($end === $start + 1) {
return;
}

for ($index = $end - 1, $last = true; $index > $start; --$index, $last = false) {
if (!$tokens[$index]->isGivenKind([T_ENCAPSED_AND_WHITESPACE, T_WHITESPACE])) {
continue;
}

$regexEnd = $last && !$currentIndent ? '(?!$)' : '';
$content = Preg::replace('/(?<=\v)'.$currentIndent.$regexEnd.'/', $indent, $tokens[$index]->getContent());
$tokens[$index] = new Token([$tokens[$index]->getId(), $content]);
}

++$index;

if ($tokens[$index]->isGivenKind(T_ENCAPSED_AND_WHITESPACE)) {
$content = $indent.substr($tokens[$index]->getContent(), \strlen($currentIndent));
$tokens[$index] = new Token([T_ENCAPSED_AND_WHITESPACE, $content]);
} else {
$tokens->insertAt($index, new Token([T_ENCAPSED_AND_WHITESPACE, $indent]));
}
}

/**
* @param int $index
*
* @return string
*/
private function getIndentAt(Tokens $tokens, $index)
{
for (; $index >= 0; --$index) {
if (!$tokens[$index]->isGivenKind([T_WHITESPACE, T_INLINE_HTML, T_OPEN_TAG])) {
continue;
}

$content = $tokens[$index]->getContent();

if ($tokens[$index]->isWhitespace() && $tokens[$index - 1]->isGivenKind(T_OPEN_TAG)) {
$content = $tokens[$index - 1]->getContent().$content;
}

if (1 === Preg::match('/\R([ \t]*)$/', $content, $matches)) {
return $matches[1];
}
}

return '';
}
}
209 changes: 209 additions & 0 deletions tests/Fixer/Whitespace/HeredocIndentationFixerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tests\Fixer\Whitespace;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
use PhpCsFixer\WhitespacesFixerConfig;

/**
* @author Gregor Harlan
*
* @internal
*
* @covers \PhpCsFixer\Fixer\Whitespace\HeredocIndentationFixer
*/
final class HeredocIndentationFixerTest extends AbstractFixerTestCase
{
/**
* @requires PHP <7.3
*/
public function testDoNotFix()
{
$this->doTest(
<<<'TEST'
<?php
foo(<<<EOD
abc
def
EOD
);
TEST
);
}

/**
* @param string $expected
* @param null|string $input
*
* @dataProvider provideFixCases
* @requires PHP 7.3
*/
public function testFix($expected, $input = null)
{
$this->doTest($expected, $input);
}

public function provideFixCases()
{
return [
[
<<<'EXPECTED'
<?php
foo(<<<EOD
EOD
);
EXPECTED
,
<<<'INPUT'
<?php
foo(<<<EOD
EOD
);
INPUT
,
],
[
<<<'EXPECTED'
<?php
foo(<<<EOD
abc
def
EOD
);
EXPECTED
,
<<<'INPUT'
<?php
foo(<<<EOD
abc
def
EOD
);
INPUT
,
],
[
<<<'EXPECTED'
<?php
foo(<<<'EOD'
abc
def
EOD
);
EXPECTED
,
<<<'INPUT'
<?php
foo(<<<'EOD'
abc
def
EOD
);
INPUT
,
],
[
<<<'EXPECTED'
<?php
foo(<<<'EOD'
abc
def
EOD
);
EXPECTED
,
<<<'INPUT'
<?php
foo(<<<'EOD'
abc
def
EOD
);
INPUT
,
],
[
<<<'EXPECTED'
<?php
foo(<<<EOD
$abc
$def
{$ghi}
EOD
);
EXPECTED
,
<<<'INPUT'
<?php
foo(<<<EOD
$abc
$def
{$ghi}
EOD
);
INPUT
,
],
[
<<<'EXPECTED'
<?php
$a = <<<'EOD'
<?php
$b = <<<FOO
abc
FOO;
EOD;
EXPECTED
,
<<<'INPUT'
<?php
$a = <<<'EOD'
<?php
$b = <<<FOO
abc
FOO;
EOD;
INPUT
,
],
];
}

/**
* @requires PHP 7.3
*/
public function testFixWithTabIndentation()
{
$this->fixer->setWhitespacesConfig(new WhitespacesFixerConfig("\t"));

$expected = <<<EXPECTED
<?php
\t\$a = <<<'EOD'
\t\tabc
\t\t def
\t\t\tghi
\t\tEOD;
EXPECTED;

$input = <<<INPUT
<?php
\t\$a = <<<'EOD'
abc
def
\tghi
EOD;
INPUT;

$this->doTest($expected, $input);
}
}

0 comments on commit ecd0db7

Please sign in to comment.