Skip to content

Commit

Permalink
✨ New Generic.Strings.UnnecessaryHeredoc sniff
Browse files Browse the repository at this point in the history
New `Generic.Strings.UnnecessaryHeredoc` sniff which encourages the use of nowdocs instead of heredocs, when there is no interpolation or expressions in the body text.

This sniff will hopefully help with the PERCS work as it intends to cover the following PER rule:
> A nowdoc SHOULD be used wherever possible. Heredoc MAY be used when a nowdoc does not satisfy requirements.

Includes fixer.
Includes tests.
Includes XML docs.
  • Loading branch information
jrfnl committed Oct 18, 2024
1 parent 9e60f9f commit 6995755
Show file tree
Hide file tree
Showing 8 changed files with 644 additions and 0 deletions.
39 changes: 39 additions & 0 deletions src/Standards/Generic/Docs/Strings/UnnecessaryHeredocStandard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<documentation title="Unnecessary Heredoc">
<standard>
<![CDATA[
If no interpolation or expressions are used in the body of a heredoc, nowdoc syntax should be used instead.
]]>
</standard>
<code_comparison>
<code title="Valid: Using nowdoc syntax for a text string without any interpolation or expressions.">
<![CDATA[
$nowdoc = <em><<<'EOD'</em>
some text
EOD;
]]>
</code>
<code title="Invalid: Using heredoc syntax for a text string without any interpolation or expressions.">
<![CDATA[
$heredoc = <em><<<EOD</em>
some text
EOD;
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: Using heredoc syntax for a text string containing interpolation or expressions.">
<![CDATA[
$heredoc = <em><<<"EOD"</em>
some $text
EOD;
]]>
</code>
<code title="Invalid: Using heredoc syntax for a text string without any interpolation or expressions.">
<![CDATA[
$heredoc = <em><<<"EOD"</em>
some text
EOD;
]]>
</code>
</code_comparison>
</documentation>
93 changes: 93 additions & 0 deletions src/Standards/Generic/Sniffs/Strings/UnnecessaryHeredocSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* Prefer the use of nowdoc over heredoc.
*
* @author Juliette Reinders Folmer <[email protected]>
* @copyright 2024 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/

namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Strings;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class UnnecessaryHeredocSniff implements Sniff
{


/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_START_HEREDOC];

}//end register()


/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in
* the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

if (isset($tokens[$stackPtr]['scope_closer']) === false) {
// Just to be safe. Shouldn't be possible as in that case, the opener shouldn't be tokenized
// to T_START_HEREDOC by PHP.
return;
}

$closer = $tokens[$stackPtr]['scope_closer'];
$body = '';

// Collect all the tokens within the heredoc body.
for ($i = ($stackPtr + 1); $i < $closer; $i++) {
$body .= $tokens[$i]['content'];
}

$tokenizedBody = token_get_all(sprintf("<?php <<<EOD\n%s\nEOD;\n?>", $body));
foreach ($tokenizedBody as $ptr => $bodyToken) {
if (is_array($bodyToken) === false) {
continue;
}

if ($bodyToken[0] === T_DOLLAR_OPEN_CURLY_BRACES
|| $bodyToken[0] === T_VARIABLE
) {
// Contains interpolation or expression.
return;
}

if ($bodyToken[0] === T_CURLY_OPEN
&& is_array($tokenizedBody[($ptr + 1)]) === false
&& $tokenizedBody[($ptr + 1)] === '$'
) {
// Contains interpolation or expression.
return;
}
}

$warning = 'Detected heredoc without interpolation or expressions. Use nowdoc syntax instead';

$fix = $phpcsFile->addFixableWarning($warning, $stackPtr, 'Found');
if ($fix === true) {
$identifier = trim(ltrim($tokens[$stackPtr]['content'], '<'));
$replaceWith = "'".trim($identifier, '"')."'";
$replacement = str_replace($identifier, $replaceWith, $tokens[$stackPtr]['content']);
$phpcsFile->fixer->replaceToken($stackPtr, $replacement);
}

}//end process()


}//end class
108 changes: 108 additions & 0 deletions src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.1.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* Test file 1 and 2 mirror each other, with file 1 containing non-indented cross-version compatible heredoc/nowdoc syntax,
* while the code samples in file 2 use PHP 7.3+ flexible heredoc/nowdoc syntax.
*
* These two files should be kept in sync!
*/

