Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions PHPCompatibility/Docs/Classes/NewFinalClassConstantsStandard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<documentation title="New final class constants">
<standard>
<![CDATA[
Class constants can be declared as final since PHP 8.1.
]]>
</standard>
<code_comparison>
<code title="Cross-version compatible: not using the final modifier.">
<![CDATA[
class Foo {
const BAR = 10;
}
]]>
</code>
<code title="PHP >= 8.1: using the final modifier.">
<![CDATA[
class Foo {
<em>final</em> const BAR = 10;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTML tag appears literally in markdown docs. I recommend avoiding markdown in code samples. Other than that, everything looks good on the docs side.

Suggested change
<em>final</em> const BAR = 10;
final const BAR = 10;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The <em> markers are intended to highlight the "issue" in the HTML view.
In the Text and the MarkDown view, they are automatically removed when the report is generated.

How did you generate the report to get them to show ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked the source code of the generators which are included with PHPCS and the above is correct. Also tested with the docs for another sniff I was just working on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the generator code I mean: https://github.com/squizlabs/PHP_CodeSniffer/blob/51335eb46b2b940b6c429643fe96f514d4a4e4a1/src/Generators/Markdown.php#L131-L141

If the code in the phpcs docs repo does not line up with that, it will need to be adjusted.

}
]]>
</code>
</code_comparison>
</documentation>
80 changes: 80 additions & 0 deletions PHPCompatibility/Sniffs/Classes/NewFinalClassConstantsSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
/**
* PHPCompatibility, an external standard for PHP_CodeSniffer.
*
* @package PHPCompatibility
* @copyright 2012-2022 PHPCompatibility Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCompatibility/PHPCompatibility
*/

namespace PHPCompatibility\Sniffs\Classes;

use PHPCompatibility\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\Scopes;

/**
* Using the "final" modifier for class constants is available since PHP 8.1.
*
* PHP version 8.1
*
* @link https://wiki.php.net/rfc/final_class_const
* @link https://www.php.net/manual/en/language.oop5.final.php#language.oop5.final.example.php81
*
* @since 10.0.0
*/
class NewFinalClassConstantsSniff extends Sniff
{

/**
* Returns an array of tokens this test wants to listen for.
*
* @since 10.0.0
*
* @return array
*/
public function register()
{
return [\T_CONST];
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @since 10.0.0
*
* @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)
{
if ($this->supportsBelow('8.0') === false) {
return;
}

// Is this a class constant ?
if (Scopes::isOOConstant($phpcsFile, $stackPtr) === false) {
return;
}

$tokens = $phpcsFile->getTokens();
$skip = Tokens::$emptyTokens + Tokens::$scopeModifiers;
$prevToken = $phpcsFile->findPrevious($skip, ($stackPtr - 1), null, true, null, true);

// Is the previous token the final keyword ?
if ($prevToken === false || $tokens[$prevToken]['code'] !== \T_FINAL) {
return;
}

$phpcsFile->addError(
'The final modifier for class constants is not supported in PHP 8.0 or earlier.',
$stackPtr,
'Found'
);
}
}
68 changes: 68 additions & 0 deletions PHPCompatibility/Tests/Classes/NewFinalClassConstantsUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

const NONCLASSCONST = 'foo';

class ConstDemo
{
const MY_CONST = 1;

// PHP 8.1+.
final const FINAL_A = 2;
final public const FINAL_PUBLIC_CONST_A = 3;
public final const FINAL_PUBLIC_CONST_B = 4;
final protected const FINAL_PROTECTED_CONST_A = 5;
protected final const FINAL_PROTECTED_CONST_B = 6;

// Fatal error as final with private is an oxymoron.
final private const FINAL_PRIVATE_CONST_A = 7;
private final const FINAL_PRIVATE_CONST_B = 8;
}

interface InterfaceDemo
{
const MY_CONST = 1;

// PHP 8.1+.
final const FINAL_A = 2;
final public /*comment*/ const FINAL_PUBLIC_CONST_A = 3;
public final const FINAL_PUBLIC_CONST_B = 4;

// Fatal error as interface constants must be public, but the check for which visibility indicator is used is outside the scope of this sniff.
final /*comment*/ protected const FINAL_PROTECTED_CONST_A = 5;
protected final const FINAL_PROTECTED_CONST_B = 6;

// Fatal error as final with private is an oxymoron.
final private const FINAL_PRIVATE_CONST_A = 7;
private final const FINAL_PRIVATE_CONST_B = 8;
}

// Test anonymous classes.
$a = new class
{
const MY_CONST = 1;

// PHP 8.1+.
final const FINAL_A = 2;
final public const FINAL_PUBLIC_CONST_A = 3;
public final const FINAL_PUBLIC_CONST_B = 4;
final protected const FINAL_PROTECTED_CONST_A = 5;
protected final const FINAL_PROTECTED_CONST_B = 6;

// Fatal error as final with private is an oxymoron.
final private const FINAL_PRIVATE_CONST_A = 7;
private final const FINAL_PRIVATE_CONST_B = 8;
};

/*
* Test against some false positives.
*
* Constants defined in the global namespace can not have the final modifier,
* but this is outside the scope of this library. Would cause a parse error anyway.
*/
final const GLOBAL_CONSTANT = 'not valid';

class NotAClassConstant {
public function something() {
final const GLOBAL_CONSTANT = 'not valid';
}
}
125 changes: 125 additions & 0 deletions PHPCompatibility/Tests/Classes/NewFinalClassConstantsUnitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
/**
* PHPCompatibility, an external standard for PHP_CodeSniffer.
*
* @package PHPCompatibility
* @copyright 2012-2022 PHPCompatibility Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCompatibility/PHPCompatibility
*/

namespace PHPCompatibility\Tests\Classes;

use PHPCompatibility\Tests\BaseSniffTest;

/**
* Test the NewFinalClassConstants sniff.
*
* @group newFinalClassConstants
* @group classes
*
* @covers \PHPCompatibility\Sniffs\Classes\NewFinalClassConstantsSniff
*
* @since 10.0.0
*/
class NewFinalClassConstantsUnitTest extends BaseSniffTest
{

/**
* Test that an error is thrown for class constants declared as final.
*
* @dataProvider dataFinalClassConstants
*
* @param int $line The line number.
*
* @return void
*/
public function testFinalClassConstants($line)
{
$file = $this->sniffFile(__FILE__, '8.0');
$this->assertError($file, $line, 'The final modifier for class constants is not supported in PHP 8.0 or earlier.');
}

/**
* Data provider.
*
* @see testFinalClassConstants()
*
* @return array
*/
public function dataFinalClassConstants()
{
return [
[10],
[11],
[12],
[13],
[14],
[17],
[18],

[26],
[27],
[28],
[31],
[32],
[35],
[36],

[45],
[46],
[47],
[48],
[49],
[52],
[53],
];
}


/**
* Verify that there are no false positives for valid code/code errors outside the scope of this sniff.
*
* @dataProvider dataNoFalsePositives
*
* @param int $line The line number.
*
* @return void
*/
public function testNoFalsePositives($line)
{
$file = $this->sniffFile(__FILE__, '8.0');
$this->assertNoViolation($file, $line);
}

/**
* Data provider.
*
* @see testNoFalsePositives()
*
* @return array
*/
public function dataNoFalsePositives()
{
return [
[3],
[7],
[23],
[42],
[62],
[66],
];
}


/**
* Verify no notices are thrown at all.
*
* @return void
*/
public function testNoViolationsInFileOnValidVersion()
{
$file = $this->sniffFile(__FILE__, '8.1');
$this->assertNoViolation($file);
}
}