Skip to content

Commit 905c6fb

Browse files
authored
Merge pull request #281 from PHPCSStandards/feature/164-handle-sniff-deprecation-natively
✨ Native handling of sniff deprecations
2 parents e0bb06c + 3fb0d95 commit 905c6fb

25 files changed

+1274
-0
lines changed

phpunit.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.2/phpunit.xsd"
55
backupGlobals="true"
6+
beStrictAboutOutputDuringTests="true"
67
beStrictAboutTestsThatDoNotTestAnything="false"
78
bootstrap="tests/bootstrap.php"
89
convertErrorsToExceptions="true"

src/Ruleset.php

+163
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace PHP_CodeSniffer;
1313

1414
use PHP_CodeSniffer\Exceptions\RuntimeException;
15+
use PHP_CodeSniffer\Sniffs\DeprecatedSniff;
1516
use PHP_CodeSniffer\Util;
1617
use stdClass;
1718

@@ -116,6 +117,16 @@ class Ruleset
116117
*/
117118
private $config = null;
118119

120+
/**
121+
* An array of the names of sniffs which have been marked as deprecated.
122+
*
123+
* The key is the sniff code and the value
124+
* is the fully qualified name of the sniff class.
125+
*
126+
* @var array<string, string>
127+
*/
128+
private $deprecatedSniffs = [];
129+
119130

