Skip to content

Commit 4db6a74

Browse files
committed
Merge branch 'feature/bugfixes-namespace-operator-in-typedeclarations' of https://github.com/jrfnl/PHP_CodeSniffer
2 parents 6525028 + 16d2cd1 commit 4db6a74

14 files changed

+233
-0
lines changed

package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
122122
<file baseinstalldir="" name="BackfillNumericSeparatorTest.php" role="test" />
123123
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
124124
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
125+
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.inc" role="test" />
126+
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.php" role="test" />
125127
<file baseinstalldir="" name="ShortArrayTest.inc" role="test" />
126128
<file baseinstalldir="" name="ShortArrayTest.php" role="test" />
127129
<file baseinstalldir="" name="StableCommentWhitespaceTest.inc" role="test" />
@@ -2000,6 +2002,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20002002
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.inc" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc" />
20012003
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
20022004
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
2005+
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
2006+
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
20032007
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
20042008
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.inc" name="tests/Core/Tokenizer/ShortArrayTest.inc" />
20052009
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.php" />
@@ -2063,6 +2067,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20632067
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.inc" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc" />
20642068
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
20652069
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
2070+
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
2071+
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
20662072
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
20672073
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.inc" name="tests/Core/Tokenizer/ShortArrayTest.inc" />
20682074
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.php" />

src/Files/File.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,7 @@ public function getMethodParameters($stackPtr)
14411441
$typeHintEndToken = $i;
14421442
}
14431443
break;
1444+
case T_NAMESPACE:
14441445
case T_NS_SEPARATOR:
14451446
// Part of a type hint or default value.
14461447
if ($defaultStart === null) {
@@ -1630,6 +1631,7 @@ public function getMethodProperties($stackPtr)
16301631
T_SELF => T_SELF,
16311632
T_PARENT => T_PARENT,
16321633
T_STATIC => T_STATIC,
1634+
T_NAMESPACE => T_NAMESPACE,
16331635
T_NS_SEPARATOR => T_NS_SEPARATOR,
16341636
];
16351637

@@ -1813,6 +1815,7 @@ public function getMemberProperties($stackPtr)
18131815
T_CALLABLE => T_CALLABLE,
18141816
T_SELF => T_SELF,
18151817
T_PARENT => T_PARENT,
1818+
T_NAMESPACE => T_NAMESPACE,
18161819
T_NS_SEPARATOR => T_NS_SEPARATOR,
18171820
];
18181821

src/Tokenizers/PHP.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,7 @@ protected function tokenize($string)
12271227
|| $tokenType === T_NAME_RELATIVE
12281228
|| $tokenType === T_NAME_QUALIFIED
12291229
|| $tokenType === T_ARRAY
1230+
|| $tokenType === T_NAMESPACE
12301231
|| $tokenType === T_NS_SEPARATOR
12311232
) {
12321233
$lastRelevantNonEmpty = $tokenType;
@@ -1473,6 +1474,7 @@ function return types. We want to keep the parenthesis map clean,
14731474
T_CALLABLE => T_CALLABLE,
14741475
T_SELF => T_SELF,
14751476
T_PARENT => T_PARENT,
1477+
T_NAMESPACE => T_NAMESPACE,
14761478
T_NS_SEPARATOR => T_NS_SEPARATOR,
14771479
];
14781480

@@ -1987,6 +1989,7 @@ protected function processAdditional()
19871989
T_STRING => T_STRING,
19881990
T_ARRAY => T_ARRAY,
19891991
T_COLON => T_COLON,
1992+
T_NAMESPACE => T_NAMESPACE,
19901993
T_NS_SEPARATOR => T_NS_SEPARATOR,
19911994
T_NULLABLE => T_NULLABLE,
19921995
T_CALLABLE => T_CALLABLE,

src/Tokenizers/Tokenizer.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,13 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0)
11111111
continue;
11121112
}
11131113

1114+
if ($tokenType === T_NAMESPACE) {
1115+
// PHP namespace keywords are special because they can be
1116+
// used as blocks but also inline as operators.
1117+
// So if we find them nested inside another opener, just skip them.
1118+
continue;
1119+
}
1120+
11141121
if ($tokenType === T_FUNCTION
11151122
&& $this->tokens[$stackPtr]['code'] !== T_FUNCTION
11161123
) {

tests/Core/File/GetMemberPropertiesTest.inc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,8 @@ class PHP8Mixed {
188188
// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method.
189189
private ?mixed $nullableMixed;
190190
}
191+
192+
class NSOperatorInType {
193+
/* testNamespaceOperatorTypeHint */
194+
public ?namespace\Name $prop;
195+
}

tests/Core/File/GetMemberPropertiesTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,16 @@ public function dataGetMemberProperties()
479479
'nullable_type' => true,
480480
],
481481
],
482+
[
483+
'/* testNamespaceOperatorTypeHint */',
484+
[
485+
'scope' => 'public',
486+
'scope_specified' => true,
487+
'is_static' => false,
488+
'type' => '?namespace\Name',
489+
'nullable_type' => true,
490+
],
491+
],
482492
];
483493

