Skip to content

Commit f09e28a

Browse files
committed
Merge branch 'feature/php-8-nullsafe-operator' of https://github.com/jrfnl/PHP_CodeSniffer
2 parents 48d5bae + 5aa9f1a commit f09e28a

File tree

8 files changed

+230
-19
lines changed

8 files changed

+230
-19
lines changed

package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
108108
<file baseinstalldir="" name="BackfillFnTokenTest.php" role="test" />
109109
<file baseinstalldir="" name="BackfillNumericSeparatorTest.inc" role="test" />
110110
<file baseinstalldir="" name="BackfillNumericSeparatorTest.php" role="test" />
111+
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
112+
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
111113
<file baseinstalldir="" name="ShortArrayTest.inc" role="test" />
112114
<file baseinstalldir="" name="ShortArrayTest.php" role="test" />
113115
<file baseinstalldir="" name="StableCommentWhitespaceTest.inc" role="test" />
@@ -1978,6 +1980,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
19781980
<install as="CodeSniffer/Core/Tokenizer/BackfillFnTokenTest.inc" name="tests/Core/Tokenizer/BackfillFnTokenTest.inc" />
19791981
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.php" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.php" />
19801982
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.inc" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc" />
1983+
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
1984+
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
19811985
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
19821986
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.inc" name="tests/Core/Tokenizer/ShortArrayTest.inc" />
19831987
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.php" />
@@ -2037,6 +2041,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20372041
<install as="CodeSniffer/Core/Tokenizer/BackfillFnTokenTest.inc" name="tests/Core/Tokenizer/BackfillFnTokenTest.inc" />
20382042
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.php" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.php" />
20392043
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.inc" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc" />
2044+
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
2045+
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
20402046
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
20412047
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.inc" name="tests/Core/Tokenizer/ShortArrayTest.inc" />
20422048
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.php" />

src/Tokenizers/PHP.php

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ class PHP extends Tokenizer
372372
T_NS_C => 13,
373373
T_NS_SEPARATOR => 1,
374374
T_NEW => 3,
375+
T_NULLSAFE_OBJECT_OPERATOR => 3,
375376
T_OBJECT_OPERATOR => 2,
376377
T_OPEN_TAG_WITH_ECHO => 3,
377378
T_OR_EQUAL => 2,
@@ -1017,6 +1018,29 @@ protected function tokenize($string)
10171018
continue;
10181019
}
10191020

