Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce multiline_string_to_heredoc fixer #7665

Merged
merged 8 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/rules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,9 @@ String Notation
- `heredoc_to_nowdoc <./string_notation/heredoc_to_nowdoc.rst>`_

Convert ``heredoc`` to ``nowdoc`` where possible.
- `multiline_string_to_heredoc <./string_notation/multiline_string_to_heredoc.rst>`_

Convert multiline string to ``heredoc`` or ``nowdoc``.
- `no_binary_string <./string_notation/no_binary_string.rst>`_

There should not be a binary flag before strings.
Expand Down
45 changes: 45 additions & 0 deletions doc/rules/string_notation/multiline_string_to_heredoc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
====================================
Rule ``multiline_string_to_heredoc``
====================================

Convert multiline string to ``heredoc`` or ``nowdoc``.

Examples
--------

Example #1
~~~~~~~~~~

.. code-block:: diff

--- Original
+++ New
<?php
-$a = 'line1
-line2';
+$a = <<<'EOD'
+line1
+line2
+EOD;

Example #2
~~~~~~~~~~

.. code-block:: diff

--- Original
+++ New
<?php
-$a = "line1
-{$obj->getName()}";
+$a = <<<EOD
+line1
+{$obj->getName()}
+EOD;
References
----------

- Fixer class: `PhpCsFixer\\Fixer\\StringNotation\\MultilineStringToHeredocFixer <./../../../src/Fixer/StringNotation/MultilineStringToHeredocFixer.php>`_
- Test class: `PhpCsFixer\\Tests\\Fixer\\StringNotation\\MultilineStringToHeredocFixerTest <./../../../tests/Fixer/StringNotation/MultilineStringToHeredocFixerTest.php>`_

The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function isCandidate(Tokens $tokens): bool
* {@inheritdoc}
*
* Must run before HeredocToNowdocFixer, SingleQuoteFixer.
* Must run after BacktickToShellExecFixer.
* Must run after BacktickToShellExecFixer, MultilineStringToHeredocFixer.
*/
public function getPriority(): int
{
Expand Down
166 changes: 166 additions & 0 deletions src/Fixer/StringNotation/MultilineStringToHeredocFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

/*
* 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\StringNotation;

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

/**
* @author Michael Vorisek <https://github.com/mvorisek>
*/
final class MultilineStringToHeredocFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Convert multiline string to `heredoc` or `nowdoc`.',
[
new CodeSample(
<<<'EOD'
<?php
$a = 'line1
line2';
EOD."\n"
),
new CodeSample(
<<<'EOD'
<?php
$a = "line1
{$obj->getName()}";
EOD."\n"
),
]
);
}

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE]);
}

/**
* {@inheritdoc}
*
* Must run before EscapeImplicitBackslashesFixer, HeredocIndentationFixer.
*/
public function getPriority(): int
{
return 16;
}

protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$inHeredoc = false;
$complexStringStartIndex = null;
foreach ($tokens as $index => $token) {
if ($token->isGivenKind([T_START_HEREDOC, T_END_HEREDOC])) {
$inHeredoc = $token->isGivenKind(T_START_HEREDOC) || !$token->isGivenKind(T_END_HEREDOC);

continue;
}

if (null === $complexStringStartIndex) {
if ($token->isGivenKind(T_CONSTANT_ENCAPSED_STRING)) {
$this->convertStringToHeredoc($tokens, $index, $index);

// skip next 2 added tokens if replaced
if ($tokens[$index]->isGivenKind(T_START_HEREDOC)) {
$inHeredoc = true;
}
} elseif ($token->equalsAny(['"', 'b"', 'B"'])) {
$complexStringStartIndex = $index;
}
} elseif ($token->equals('"')) {
$this->convertStringToHeredoc($tokens, $complexStringStartIndex, $index);

$complexStringStartIndex = null;
}
}
}

private function convertStringToHeredoc(Tokens $tokens, int $stringStartIndex, int $stringEndIndex): void
{
$closingMarker = 'EOD';

if ($tokens[$stringStartIndex]->isGivenKind(T_CONSTANT_ENCAPSED_STRING)) {
$content = $tokens[$stringStartIndex]->getContent();
if ('b' === strtolower(substr($content, 0, 1))) {
$content = substr($content, 1);
}
$isSingleQuoted = str_starts_with($content, '\'');
$content = substr($content, 1, -1);

if ($isSingleQuoted) {
$content = Preg::replace('~\\\\([\\\\\'])~', '$1', $content);
} else {
$content = Preg::replace('~(\\\\\\\\)|\\\\(")~', '$1$2', $content);
}

$constantStringToken = new Token([T_ENCAPSED_AND_WHITESPACE, $content."\n"]);
} else {
$content = $tokens->generatePartialCode($stringStartIndex + 1, $stringEndIndex - 1);
$isSingleQuoted = false;
$constantStringToken = null;
}

if (!str_contains($content, "\n") && !str_contains($content, "\r")) {
return;
}

while (Preg::match('~(^|[\r\n])\s*'.preg_quote($closingMarker, '~').'(?!\w)~', $content)) {
$closingMarker .= '_';
}

$quoting = $isSingleQuoted ? '\'' : '';
$heredocStartToken = new Token([T_START_HEREDOC, '<<<'.$quoting.$closingMarker.$quoting."\n"]);
$heredocEndToken = new Token([T_END_HEREDOC, $closingMarker]);

if (null !== $constantStringToken) {
$tokens->overrideRange($stringStartIndex, $stringEndIndex, [
$heredocStartToken,
$constantStringToken,
$heredocEndToken,
]);
} else {
for ($i = $stringStartIndex + 1; $i < $stringEndIndex; ++$i) {
if ($tokens[$i]->isGivenKind(T_ENCAPSED_AND_WHITESPACE)) {
$tokens[$i] = new Token([
$tokens[$i]->getId(),
Preg::replace('~(\\\\\\\\)|\\\\(")~', '$1$2', $tokens[$i]->getContent()),
]);
}
}

$tokens[$stringStartIndex] = $heredocStartToken;
$tokens[$stringEndIndex] = $heredocEndToken;
if ($tokens[$stringEndIndex - 1]->isGivenKind(T_ENCAPSED_AND_WHITESPACE)) {
$tokens[$stringEndIndex - 1] = new Token([
$tokens[$stringEndIndex - 1]->getId(),
$tokens[$stringEndIndex - 1]->getContent()."\n",
]);
} else {
$tokens->insertAt($stringEndIndex, new Token([
T_ENCAPSED_AND_WHITESPACE,
"\n",
]));
}
}
}
}
2 changes: 1 addition & 1 deletion src/Fixer/Whitespace/HeredocIndentationFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function getDefinition(): FixerDefinitionInterface
/**
* {@inheritdoc}
*
* Must run after BracesFixer, StatementIndentationFixer.
* Must run after BracesFixer, MultilineStringToHeredocFixer, StatementIndentationFixer.
*/
public function getPriority(): int
{
Expand Down
4 changes: 4 additions & 0 deletions tests/AutoReview/FixerFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,10 @@ private static function getFixersPriorityGraph(): array
'modernize_types_casting' => [
'no_unneeded_control_parentheses',
],
'multiline_string_to_heredoc' => [
'escape_implicit_backslashes',
'heredoc_indentation',
],
'multiline_whitespace_before_semicolons' => [
'space_after_semicolon',
],
Expand Down