$nowdoc = <<<'EOD'
some text
EOD;

$heredoc = <<<END
some $foo text
END;

$heredoc = <<<"END"
some {$foo[0]} text
END;

$heredoc = <<<END
{$foo?->bar}
END;

$heredoc = <<< "END"
some ${beers::softdrink}
END;

$heredoc = <<< END
{${$object->getName()}} text
END;

$heredoc = <<<"END"
some {${getName()}}
END;

$heredoc = <<<END
${substr('laruence', 0, 2)}
END;

$heredoc = <<<"END"
some {$foo['bar']->baz()()}
END;

$heredoc = <<<END
{$obj->values[3]->name} text
END;

$heredoc = <<<"END"
some ${$bar}
END;

$heredoc = <<<END
${foo->bar} text
END;

$heredoc = <<<"END"
${foo["${bar}"]} text
END;

$heredoc = <<<END
some ${foo["${bar[\'baz\']}"]}
END;

$heredoc = <<<"END"
${foo->{${'a'}}} text
END;

$heredoc = <<<END
some {$foo->{$baz[1]}}
END;

$heredoc = <<<END
some text
{${beers::$ale}}
some text
END;

$heredoc = <<<"END"
$people->john's wife greeted $people->robert.
END;

$heredoc = <<<END
Let's make sure it also works with this: {$arr[foo][3]}
END;

$heredoc = <<<END
Testing ${foo["${bar
['baz']
}"]} and more testing
END;

$heredoc = <<<"END"
Testing {${foo["${bar
['baz']
}"]}} and more testing
END;

$heredoc = <<<END
some text
END;

$heredoc = <<< "END"
some text
some \$text
some text
END;
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* Test file 1 and 2 mirror each other, with file 1 containing non-indented cross-version compatible heredoc/nowdoc syntax,
* while the code samples in file 2 use PHP 7.3+ flexible heredoc/nowdoc syntax.
*
* These two files should be kept in sync!
*/

$nowdoc = <<<'EOD'
some text
EOD;

$heredoc = <<<END
some $foo text
END;

$heredoc = <<<"END"
some {$foo[0]} text
END;

$heredoc = <<<END
{$foo?->bar}
END;

$heredoc = <<< "END"
some ${beers::softdrink}
END;

$heredoc = <<< END
{${$object->getName()}} text
END;

$heredoc = <<<"END"
some {${getName()}}
END;

$heredoc = <<<END
${substr('laruence', 0, 2)}
END;

$heredoc = <<<"END"
some {$foo['bar']->baz()()}
END;

$heredoc = <<<END
{$obj->values[3]->name} text
END;

$heredoc = <<<"END"
some ${$bar}
END;

$heredoc = <<<END
${foo->bar} text
END;

$heredoc = <<<"END"
${foo["${bar}"]} text
END;

$heredoc = <<<END
some ${foo["${bar[\'baz\']}"]}
END;

$heredoc = <<<"END"
${foo->{${'a'}}} text
END;

$heredoc = <<<END
some {$foo->{$baz[1]}}
END;

$heredoc = <<<END
some text
{${beers::$ale}}
some text
END;

$heredoc = <<<"END"
$people->john's wife greeted $people->robert.
END;

$heredoc = <<<END
Let's make sure it also works with this: {$arr[foo][3]}
END;

$heredoc = <<<END
Testing ${foo["${bar
['baz']
}"]} and more testing
END;

$heredoc = <<<"END"
Testing {${foo["${bar
['baz']
}"]}} and more testing
END;

$heredoc = <<<'END'
some text
END;

$heredoc = <<< 'END'
some text
some \$text
some text
END;
Loading

0 comments on commit 6995755

Please sign in to comment.