1021+
/*
1022+
Before PHP 8, the ?-> operator was tokenized as
1023+
T_INLINE_THEN followed by T_OBJECT_OPERATOR.
1024+
So look for and combine these tokens in earlier versions.
1025+
*/
1026+
1027+
if ($tokenIsArray === false
1028+
&& $token[0] === '?'
1029+
&& isset($tokens[($stackPtr + 1)]) === true
1030+
&& is_array($tokens[($stackPtr + 1)]) === true
1031+
&& $tokens[($stackPtr + 1)][0] === T_OBJECT_OPERATOR
1032+
) {
1033+
$newToken = [];
1034+
$newToken['code'] = T_NULLSAFE_OBJECT_OPERATOR;
1035+
$newToken['type'] = 'T_NULLSAFE_OBJECT_OPERATOR';
1036+
$newToken['content'] = '?->';
1037+
$finalTokens[$newStackPtr] = $newToken;
1038+
1039+
$newStackPtr++;
1040+
$stackPtr++;
1041+
continue;
1042+
}
1043+
10201044
/*
10211045
Before PHP 7.4, underscores inside T_LNUMBER and T_DNUMBER
10221046
tokens split the token with a T_STRING. So look for
@@ -1513,17 +1537,18 @@ function return types. We want to keep the parenthesis map clean,
15131537
// Some T_STRING tokens should remain that way
15141538
// due to their context.
15151539
$context = [
1516-
T_OBJECT_OPERATOR => true,
1517-
T_FUNCTION => true,
1518-
T_CLASS => true,
1519-
T_EXTENDS => true,
1520-
T_IMPLEMENTS => true,
1521-
T_NEW => true,
1522-
T_CONST => true,
1523-
T_NS_SEPARATOR => true,
1524-
T_USE => true,
1525-
T_NAMESPACE => true,
1526-
T_PAAMAYIM_NEKUDOTAYIM => true,
1540+
T_OBJECT_OPERATOR => true,
1541+
T_NULLSAFE_OBJECT_OPERATOR => true,
1542+
T_FUNCTION => true,
1543+
T_CLASS => true,
1544+
T_EXTENDS => true,
1545+
T_IMPLEMENTS => true,
1546+
T_NEW => true,
1547+
T_CONST => true,
1548+
T_NS_SEPARATOR => true,
1549+
T_USE => true,
1550+
T_NAMESPACE => true,
1551+
T_PAAMAYIM_NEKUDOTAYIM => true,
15271552
];
15281553

15291554
if (isset($context[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
@@ -2018,6 +2043,7 @@ protected function processAdditional()
20182043
T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS,
20192044
T_VARIABLE => T_VARIABLE,
20202045
T_OBJECT_OPERATOR => T_OBJECT_OPERATOR,
2046+
T_NULLSAFE_OBJECT_OPERATOR => T_NULLSAFE_OBJECT_OPERATOR,
20212047
T_STRING => T_STRING,
20222048
T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING,
20232049
];
@@ -2087,9 +2113,10 @@ protected function processAdditional()
20872113
}
20882114

20892115
$context = [
2090-
T_OBJECT_OPERATOR => true,
2091-
T_NS_SEPARATOR => true,
2092-
T_PAAMAYIM_NEKUDOTAYIM => true,
2116+
T_OBJECT_OPERATOR => true,
2117+
T_NULLSAFE_OBJECT_OPERATOR => true,
2118+
T_NS_SEPARATOR => true,
2119+
T_PAAMAYIM_NEKUDOTAYIM => true,
20932120
];
20942121
if (isset($context[$this->tokens[$x]['code']]) === true) {
20952122
if (PHP_CODESNIFFER_VERBOSITY > 1) {

src/Tokenizers/Tokenizer.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,11 +1291,12 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0)
12911291
// a new statement, it isn't a scope opener.
12921292
$disallowed = Util\Tokens::$assignmentTokens;
12931293
$disallowed += [
1294-
T_DOLLAR => true,
1295-
T_VARIABLE => true,
1296-
T_OBJECT_OPERATOR => true,
1297-
T_COMMA => true,
1298-
T_OPEN_PARENTHESIS => true,
1294+
T_DOLLAR => true,
1295+
T_VARIABLE => true,
1296+
T_OBJECT_OPERATOR => true,
1297+
T_NULLSAFE_OBJECT_OPERATOR => true,
1298+
T_COMMA => true,
1299+
T_OPEN_PARENTHESIS => true,
12991300
];
13001301

13011302
if (isset($disallowed[$this->tokens[$x]['code']]) === true) {

src/Util/Tokens.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@
124124
define('T_FN', 'PHPCS_T_FN');
125125
}
126126

127+
// Some PHP 8.0 tokens, replicated for lower versions.
128+
if (defined('T_NULLSAFE_OBJECT_OPERATOR') === false) {
129+
define('T_NULLSAFE_OBJECT_OPERATOR', 'PHPCS_T_NULLSAFE_OBJECT_OPERATOR');
130+
}
131+
127132
// Tokens used for parsing doc blocks.
128133
define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR');
129134
define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE');
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* Null safe operator.
5+
*/
6+
7+
/* testObjectOperator */
8+
echo $obj->foo;
9+
10+
/* testNullsafeObjectOperator */
11+
echo $obj?->foo;
12+
13+
/* testNullsafeObjectOperatorWriteContext */
14+
// Intentional parse error, but not the concern of the tokenizer.
15+
$foo?->bar->baz = 'baz';
16+
17+
/* testTernaryThen */
18+
echo $obj ? $obj->prop : $other->prop;
19+
20+
/* testParseErrorWhitespaceNotAllowed */
21+
echo $obj ?
22+
-> foo;
23+
24+
/* testParseErrorCommentNotAllowed */
25+
echo $obj ?/*comment*/-> foo;
26+
27+
/* testLiveCoding */
28+
// Intentional parse error. This has to be the last test in the file.
29+
echo $obj?
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
/**
3+
* Tests the backfill for the PHP >= 8.0 nullsafe object operator.
4+
*
5+
* @author Juliette Reinders Folmer <[email protected]>
6+
* @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
7+
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8+
*/
9+
10+
namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
11+
12+
use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
13+
use PHP_CodeSniffer\Util\Tokens;
14+
15+
class NullsafeObjectOperatorTest extends AbstractMethodUnitTest
16+
{
17+
18+
/**
19+
* Tokens to search for.
20+
*
21+
* @var array
22+
*/
23+
protected $find = [
24+
T_NULLSAFE_OBJECT_OPERATOR,
25+
T_OBJECT_OPERATOR,
26+
T_INLINE_THEN,
27+
];
28+
29+
30+
/**
31+
* Test that a normal object operator is still tokenized as such.
32+
*
33+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
34+
*
35+
* @return void
36+
*/
37+
public function testObjectOperator()
38+
{
39+
$tokens = self::$phpcsFile->getTokens();
40+
41+
$operator = $this->getTargetToken('/* testObjectOperator */', $this->find);
42+
$this->assertSame(T_OBJECT_OPERATOR, $tokens[$operator]['code'], 'Failed asserting code is object operator');
43+
$this->assertSame('T_OBJECT_OPERATOR', $tokens[$operator]['type'], 'Failed asserting type is object operator');
44+
45+
}//end testObjectOperator()
46+
47+
48+
/**
49+
* Test that a nullsafe object operator is tokenized as such.
50+
*
51+
* @param string $testMarker The comment which prefaces the target token in the test file.
52+
*
53+
* @dataProvider dataNullsafeObjectOperator
54+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
55+
*
56+
* @return void
57+
*/
58+
public function testNullsafeObjectOperator($testMarker)
59+
{
60+
$tokens = self::$phpcsFile->getTokens();
61+
62+
$operator = $this->getTargetToken($testMarker, $this->find);
63+
$this->assertSame(T_NULLSAFE_OBJECT_OPERATOR, $tokens[$operator]['code'], 'Failed asserting code is nullsafe object operator');
64+
$this->assertSame('T_NULLSAFE_OBJECT_OPERATOR', $tokens[$operator]['type'], 'Failed asserting type is nullsafe object operator');
65+
66+
}//end testNullsafeObjectOperator()
67+
68+
69+
/**
70+
* Data provider.
71+
*
72+
* @see testNullsafeObjectOperator()
73+
*
74+
* @return array
75+
*/
76+
public function dataNullsafeObjectOperator()
77+
{
78+
return [
79+
['/* testNullsafeObjectOperator */'],
80+
['/* testNullsafeObjectOperatorWriteContext */'],
81+
];
82+
83+
}//end dataNullsafeObjectOperator()
84+
85+
86+
/**
87+
* Test that a question mark not followed by an object operator is tokenized as T_TERNARY_THEN.
88+
*
89+
* @param string $testMarker The comment which prefaces the target token in the test file.
90+
* @param bool $testObjectOperator Whether to test for the next non-empty token being tokenized
91+
* as an object operator.
92+
*
93+
* @dataProvider dataTernaryThen
94+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
95+
*
96+
* @return void
97+
*/
98+
public function testTernaryThen($testMarker, $testObjectOperator=false)
99+
{
100+
$tokens = self::$phpcsFile->getTokens();
101+
102+
$operator = $this->getTargetToken($testMarker, $this->find);
103+
$this->assertSame(T_INLINE_THEN, $tokens[$operator]['code'], 'Failed asserting code is inline then');
104+
$this->assertSame('T_INLINE_THEN', $tokens[$operator]['type'], 'Failed asserting type is inline then');
105+
106+
if ($testObjectOperator === true) {
107+
$next = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($operator + 1), null, true);
108+
$this->assertSame(T_OBJECT_OPERATOR, $tokens[$next]['code'], 'Failed asserting code is object operator');
109+
$this->assertSame('T_OBJECT_OPERATOR', $tokens[$next]['type'], 'Failed asserting type is object operator');
110+
}
111+
112+
}//end testTernaryThen()
113+
114+
115+
/**
116+
* Data provider.
117+
*
118+
* @see testTernaryThen()
119+
*
120+
* @return array
121+
*/
122+
public function dataTernaryThen()
123+
{
124+
return [
125+
['/* testTernaryThen */'],
126+
[
127+
'/* testParseErrorWhitespaceNotAllowed */',
128+
true,
129+
],
130+
[
131+
'/* testParseErrorCommentNotAllowed */',
132+
true,
133+
],
134+
['/* testLiveCoding */'],
135+
];
136+
137+
}//end dataTernaryThen()
138+
139+
140+
}//end class

tests/Core/Tokenizer/ShortArrayTest.inc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ $a = (new Foo( array(1, array(4, 5), 3) ))[1][0];
6262
/* testClassMemberDereferencingOnClone */
6363
echo (clone $iterable)[20];
6464

65+
/* testNullsafeMethodCallDereferencing */
66+
$var = $obj?->function_call()[$x];
6567

6668
/*
6769
* Short array brackets.

tests/Core/Tokenizer/ShortArrayTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public function dataSquareBrackets()
7272
['/* testClassMemberDereferencingOnInstantiation1 */'],
7373
['/* testClassMemberDereferencingOnInstantiation2 */'],
7474
['/* testClassMemberDereferencingOnClone */'],
75+
['/* testNullsafeMethodCallDereferencing */'],
7576
['/* testLiveCoding */'],
7677
];
7778

0 commit comments

Comments
 (0)