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

✨ Native handling of sniff deprecations #281

Merged
merged 3 commits into from
Jan 25, 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
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.2/phpunit.xsd"
backupGlobals="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="false"
bootstrap="tests/bootstrap.php"
convertErrorsToExceptions="true"
Expand Down
163 changes: 163 additions & 0 deletions src/Ruleset.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace PHP_CodeSniffer;

use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Sniffs\DeprecatedSniff;
use PHP_CodeSniffer\Util;
use stdClass;

Expand Down Expand Up @@ -116,6 +117,16 @@ class Ruleset
*/
private $config = null;

/**
* An array of the names of sniffs which have been marked as deprecated.
*
* The key is the sniff code and the value
* is the fully qualified name of the sniff class.
*
* @var array<string, string>
*/
private $deprecatedSniffs = [];


/**
* Initialise the ruleset that the run will use.
Expand Down Expand Up @@ -290,13 +301,161 @@ public function explain()
}
}//end if

if (isset($this->deprecatedSniffs[$sniff]) === true) {
$sniff .= ' *';
}

$sniffsInStandard[] = $sniff;
++$lastCount;
}//end foreach

if (count($this->deprecatedSniffs) > 0) {
echo PHP_EOL.'* Sniffs marked with an asterix are deprecated.'.PHP_EOL;
}

}//end explain()


/**
* Checks whether any deprecated sniffs were registered via the ruleset.
*
* @return bool
*/
public function hasSniffDeprecations()
{
return (count($this->deprecatedSniffs) > 0);

}//end hasSniffDeprecations()


/**
* Prints an information block about deprecated sniffs being used.
*
* @return void
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException When the interface implementation is faulty.
*/
public function showSniffDeprecations()
{
if ($this->hasSniffDeprecations() === false) {
return;
}

// Don't show deprecation notices in quiet mode, in explain mode
// or when the documentation is being shown.
// Documentation and explain will mark a sniff as deprecated natively
// and also call the Ruleset multiple times which would lead to duplicate
// display of the deprecation messages.
if ($this->config->quiet === true
|| $this->config->explain === true
|| $this->config->generator !== null
) {
return;
}

$reportWidth = $this->config->reportWidth;
// Message takes report width minus the leading dash + two spaces, minus a one space gutter at the end.
$maxMessageWidth = ($reportWidth - 4);
$maxActualWidth = 0;

ksort($this->deprecatedSniffs, (SORT_NATURAL | SORT_FLAG_CASE));

$messages = [];
$messageTemplate = 'This sniff has been deprecated since %s and will be removed in %s. %s';
$errorTemplate = 'The %s::%s() method must return a %sstring, received %s';

foreach ($this->deprecatedSniffs as $sniffCode => $className) {
if (isset($this->sniffs[$className]) === false) {
// Should only be possible in test situations, but some extra defensive coding is never a bad thing.
continue;
}

// Verify the interface was implemented correctly.
// Unfortunately can't be safeguarded via type declarations yet.
$deprecatedSince = $this->sniffs[$className]->getDeprecationVersion();
if (is_string($deprecatedSince) === false) {
throw new RuntimeException(
sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', gettype($deprecatedSince))
);
}

if ($deprecatedSince === '') {
throw new RuntimeException(
sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', '""')
);
}

$removedIn = $this->sniffs[$className]->getRemovalVersion();
if (is_string($removedIn) === false) {
throw new RuntimeException(
sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', gettype($removedIn))
);
}

if ($removedIn === '') {
throw new RuntimeException(
sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', '""')
);
}

$customMessage = $this->sniffs[$className]->getDeprecationMessage();
if (is_string($customMessage) === false) {
throw new RuntimeException(
sprintf($errorTemplate, $className, 'getDeprecationMessage', '', gettype($customMessage))
);
}

// Truncate the error code if there is not enough report width.
if (strlen($sniffCode) > $maxMessageWidth) {
$sniffCode = substr($sniffCode, 0, ($maxMessageWidth - 3)).'...';
}

$message = '- '.$sniffCode.PHP_EOL;
if ($this->config->colors === true) {
$message = '- '."\033[36m".$sniffCode."\033[0m".PHP_EOL;
}

$maxActualWidth = max($maxActualWidth, strlen($sniffCode));

// Normalize new line characters in custom message.
$customMessage = preg_replace('`\R`', PHP_EOL, $customMessage);

$notice = trim(sprintf($messageTemplate, $deprecatedSince, $removedIn, $customMessage));
$maxActualWidth = max($maxActualWidth, min(strlen($notice), $maxMessageWidth));
$wrapped = wordwrap($notice, $maxMessageWidth, PHP_EOL);
$message .= ' '.implode(PHP_EOL.' ', explode(PHP_EOL, $wrapped));

$messages[] = $message;
}//end foreach

if (count($messages) === 0) {
return;
}

$summaryLine = "WARNING: The $this->name standard uses 1 deprecated sniff";
$sniffCount = count($messages);
if ($sniffCount !== 1) {
$summaryLine = str_replace('1 deprecated sniff', "$sniffCount deprecated sniffs", $summaryLine);
}

$maxActualWidth = max($maxActualWidth, min(strlen($summaryLine), $maxMessageWidth));

$summaryLine = wordwrap($summaryLine, $reportWidth, PHP_EOL);
if ($this->config->colors === true) {
echo "\033[33m".$summaryLine."\033[0m".PHP_EOL;
} else {
echo $summaryLine.PHP_EOL;
}

echo str_repeat('-', min(($maxActualWidth + 4), $reportWidth)).PHP_EOL;
echo implode(PHP_EOL, $messages);

$closer = wordwrap('Deprecated sniffs are still run, but will stop working at some point in the future.', $reportWidth, PHP_EOL);
echo PHP_EOL.PHP_EOL.$closer.PHP_EOL.PHP_EOL;

}//end showSniffDeprecations()


