Skip to content

Commit

Permalink
PHP 8.1: Added support for "readonly" keyword
Browse files Browse the repository at this point in the history
  • Loading branch information
kukulich committed Nov 20, 2021
1 parent 5fb9b64 commit 2bd65e9
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 1 deletion.
6 changes: 6 additions & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
<file baseinstalldir="" name="NamedFunctionCallArgumentsTest.php" role="test" />
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
<file baseinstalldir="" name="ReadonlyTest.inc" role="test" />
<file baseinstalldir="" name="ReadonlyTest.php" role="test" />
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.inc" role="test" />
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.php" role="test" />
<file baseinstalldir="" name="ShortArrayTest.inc" role="test" />
Expand Down Expand Up @@ -2092,6 +2094,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
Expand Down Expand Up @@ -2182,6 +2186,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
Expand Down
39 changes: 38 additions & 1 deletion src/Tokenizers/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ class PHP extends Tokenizer
T_PRIVATE => 7,
T_PUBLIC => 6,
T_PROTECTED => 9,
T_READONLY => 8,
T_REQUIRE => 7,
T_REQUIRE_ONCE => 12,
T_RETURN => 6,
Expand Down Expand Up @@ -506,7 +507,7 @@ protected function tokenize($string)
}
}

$tokens = @token_get_all($string);
$tokens = @token_get_all($string, TOKEN_PARSE);
$finalTokens = [];

$newStackPtr = 0;
Expand Down Expand Up @@ -945,6 +946,42 @@ protected function tokenize($string)
continue;
}//end if

/*
"readonly" keyword for PHP < 8.1
T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED and T_NAME_RELATIVE have to be converted to T_STRING first
*/

if ($tokenIsArray === true
&& $token[0] === T_STRING
&& strtolower($token[1]) === 'readonly'
) {
// Get the next non-empty token.
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
if (is_array($tokens[$i]) === false
|| isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
) {
break;
}
}

if (isset($tokens[$i]) === false
|| (is_array($tokens[$i]) === true && in_array($tokens[$i][0], [T_STRING, T_PUBLIC, T_PROTECTED, T_PRIVATE]) === true)
) {
$newToken = [];
$newToken['code'] = T_READONLY;
$newToken['type'] = 'T_READONLY';
$newToken['content'] = $tokens[$i][1];
$finalTokens[$newStackPtr] = $newToken;

if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "\t\t* token $stackPtr changed from T_STRING to T_READONLY".PHP_EOL;
}

$newStackPtr++;
continue;
}
}//end if

/*
PHP 8.0 Attributes
*/
Expand Down
4 changes: 4 additions & 0 deletions src/Util/Tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
define('T_ATTRIBUTE', 'PHPCS_T_ATTRIBUTE');
}

if (defined('T_READONLY') === false) {
define('T_READONLY', 'PHPCS_T_READONLY');
}

// Some PHP 8.1 tokens, replicated for lower versions.
if (defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG') === false) {
define('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG');
Expand Down
53 changes: 53 additions & 0 deletions tests/Core/Tokenizer/ReadonlyTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

class Foo
{
/* testReadonlyProperty */
readonly int $readonlyProperty;
/* testPublicReadonlyProperty */
public readonly int $publicReadonlyProperty;
/* testProtectedReadonlyProperty */
protected readonly int $protectedReadonlyProperty;
/* testPrivateReadonlyProperty */
private readonly int $privateReadonlyProperty;
/* testPublicReadonlyPropertyWithReadonlyFirst */
readonly public int $publicReadonlyProperty;
/* testProtectedReadonlyPropertyWithReadonlyFirst */
readonly protected int $protectedReadonlyProperty;
/* testPrivateReadonlyPropertyWithReadonlyFirst */
readonly private int $privateReadonlyProperty;
/* testReadonlyWithCommentsInDeclaration */
private /* Comment */ readonly /* Comment */ int /* Comment */ $readonlyPropertyWithCommentsInDeclaration;

}

class ClassName {
/* testReadonlyUsedAsClassConstantName */
const READONLY = 'readonly';

/* testReadonlyUsedAsMethodName */
public function readonly() {
// Do something.

/* testReadonlyUsedAsPropertyName */
$this->readonly = 'foo';
}
}

/* testReadonlyUsedAsFunctionName */
function readonly()
{
}

/* testReadonlyUsedAsNamespaceName */
namespace Readonly;
/* testReadonlyUsedAsPartOfNamespaceName */
namespace My\Readonly\Collection;
/* testReadonlyAsFunctionCall */
$var = readonly($a, $b);
/* testClassConstantFetchWithReadonlyAsConstantName */
echo ClassName::READONLY;

/* testParseErrorLiveCoding */
// This must be the last test in the file.
readonly
107 changes: 107 additions & 0 deletions tests/Core/Tokenizer/ReadonlyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
/**
* Tests the support of PHP 8.1 "readonly" keyword.
*
* @author Jaroslav Hanslík <kukulich@kukulich.cz>
* @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/

namespace PHP_CodeSniffer\Tests\Core\Tokenizer;

use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;

class ReadonlyTest extends AbstractMethodUnitTest
{


/**
* Test that the "readonly" keyword is tokenized as such.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
*
* @dataProvider dataReadonly
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
*
* @return void
*/
public function testReadonly($testMarker)
{
$tokens = self::$phpcsFile->getTokens();

$target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING]);
$this->assertSame(T_READONLY, $tokens[$target]['code']);
$this->assertSame('T_READONLY', $tokens[$target]['type']);

}//end testReadonly()


/**
* Data provider.
*
* @see testReadonly()
*
* @return array
*/
public function dataReadonly()
{
return [
['/* testReadonlyProperty */'],
['/* testPublicReadonlyProperty */'],
['/* testProtectedReadonlyProperty */'],
['/* testPrivateReadonlyProperty */'],
['/* testPublicReadonlyPropertyWithReadonlyFirst */'],
['/* testProtectedReadonlyPropertyWithReadonlyFirst */'],
['/* testPrivateReadonlyPropertyWithReadonlyFirst */'],
['/* testReadonlyWithCommentsInDeclaration */'],
['/* testParseErrorLiveCoding */'],
];

}//end dataReadonly()


/**
* Test that "readonly" when not used as the keyword is still tokenized as `T_STRING`.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
*
* @dataProvider dataNotReadonly
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
*
* @return void
*/
public function testNotReadonly($testMarker)
{
$tokens = self::$phpcsFile->getTokens();

$target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING]);
$this->assertSame(T_STRING, $tokens[$target]['code']);
$this->assertSame('T_STRING', $tokens[$target]['type']);

}//end testNotReadonly()


/**
* Data provider.
*
* @see testNotReadonly()
*
* @return array
*/
public function dataNotReadonly()
{
return [
['/* testReadonlyUsedAsClassConstantName */'],
['/* testReadonlyUsedAsMethodName */'],
['/* testReadonlyUsedAsPropertyName */'],
['/* testReadonlyUsedAsFunctionName */'],
['/* testReadonlyUsedAsNamespaceName */'],
['/* testReadonlyUsedAsPartOfNamespaceName */'],
['/* testReadonlyAsFunctionCall */'],
['/* testClassConstantFetchWithReadonlyAsConstantName */'],
];

}//end dataNotReadonly()


}//end class

0 comments on commit 2bd65e9

Please sign in to comment.