120131
/**
121132
* Initialise the ruleset that the run will use.
@@ -290,13 +301,161 @@ public function explain()
290301
}
291302
}//end if
292303

304+
if (isset($this->deprecatedSniffs[$sniff]) === true) {
305+
$sniff .= ' *';
306+
}
307+
293308
$sniffsInStandard[] = $sniff;
294309
++$lastCount;
295310
}//end foreach
296311

312+
if (count($this->deprecatedSniffs) > 0) {
313+
echo PHP_EOL.'* Sniffs marked with an asterix are deprecated.'.PHP_EOL;
314+
}
315+
297316
}//end explain()
298317

299318

319+
/**
320+
* Checks whether any deprecated sniffs were registered via the ruleset.
321+
*
322+
* @return bool
323+
*/
324+
public function hasSniffDeprecations()
325+
{
326+
return (count($this->deprecatedSniffs) > 0);
327+
328+
}//end hasSniffDeprecations()
329+
330+
331+
/**
332+
* Prints an information block about deprecated sniffs being used.
333+
*
334+
* @return void
335+
*
336+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException When the interface implementation is faulty.
337+
*/
338+
public function showSniffDeprecations()
339+
{
340+
if ($this->hasSniffDeprecations() === false) {
341+
return;
342+
}
343+
344+
// Don't show deprecation notices in quiet mode, in explain mode
345+
// or when the documentation is being shown.
346+
// Documentation and explain will mark a sniff as deprecated natively
347+
// and also call the Ruleset multiple times which would lead to duplicate
348+
// display of the deprecation messages.
349+
if ($this->config->quiet === true
350+
|| $this->config->explain === true
351+
|| $this->config->generator !== null
352+
) {
353+
return;
354+
}
355+
356+
$reportWidth = $this->config->reportWidth;
357+
// Message takes report width minus the leading dash + two spaces, minus a one space gutter at the end.
358+
$maxMessageWidth = ($reportWidth - 4);
359+
$maxActualWidth = 0;
360+
361+
ksort($this->deprecatedSniffs, (SORT_NATURAL | SORT_FLAG_CASE));
362+
363+
$messages = [];
364+
$messageTemplate = 'This sniff has been deprecated since %s and will be removed in %s. %s';
365+
$errorTemplate = 'The %s::%s() method must return a %sstring, received %s';
366+
367+
foreach ($this->deprecatedSniffs as $sniffCode => $className) {
368+
if (isset($this->sniffs[$className]) === false) {
369+
// Should only be possible in test situations, but some extra defensive coding is never a bad thing.
370+
continue;
371+
}
372+
373+
// Verify the interface was implemented correctly.
374+
// Unfortunately can't be safeguarded via type declarations yet.
375+
$deprecatedSince = $this->sniffs[$className]->getDeprecationVersion();
376+
if (is_string($deprecatedSince) === false) {
377+
throw new RuntimeException(
378+
sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', gettype($deprecatedSince))
379+
);
380+
}
381+
382+
if ($deprecatedSince === '') {
383+
throw new RuntimeException(
384+
sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', '""')
385+
);
386+
}
387+
388+
$removedIn = $this->sniffs[$className]->getRemovalVersion();
389+
if (is_string($removedIn) === false) {
390+
throw new RuntimeException(
391+
sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', gettype($removedIn))
392+
);
393+
}
394+
395+
if ($removedIn === '') {
396+
throw new RuntimeException(
397+
sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', '""')
398+
);
399+
}
400+
401+
$customMessage = $this->sniffs[$className]->getDeprecationMessage();
402+
if (is_string($customMessage) === false) {
403+
throw new RuntimeException(
404+
sprintf($errorTemplate, $className, 'getDeprecationMessage', '', gettype($customMessage))
405+
);
406+
}
407+
408+
// Truncate the error code if there is not enough report width.
409+
if (strlen($sniffCode) > $maxMessageWidth) {
410+
$sniffCode = substr($sniffCode, 0, ($maxMessageWidth - 3)).'...';
411+
}
412+
413+
$message = '- '.$sniffCode.PHP_EOL;
414+
if ($this->config->colors === true) {
415+
$message = '- '."\033[36m".$sniffCode."\033[0m".PHP_EOL;
416+
}
417+
418+
$maxActualWidth = max($maxActualWidth, strlen($sniffCode));
419+
420+
// Normalize new line characters in custom message.
421+
$customMessage = preg_replace('`\R`', PHP_EOL, $customMessage);
422+
423+
$notice = trim(sprintf($messageTemplate, $deprecatedSince, $removedIn, $customMessage));
424+
$maxActualWidth = max($maxActualWidth, min(strlen($notice), $maxMessageWidth));
425+
$wrapped = wordwrap($notice, $maxMessageWidth, PHP_EOL);
426+
$message .= ' '.implode(PHP_EOL.' ', explode(PHP_EOL, $wrapped));
427+
428+
$messages[] = $message;
429+
}//end foreach
430+
431+
if (count($messages) === 0) {
432+
return;
433+
}
434+
435+
$summaryLine = "WARNING: The $this->name standard uses 1 deprecated sniff";
436+
$sniffCount = count($messages);
437+
if ($sniffCount !== 1) {
438+
$summaryLine = str_replace('1 deprecated sniff', "$sniffCount deprecated sniffs", $summaryLine);
439+
}
440+
441+
$maxActualWidth = max($maxActualWidth, min(strlen($summaryLine), $maxMessageWidth));
442+
443+
$summaryLine = wordwrap($summaryLine, $reportWidth, PHP_EOL);
444+
if ($this->config->colors === true) {
445+
echo "\033[33m".$summaryLine."\033[0m".PHP_EOL;
446+
} else {
447+
echo $summaryLine.PHP_EOL;
448+
}
449+
450+
echo str_repeat('-', min(($maxActualWidth + 4), $reportWidth)).PHP_EOL;
451+
echo implode(PHP_EOL, $messages);
452+
453+
$closer = wordwrap('Deprecated sniffs are still run, but will stop working at some point in the future.', $reportWidth, PHP_EOL);
454+
echo PHP_EOL.PHP_EOL.$closer.PHP_EOL.PHP_EOL;
455+
456+
}//end showSniffDeprecations()
457+
458+
300459
/**
301460
* Processes a single ruleset and returns a list of the sniffs it represents.
302461
*
@@ -1225,6 +1384,10 @@ public function populateTokenListeners()
12251384
$sniffCode = Util\Common::getSniffCode($sniffClass);
12261385
$this->sniffCodes[$sniffCode] = $sniffClass;
12271386

1387+
if ($this->sniffs[$sniffClass] instanceof DeprecatedSniff) {
1388+
$this->deprecatedSniffs[$sniffCode] = $sniffClass;
1389+
}
1390+
12281391
// Set custom properties.
12291392
if (isset($this->ruleset[$sniffCode]['properties']) === true) {
12301393
foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) {

src/Runner.php

+4
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ public function init()
334334
// should be checked and/or fixed.
335335
try {
336336
$this->ruleset = new Ruleset($this->config);
337+
338+
if ($this->ruleset->hasSniffDeprecations() === true) {
339+
$this->ruleset->showSniffDeprecations();
340+
}
337341
} catch (RuntimeException $e) {
338342
$error = 'ERROR: '.$e->getMessage().PHP_EOL.PHP_EOL;
339343
$error .= $this->config->printShortUsage(true);

src/Sniffs/DeprecatedSniff.php

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Marks a sniff as deprecated.
4+
*
5+
* Implementing this interface allows for marking a sniff as deprecated and
6+
* displaying information about the deprecation to the end-user.
7+
*
8+
* A sniff will still need to implement the `PHP_CodeSniffer\Sniffs\Sniff` interface
9+
* as well, or extend an abstract sniff which does, to be recognized as a valid sniff.
10+
*
11+
* @author Juliette Reinders Folmer <[email protected]>
12+
* @copyright 2024 PHPCSStandards Contributors
13+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
14+
*/
15+
16+
namespace PHP_CodeSniffer\Sniffs;
17+
18+
interface DeprecatedSniff
19+
{
20+
21+
22+
/**
23+
* Provide the version number in which the sniff was deprecated.
24+
*
25+
* Recommended format for PHPCS native sniffs: "v3.3.0".
26+
* Recommended format for external sniffs: "StandardName v3.3.0".
27+
*
28+
* @return string
29+
*/
30+
public function getDeprecationVersion();
31+
32+
33+
/**
34+
* Provide the version number in which the sniff will be removed.
35+
*
36+
* Recommended format for PHPCS native sniffs: "v3.3.0".
37+
* Recommended format for external sniffs: "StandardName v3.3.0".
38+
*
39+
* If the removal version is not yet known, it is recommended to set
40+
* this to: "a future version".
41+
*
42+
* @return string
43+
*/
44+
public function getRemovalVersion();
45+
46+
47+
/**
48+
* Optionally provide an arbitrary custom message to display with the deprecation.
49+
*
50+
* Typically intended to allow for displaying information about what to
51+
* replace the deprecated sniff with.
52+
* Example: "Use the Stnd.Cat.SniffName sniff instead."
53+
* Multi-line messages (containing new line characters) are supported.
54+
*
55+
* An empty string can be returned if there is no replacement/no need
56+
* for a custom message.
57+
*
58+
* @return string
59+
*/
60+
public function getDeprecationMessage();
61+
62+
63+
}//end interface