/**
* Processes a single ruleset and returns a list of the sniffs it represents.
*
Expand Down Expand Up @@ -1225,6 +1384,10 @@ public function populateTokenListeners()
$sniffCode = Util\Common::getSniffCode($sniffClass);
$this->sniffCodes[$sniffCode] = $sniffClass;

if ($this->sniffs[$sniffClass] instanceof DeprecatedSniff) {
$this->deprecatedSniffs[$sniffCode] = $sniffClass;
}

// Set custom properties.
if (isset($this->ruleset[$sniffCode]['properties']) === true) {
foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) {
Expand Down
4 changes: 4 additions & 0 deletions src/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ public function init()
// should be checked and/or fixed.
try {
$this->ruleset = new Ruleset($this->config);

if ($this->ruleset->hasSniffDeprecations() === true) {
$this->ruleset->showSniffDeprecations();
}
} catch (RuntimeException $e) {
$error = 'ERROR: '.$e->getMessage().PHP_EOL.PHP_EOL;
$error .= $this->config->printShortUsage(true);
Expand Down
63 changes: 63 additions & 0 deletions src/Sniffs/DeprecatedSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* Marks a sniff as deprecated.
*
* Implementing this interface allows for marking a sniff as deprecated and
* displaying information about the deprecation to the end-user.
*
* A sniff will still need to implement the `PHP_CodeSniffer\Sniffs\Sniff` interface
* as well, or extend an abstract sniff which does, to be recognized as a valid sniff.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2024 PHPCSStandards Contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/

namespace PHP_CodeSniffer\Sniffs;

interface DeprecatedSniff
{


/**
* Provide the version number in which the sniff was deprecated.
*
* Recommended format for PHPCS native sniffs: "v3.3.0".
* Recommended format for external sniffs: "StandardName v3.3.0".
*
* @return string
*/
public function getDeprecationVersion();


/**
* Provide the version number in which the sniff will be removed.
*
* Recommended format for PHPCS native sniffs: "v3.3.0".
* Recommended format for external sniffs: "StandardName v3.3.0".
*
* If the removal version is not yet known, it is recommended to set
* this to: "a future version".
*
* @return string
*/
public function getRemovalVersion();


