Skip to content

Commit 4ecde16

Browse files
committed
PHP 8.2 | New PHPCompatibility.Classes.NewReadonlyClasses sniff
> Readonly Classes > > Support for readonly on classes has been added. This adds a new sniff to detect this. Includes unit tests. Includes docs. Refs: * https://wiki.php.net/rfc/readonly_classes * https://www.php.net/manual/en/migration82.new-features.php#migration82.new-features.core.readonly-classes * https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class.readonly * php/php-src#7305
1 parent 924ed9b commit 4ecde16

File tree

4 files changed

+262
-0
lines changed

4 files changed

+262
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0"?>
2+
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
4+
title="New Readonly Classes"
5+
>
6+
<standard>
7+
<![CDATA[
8+
Declaring classes as readonly is supported since PHP 8.2.
9+
10+
Using the `readonly` keyword on a class declaration will make all properties declared in the class readonly and will forbid declaring dynamic properties on the class.
11+
Note: static properties or properties without type declaration are not supported.
12+
]]>
13+
</standard>
14+
<code_comparison>
15+
<code title="Cross-version compatible: class without the readonly keyword.">
16+
<![CDATA[
17+
class MyClass {}
18+
final class MyFinalClass {}
19+
abstract class MyAbstractClass {}
20+
]]>
21+
</code>
22+
<code title="PHP &gt;= 8.2: class using the readonly keyword.">
23+
<![CDATA[
24+
readonly class MyClass {}
25+
final readonly class MyFinalClass {}
26+
readonly abstract class MyAbstractClass {}
27+
]]>
28+
</code>
29+
</code_comparison>
30+
</documentation>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
/**
3+
* PHPCompatibility, an external standard for PHP_CodeSniffer.
4+
*
5+
* @package PHPCompatibility
6+
* @copyright 2012-2020 PHPCompatibility Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCompatibility/PHPCompatibility
9+
*/
10+
11+
namespace PHPCompatibility\Sniffs\Classes;
12+
13+
use PHPCompatibility\Sniff;
14+
use PHP_CodeSniffer\Files\File;
15+
use PHPCSUtils\Utils\ObjectDeclarations;
16+
17+
/**
18+
* Declaring classes as readonly is supported since PHP 8.2.
19+
*
20+
* PHP version 8.2
21+
*
22+
* @link https://wiki.php.net/rfc/readonly_classes
23+
* @link https://www.php.net/manual/en/migration82.new-features.php#migration82.new-features.core.readonly-classes
24+
* @link https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class.readonly
25+
*
26+
* @since 10.0.0
27+
*/
28+
final class NewReadonlyClassesSniff extends Sniff
29+
{
30+
31+
/**
32+
* Returns an array of tokens this test wants to listen for.
33+
*
34+
* @since 10.0.0
35+
*
36+
* @return array
37+
*/
38+
public function register()
39+
{
40+
return [\T_CLASS];
41+
}
42+
43+
/**
44+
* Processes this test, when one of its tokens is encountered.
45+
*
46+
* @since 10.0.0
47+
*
48+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
49+
* @param int $stackPtr The position of the current token
50+
* in the stack passed in $tokens.
51+
*
52+
* @return void
53+
*/
54+
public function process(File $phpcsFile, $stackPtr)
55+
{
56+
if ($this->supportsBelow('8.1') === false) {
57+
return;
58+
}
59+
60+
$properties = ObjectDeclarations::getClassProperties($phpcsFile, $stackPtr);
61+
if ($properties['is_readonly'] === false) {
62+
return;
63+
}
64+
65+
$phpcsFile->addError(
66+
'Readonly classes are not supported in PHP 8.1 or earlier.',
67+
$properties['readonly_token'],
68+
'Found'
69+
);
70+
}
71+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/**
4+
* Valid cross-version/not our targets.
5+
*/
6+
7+
class ClassWithoutKeywords {}
8+
9+
final class FinalClass {}
10+
11+
abstract class AbstractClass {
12+
function class() {}
13+
}
14+
15+
// Anon classes do not take keywords (and are not mentioned in the RFC).
16+
$anon = new class () {};
17+
18+
/**
19+
* PHP 8.2+: readonly classes.
20+
*/
21+
readonly class MyReadonlyClass {
22+
public string $foo;
23+
24+
// Untyped properties are not allowed in readonly classes, but that's not the concern of this sniff.
25+
public $bar;
26+
27+
// Static properties are not allowed in readonly classes, but that's not the concern of this sniff.
28+
public static int $baz;
29+
}
30+
31+
abstract readonly class AbstractReadonly {}
32+
readonly abstract class ReadonlyAbstract {}
33+
34+
final readonly class FinalReadonly {}
35+
readonly final class ReadonlyFinal {}
36+
37+
// Extending readonly classes is allowed, as long as the child is also marked readonly.
38+
readonly class ChildClass extends ParentClass {}
39+
40+
// The AllowDynamicProperties attribute is not allowed on readonly classes, but that's not the concern of this sniff.
41+
#[AllowDynamicProperties]
42+
readonly class DynamicPropertiesNotAllowed {}
43+
44+
// Verify handling with superfluous whitespace and comments.
45+
readonly
46+
// Comment
47+
abstract
48+
class
49+
ClassName {}
50+
51+
// Duplicate readonly modifier is not allowed, but that's not the concern of this sniff.
52+
readonly readonly class DoubleReadonly {}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
/**
3+
* PHPCompatibility, an external standard for PHP_CodeSniffer.
4+
*
5+
* @package PHPCompatibility
6+
* @copyright 2012-2020 PHPCompatibility Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCompatibility/PHPCompatibility
9+
*/
10+
11+
namespace PHPCompatibility\Tests\Classes;
12+
13+
use PHPCompatibility\Tests\BaseSniffTest;
14+
15+
/**
16+
* Test the NewReadonlyClasses sniff.
17+
*
18+
* @group newReadonlyClasses
19+
* @group constants
20+
*
21+
* @covers \PHPCompatibility\Sniffs\Classes\NewReadonlyClassesSniff
22+
*
23+
* @since 10.0.0
24+
*/
25+
final class NewReadonlyClassesUnitTest extends BaseSniffTest
26+
{
27+
28+
/**
29+
* Test that an error is thrown for class constants declared with visibility.
30+
*
31+
* @dataProvider dataReadonlyClass
32+
*
33+
* @param int $line The line number.
34+
*
35+
* @return void
36+
*/
37+
public function testReadonlyClass($line)
38+
{
39+
$file = $this->sniffFile(__FILE__, '8.1');
40+
$this->assertError($file, $line, 'Readonly classes are not supported in PHP 8.1 or earlier.');
41+
}
42+
43+
/**
44+
* Data provider.
45+
*
46+
* @see testReadonlyClass()
47+
*
48+
* @return array
49+
*/
50+
public function dataReadonlyClass()
51+
{
52+
return [
53+
[21],
54+
[31],
55+
[32],
56+
[34],
57+
[35],
58+
[38],
59+
[42],
60+
[45],
61+
[52],
62+
];
63+
}
64+
65+
66+
/**
67+
* Verify that there are no false positives for valid code.
68+
*
69+
* @dataProvider dataNoFalsePositives
70+
*
71+
* @param int $line The line number.
72+
*
73+
* @return void
74+
*/
75+
public function testNoFalsePositives($line)
76+
{
77+
$file = $this->sniffFile(__FILE__, '8.1');
78+
$this->assertNoViolation($file, $line);
79+
}
80+
81+
/**
82+
* Data provider.
83+
*
84+
* @see testNoFalsePositives()
85+
*
86+
* @return array
87+
*/
88+
public function dataNoFalsePositives()
89+
{
90+
$data = [];
91+
for ($line = 1; $line <= 17; $line++) {
92+
$data[] = [$line];
93+
}
94+
95+
return $data;
96+
}
97+
98+
99+
/**
100+
* Verify no notices are thrown at all.
101+
*
102+
* @return void
103+
*/
104+
public function testNoViolationsInFileOnValidVersion()
105+
{
106+
$file = $this->sniffFile(__FILE__, '8.2');
107+
$this->assertNoViolation($file);
108+
}
109+
}

0 commit comments

Comments
 (0)