Skip to content

Commit 2507c78

Browse files
committed
✨ Native handling of sniff deprecations
As per 164, this commit introduces a new feature to PHPCS: native handling of sniff deprecations. ### Background There are quite a few sniffs slated for removal in PHPCS 4.0 - 45 so far, to be exact - and save for two sniffs which have received a deprecation mention in the changelogs, this has only been announced in issues (squizlabs/PHP_CodeSniffer 2448 + squizlabs/PHP_CodeSniffer 2471) in the Squizlabs repo and should therefore only be considered "known" to a very small group of people. Increasing awareness of the upcoming sniff removals should allow for a smoother upgrade experience to PHPCS 4.0 for end-users. Aside from use by PHPCS itself, this feature can also be used by external standards to signal sniff deprecations to _their_ end-users. All in all, this feature should hopefully improve the end-user experience. ### Policy for use of this feature in PHP_CodeSniffer itself As per the notes in 188, the intention is for PHPCS itself to use this feature as follows: * Soft deprecate (changelog notice and `@deprecated` tag in sniff) sniffs during the lifetime of a major. * Hard deprecate sniffs, i.e. implement the `DeprecatedSniff` interface, in the last minor before the next major release. External standards are, of course, free to apply a different deprecation policy. ### Implementation details This commit introduces: #### A new `PHP_CodeSniffer\Sniffs\DeprecatedSniff` interface This interface enforces the implementation of three new methods: * `getDeprecationVersion(): string` - the return value should be a non-empty string with the version in which the sniff was deprecated. * `getRemovalVersion(): string` - the return value should be a non-empty string with the version in which the sniff will be removed. If the removal version is not yet known, it is recommended to set this to: "a future version". * `getDeprecationMessage(): string` - the return value allows for an arbitrary message to be added, such as a replacement recommendation. If no additional information needs to be conveyed to end-users, an empty string can be returned. Custom messages are allowed to contain new lines. #### Changes to the `Ruleset` class to allow for showing the deprecation notices The Ruleset class contains two new methods: * `hasSniffDeprecations(): bool` to allow for checking whether a loading ruleset includes deprecated sniffs. * `showSniffDeprecations(): void` to display the applicable deprecation notices. The implementation for showing the sniff deprecation notices is as follows: * No notices will be shown when PHPCS is running with the `-q` flag (quite mode). * No notices will be shown when PHPCS was giving the `-e` flag to "explain" a standard (list all sniffs). * No notices will be shown when PHPCS was asked to display documentation using the `--generator=...` CLI argument. * No notices will be shown when PHPCS is asked for information which doesn't involve loading a ruleset, such as running `phpcs` with the any of the following flags: `-i` (listing installed standards), `--version`, `--help`, `--config-show`, `--config-set`, `--config-delete`. * Only deprecation notices will be shown for _active_ sniffs. This means that when `--exclude=...` is used and deprecated sniffs are excluded, no notices will be shown for the excluded sniffs. It also means that when `--sniffs=...` is used, deprecation notices will only be shown for those sniffs selected (if deprecated). * The deprecation notices have no impact on the PHPCS (or PHPCBF) run itself. - Deprecated sniffs will still be loaded and run. - Properties can be set on deprecated sniffs. - The exit codes of runs are not affected by whether or not a ruleset contains deprecated sniffs. * As things are, deprecation notices will show both for `phpcs` as well as `phpcbf` runs. I did considered silencing them by default for `phpcbf`. For now, however, I've decided against this as the whole point of showing deprecation notices is to increase awareness of upcoming changes and as a subsection of the PHPCS users only run `phpcbf` and rarely `phpcs`, silencing the notices for `phpcbf` could be counter-productive. Additional implementation notes: * Any user set `--report-width` will be respected. This includes when the `report-width` is set to `auto`. * New lines in custom messages will be normalized to be suitable for the OS of the end-user. * The output will have select colourization if `--colors` is turned on. * The deprecation notices will not be included in generated reports saved to file using `--report-file=...` (and variants thereof). * If the interface is implemented incorrectly, a `PHP_CodeSniffer\Exceptions\RuntimeException` will be thrown. The intention is to add return type declarations to the interface in the future. The complete new feature is extensively covered by tests. Related issues: 188, 276 Fixes 164
1 parent 16b613d commit 2507c78

23 files changed

+1223
-0
lines changed

src/Ruleset.php

+155
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.
@@ -297,6 +308,146 @@ public function explain()
297308
}//end explain()
298309

299310

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

1379+
if ($this->sniffs[$sniffClass] instanceof DeprecatedSniff) {
1380+
$this->deprecatedSniffs[$sniffCode] = $sniffClass;
1381+
}
1382+
12281383
// Set custom properties.
12291384
if (isset($this->ruleset[$sniffCode]['properties']) === true) {
12301385
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
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 WithReplacementContainingLinuxNewlinesSniff 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.\n"
30+
."Fusce vel vestibulum nunc. Sed luctus dolor tortor, eu euismod purus pretium sed.\n"
31+
."Fusce egestas congue massa semper cursus. Donec quis pretium tellus.\n"
32+
."In lacinia, augue ut ornare porttitor, diam nunc faucibus purus, et accumsan eros sapien at sem.\n"
33+
.'Sed pulvinar aliquam malesuada. Aliquam erat volutpat. Mauris gravida rutrum lectus at egestas.';
34+
}
35+
36+
public function register()
37+
{
38+
return [T_WHITESPACE];
39+
}
40+
41+
public function process(File $phpcsFile, $stackPtr)
42+
{
43+
// Do something.
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 WithReplacementContainingNewlinesSniff 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.'.PHP_EOL
30+
.'Fusce vel vestibulum nunc. Sed luctus dolor tortor, eu euismod purus pretium sed.'.PHP_EOL
31+
.'Fusce egestas congue massa semper cursus. Donec quis pretium tellus.'.PHP_EOL
32+
.'In lacinia, augue ut ornare porttitor, diam nunc faucibus purus, et accumsan eros sapien at sem.'.PHP_EOL
33+
.'Sed pulvinar aliquam malesuada. Aliquam erat volutpat. Mauris gravida rutrum lectus at egestas';
34+
}
35+
36+
public function register()
37+
{
38+
return [T_WHITESPACE];
39+
}
40+
41+
public function process(File $phpcsFile, $stackPtr)
42+
{
43+
// Do something.
44+
}
45+
}

0 commit comments

Comments
 (0)