/**
* Optionally provide an arbitrary custom message to display with the deprecation.
*
* Typically intended to allow for displaying information about what to
* replace the deprecated sniff with.
* Example: "Use the Stnd.Cat.SniffName sniff instead."
* Multi-line messages (containing new line characters) are supported.
*
* An empty string can be returned if there is no replacement/no need
* for a custom message.
*
* @return string
*/
public function getDeprecationMessage();


}//end interface
42 changes: 42 additions & 0 deletions tests/Core/Ruleset/ExplainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,48 @@ public function testExplainCustomRuleset()
}//end testExplainCustomRuleset()


/**
* Test the output of the "explain" command for a standard containing both deprecated
* and non-deprecated sniffs.
*
* Tests that:
* - Deprecated sniffs are marked with an asterix in the list.
* - A footnote is displayed explaining the asterix.
* - And that the "standard uses # deprecated sniffs" listing is **not** displayed.
*
* @return void
*/
public function testExplainWithDeprecatedSniffs()
{
// Set up the ruleset.
$standard = __DIR__."/ShowSniffDeprecationsTest.xml";
$config = new ConfigDouble(["--standard=$standard", '-e']);
$ruleset = new Ruleset($config);

$expected = PHP_EOL;
$expected .= 'The SniffDeprecationTest standard contains 9 sniffs'.PHP_EOL.PHP_EOL;

$expected .= 'Fixtures (9 sniffs)'.PHP_EOL;
$expected .= '-------------------'.PHP_EOL;
$expected .= ' Fixtures.Deprecated.WithLongReplacement *'.PHP_EOL;
$expected .= ' Fixtures.Deprecated.WithoutReplacement *'.PHP_EOL;
$expected .= ' Fixtures.Deprecated.WithReplacement *'.PHP_EOL;
$expected .= ' Fixtures.Deprecated.WithReplacementContainingLinuxNewlines *'.PHP_EOL;
$expected .= ' Fixtures.Deprecated.WithReplacementContainingNewlines *'.PHP_EOL;
$expected .= ' Fixtures.SetProperty.AllowedAsDeclared'.PHP_EOL;
$expected .= ' Fixtures.SetProperty.AllowedViaMagicMethod'.PHP_EOL;
$expected .= ' Fixtures.SetProperty.AllowedViaStdClass'.PHP_EOL;
$expected .= ' Fixtures.SetProperty.NotAllowedViaAttribute'.PHP_EOL.PHP_EOL;

$expected .= '* Sniffs marked with an asterix are deprecated.'.PHP_EOL;

$this->expectOutputString($expected);

$ruleset->explain();

}//end testExplainWithDeprecatedSniffs()


/**
* Test that each standard passed on the command-line is explained separately.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Test fixture.
*
* @see \PHP_CodeSniffer\Tests\Core\Ruleset\SniffDeprecationTest
*/

namespace Fixtures\Sniffs\Deprecated;

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

class WithLongReplacementSniff implements Sniff,DeprecatedSniff
{

public function getDeprecationVersion()
{
return 'v3.8.0';
}

public function getRemovalVersion()
{
return 'v4.0.0';
}

public function getDeprecationMessage()
{
return 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vel vestibulum nunc. Sed luctus dolor tortor, eu euismod purus pretium sed. Fusce egestas congue massa semper cursus. Donec quis pretium tellus. In lacinia, augue ut ornare porttitor, diam nunc faucibus purus, et accumsan eros sapien at sem. Sed pulvinar aliquam malesuada. Aliquam erat volutpat. Mauris gravida rutrum lectus at egestas. Fusce tempus elit in tincidunt dictum. Suspendisse dictum egestas sapien, eget ullamcorper metus elementum semper. Vestibulum sem justo, consectetur ac tincidunt et, finibus eget libero.';
}

public function register()
{
return [T_WHITESPACE];
}

public function process(File $phpcsFile, $stackPtr)
{
// Do something.
}
}