tests/Core/Ruleset/ExplainTest.php

+42
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,48 @@ public function testExplainCustomRuleset()
166166
}//end testExplainCustomRuleset()
167167

168168

169+
/**
170+
* Test the output of the "explain" command for a standard containing both deprecated
171+
* and non-deprecated sniffs.
172+
*
173+
* Tests that:
174+
* - Deprecated sniffs are marked with an asterix in the list.
175+
* - A footnote is displayed explaining the asterix.
176+
* - And that the "standard uses # deprecated sniffs" listing is **not** displayed.
177+
*
178+
* @return void
179+
*/
180+
public function testExplainWithDeprecatedSniffs()
181+
{
182+
// Set up the ruleset.
183+
$standard = __DIR__."/ShowSniffDeprecationsTest.xml";
184+
$config = new ConfigDouble(["--standard=$standard", '-e']);
185+
$ruleset = new Ruleset($config);
186+
187+
$expected = PHP_EOL;
188+
$expected .= 'The SniffDeprecationTest standard contains 9 sniffs'.PHP_EOL.PHP_EOL;
189+
190+
$expected .= 'Fixtures (9 sniffs)'.PHP_EOL;
191+
$expected .= '-------------------'.PHP_EOL;
192+
$expected .= ' Fixtures.Deprecated.WithLongReplacement *'.PHP_EOL;
193+
$expected .= ' Fixtures.Deprecated.WithoutReplacement *'.PHP_EOL;
194+
$expected .= ' Fixtures.Deprecated.WithReplacement *'.PHP_EOL;
195+
$expected .= ' Fixtures.Deprecated.WithReplacementContainingLinuxNewlines *'.PHP_EOL;
196+
$expected .= ' Fixtures.Deprecated.WithReplacementContainingNewlines *'.PHP_EOL;
197+
$expected .= ' Fixtures.SetProperty.AllowedAsDeclared'.PHP_EOL;
198+
$expected .= ' Fixtures.SetProperty.AllowedViaMagicMethod'.PHP_EOL;
199+
$expected .= ' Fixtures.SetProperty.AllowedViaStdClass'.PHP_EOL;
200+
$expected .= ' Fixtures.SetProperty.NotAllowedViaAttribute'.PHP_EOL.PHP_EOL;
201+
202+
$expected .= '* Sniffs marked with an asterix are deprecated.'.PHP_EOL;
203+
204+
$this->expectOutputString($expected);
205+
206+
$ruleset->explain();
207+
208+
}//end testExplainWithDeprecatedSniffs()
209+
210+
169211
/**
170212
* Test that each standard passed on the command-line is explained separately.
171213
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Test fixture.
4+
*
5+
* @see \PHP_CodeSniffer\Tests\Core\Ruleset\SniffDeprecationTest
6+
*/
7+
8+
namespace Fixtures\Sniffs\Deprecated;
9+
10+
use PHP_CodeSniffer\Files\File;
11+
use PHP_CodeSniffer\Sniffs\DeprecatedSniff;
12+
use PHP_CodeSniffer\Sniffs\Sniff;
13+
14+
class WithLongReplacementSniff implements Sniff,DeprecatedSniff
15+
{
16+
17+
public function getDeprecationVersion()
18+
{
19+
return 'v3.8.0';
20+
}
21+
22+
public function getRemovalVersion()
23+
{
24+
return 'v4.0.0';
25+
}
26+
27+
public function getDeprecationMessage()
28+
{
29+
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.';
30+
}
31+
32+
public function register()
33+
{
34+
return [T_WHITESPACE];
35+
}
36+
37+
public function process(File $phpcsFile, $stackPtr)
38+
{
39+
// Do something.
40+
}
41+
}

0 commit comments

Comments
 (0)