484494
}//end dataGetMemberProperties()

tests/Core/File/GetMethodParametersTest.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ function mixedTypeHint(mixed &...$var1) {}
3838
/* testPHP8MixedTypeHintNullable */
3939
// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method.
4040
function mixedTypeHintNullable(?Mixed $var1) {}
41+
42+
/* testNamespaceOperatorTypeHint */
43+
function namespaceOperatorTypeHint(?namespace\Name $var1) {}

tests/Core/File/GetMethodParametersTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,28 @@ public function testPHP8MixedTypeHintNullable()
318318
}//end testPHP8MixedTypeHintNullable()
319319

320320

321+
/**
322+
* Verify recognition of type declarations using the namespace operator.
323+
*
324+
* @return void
325+
*/
326+
public function testNamespaceOperatorTypeHint()
327+
{
328+
$expected = [];
329+
$expected[0] = [
330+
'name' => '$var1',
331+
'content' => '?namespace\Name $var1',
332+
'pass_by_reference' => false,
333+
'variable_length' => false,
334+
'type_hint' => '?namespace\Name',
335+
'nullable_type' => true,
336+
];
337+
338+
$this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
339+
340+
}//end testNamespaceOperatorTypeHint()
341+
342+
321343
/**
322344
* Test helper.
323345
*

tests/Core/File/GetMethodPropertiesTest.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,6 @@ function mixedTypeHint() :mixed {}
8080
/* testPHP8MixedTypeHintNullable */
8181
// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method.
8282
function mixedTypeHintNullable(): ?mixed {}
83+
84+
/* testNamespaceOperatorTypeHint */
85+
function namespaceOperatorTypeHint() : ?namespace\Name {}

tests/Core/File/GetMethodPropertiesTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,29 @@ public function testPHP8MixedTypeHintNullable()
452452
}//end testPHP8MixedTypeHintNullable()
453453

454454

455+
/**
456+
* Test a function with return type using the namespace operator.
457+
*
458+
* @return void
459+
*/
460+
public function testNamespaceOperatorTypeHint()
461+
{
462+
$expected = [
463+
'scope' => 'public',
464+
'scope_specified' => false,
465+
'return_type' => '?namespace\Name',
466+
'nullable_return_type' => true,
467+
'is_abstract' => false,
468+
'is_final' => false,
469+
'is_static' => false,
470+
'has_body' => true,
471+
];
472+
473+
$this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
474+
475+
}//end testNamespaceOperatorTypeHint()
476+
477+
455478
/**
456479
* Test helper.
457480
*

tests/Core/Tokenizer/BackfillFnTokenTest.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ $a = fn($x) => yield 'k' => $x;
6363
/* testNullableNamespace */
6464
$a = fn(?\DateTime $x) : ?\DateTime => $x;
6565

66+
/* testNamespaceOperatorInTypes */
67+
$fn = fn(namespace\Foo $a) : ?namespace\Foo => $a;
68+
6669
/* testSelfReturnType */
6770
fn(self $a) : self => $a;
6871

tests/Core/Tokenizer/BackfillFnTokenTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,34 @@ public function testNullableNamespace()
465465
}//end testNullableNamespace()
466466

467467

468+
/**
469+
* Test arrow functions that use the namespace operator in the return type.
470+
*
471+
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
472+
*
473+
* @return void
474+
*/
475+
public function testNamespaceOperatorInTypes()
476+
{
477+
$tokens = self::$phpcsFile->getTokens();
478+
479+
$token = $this->getTargetToken('/* testNamespaceOperatorInTypes */', T_FN);
480+
$this->backfillHelper($token);
481+
482+
$this->assertSame($tokens[$token]['scope_opener'], ($token + 16), 'Scope opener is not the arrow token');
483+
$this->assertSame($tokens[$token]['scope_closer'], ($token + 19), 'Scope closer is not the semicolon token');
484+
485+
$opener = $tokens[$token]['scope_opener'];
486+
$this->assertSame($tokens[$opener]['scope_opener'], ($token + 16), 'Opener scope opener is not the arrow token');
487+
$this->assertSame($tokens[$opener]['scope_closer'], ($token + 19), 'Opener scope closer is not the semicolon token');
488+
489+
$closer = $tokens[$token]['scope_closer'];
490+
$this->assertSame($tokens[$closer]['scope_opener'], ($token + 16), 'Closer scope opener is not the arrow token');
491+
$this->assertSame($tokens[$closer]['scope_closer'], ($token + 19), 'Closer scope closer is not the semicolon token');
492+
493+
}//end testNamespaceOperatorInTypes()
494+
495+
468496
/**
469497
* Test arrow functions that use self/parent/callable/array/static return types.
470498
*
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/* testClassExtends */
4+
class Foo extends namespace\Bar {}
5+
6+
/* testClassImplements */
7+
$anon = new class implements namespace\Foo {}
8+
9+
/* testInterfaceExtends */
10+
interface FooBar extends namespace\BarFoo {}
11+
12+
/* testFunctionReturnType */
13+
function foo() : namespace\Baz {}
14+
15+
/* testClosureReturnType */
16+
$closure = function () : namespace\Baz {}
17+
18+
/* testArrowFunctionReturnType */
19+
$fn = fn() : namespace\Baz => new namespace\Baz;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
/**
3+
* Tests the adding of the "bracket_opener/closer" keys to use group tokens.
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+
14+
class ScopeSettingWithNamespaceOperatorTest extends AbstractMethodUnitTest
15+
{
16+
17+
18+
/**
19+
* Test that the scope opener/closers are set correctly when the namespace keyword is encountered as an operator.
20+
*
21+
* @param string $testMarker The comment which prefaces the target tokens in the test file.
22+
* @param int|string[] $tokenTypes The token type to search for.
23+
* @param int|string[] $open Optional. The token type for the scope opener.
24+
* @param int|string[] $close Optional. The token type for the scope closer.
25+
*
26+
* @dataProvider dataScopeSetting
27+
* @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap
28+
*
29+
* @return void
30+
*/
31+
public function testScopeSetting($testMarker, $tokenTypes, $open=T_OPEN_CURLY_BRACKET, $close=T_CLOSE_CURLY_BRACKET)
32+
{
33+
$tokens = self::$phpcsFile->getTokens();
34+
35+
$target = $this->getTargetToken($testMarker, $tokenTypes);
36+
$opener = $this->getTargetToken($testMarker, $open);
37+
$closer = $this->getTargetToken($testMarker, $close);
38+
39+
$this->assertArrayHasKey('scope_opener', $tokens[$target], 'Scope opener missing');
40+
$this->assertArrayHasKey('scope_closer', $tokens[$target], 'Scope closer missing');
41+
$this->assertSame($opener, $tokens[$target]['scope_opener'], 'Scope opener not same');
42+
$this->assertSame($closer, $tokens[$target]['scope_closer'], 'Scope closer not same');
43+
44+
$this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Scope opener missing for open curly');
45+
$this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Scope closer missing for open curly');
46+
$this->assertSame($opener, $tokens[$opener]['scope_opener'], 'Scope opener not same for open curly');
47+
$this->assertSame($closer, $tokens[$opener]['scope_closer'], 'Scope closer not same for open curly');
48+
49+
$this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Scope opener missing for close curly');
50+
$this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Scope closer missing for close curly');
51+
$this->assertSame($opener, $tokens[$closer]['scope_opener'], 'Scope opener not same for close curly');
52+
$this->assertSame($closer, $tokens[$closer]['scope_closer'], 'Scope closer not same for close curly');
53+
54+
}//end testScopeSetting()
55+
56+
57+
/**
58+
* Data provider.
59+
*
60+
* @see testScopeSetting()
61+
*
62+
* @return array
63+
*/
64+
public function dataScopeSetting()
65+
{
66+
return [
67+
[
68+
'/* testClassExtends */',
69+
[T_CLASS],
70+
],
71+
[
72+
'/* testClassImplements */',
73+
[T_ANON_CLASS],
74+
],
75+
[
76+
'/* testInterfaceExtends */',
77+
[T_INTERFACE],
78+
],
79+
[
80+
'/* testFunctionReturnType */',
81+
[T_FUNCTION],
82+
],
83+
[
84+
'/* testClosureReturnType */',
85+
[T_CLOSURE],
86+
],
87+
[
88+
'/* testArrowFunctionReturnType */',
89+
[T_FN],
90+
[T_FN_ARROW],
91+
[T_SEMICOLON],
92+
],
93+
];
94+
95+
}//end dataScopeSetting()
96+
97+
98+
}//end class

0 commit comments

Comments
 (0)