diff --git a/package.xml b/package.xml index 5c25d1239f..ced5568255 100644 --- a/package.xml +++ b/package.xml @@ -122,6 +122,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2000,6 +2002,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2063,6 +2067,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + diff --git a/src/Files/File.php b/src/Files/File.php index b7e35488f0..d1e9a4da87 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -1441,6 +1441,7 @@ public function getMethodParameters($stackPtr) $typeHintEndToken = $i; } break; + case T_NAMESPACE: case T_NS_SEPARATOR: // Part of a type hint or default value. if ($defaultStart === null) { @@ -1630,6 +1631,7 @@ public function getMethodProperties($stackPtr) T_SELF => T_SELF, T_PARENT => T_PARENT, T_STATIC => T_STATIC, + T_NAMESPACE => T_NAMESPACE, T_NS_SEPARATOR => T_NS_SEPARATOR, ]; @@ -1813,6 +1815,7 @@ public function getMemberProperties($stackPtr) T_CALLABLE => T_CALLABLE, T_SELF => T_SELF, T_PARENT => T_PARENT, + T_NAMESPACE => T_NAMESPACE, T_NS_SEPARATOR => T_NS_SEPARATOR, ]; diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index d76e0d60ac..7aa484ef70 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -1227,6 +1227,7 @@ protected function tokenize($string) || $tokenType === T_NAME_RELATIVE || $tokenType === T_NAME_QUALIFIED || $tokenType === T_ARRAY + || $tokenType === T_NAMESPACE || $tokenType === T_NS_SEPARATOR ) { $lastRelevantNonEmpty = $tokenType; @@ -1473,6 +1474,7 @@ function return types. We want to keep the parenthesis map clean, T_CALLABLE => T_CALLABLE, T_SELF => T_SELF, T_PARENT => T_PARENT, + T_NAMESPACE => T_NAMESPACE, T_NS_SEPARATOR => T_NS_SEPARATOR, ]; @@ -1987,6 +1989,7 @@ protected function processAdditional() T_STRING => T_STRING, T_ARRAY => T_ARRAY, T_COLON => T_COLON, + T_NAMESPACE => T_NAMESPACE, T_NS_SEPARATOR => T_NS_SEPARATOR, T_NULLABLE => T_NULLABLE, T_CALLABLE => T_CALLABLE, diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php index 24f11d2505..e0bf22fb01 100644 --- a/src/Tokenizers/Tokenizer.php +++ b/src/Tokenizers/Tokenizer.php @@ -1111,6 +1111,13 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0) continue; } + if ($tokenType === T_NAMESPACE) { + // PHP namespace keywords are special because they can be + // used as blocks but also inline as operators. + // So if we find them nested inside another opener, just skip them. + continue; + } + if ($tokenType === T_FUNCTION && $this->tokens[$stackPtr]['code'] !== T_FUNCTION ) { diff --git a/tests/Core/File/GetMemberPropertiesTest.inc b/tests/Core/File/GetMemberPropertiesTest.inc index 8c87c64035..0a511e588d 100644 --- a/tests/Core/File/GetMemberPropertiesTest.inc +++ b/tests/Core/File/GetMemberPropertiesTest.inc @@ -188,3 +188,8 @@ class PHP8Mixed { // Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method. private ?mixed $nullableMixed; } + +class NSOperatorInType { + /* testNamespaceOperatorTypeHint */ + public ?namespace\Name $prop; +} diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php index 3e8d164dcd..dfee43e203 100644 --- a/tests/Core/File/GetMemberPropertiesTest.php +++ b/tests/Core/File/GetMemberPropertiesTest.php @@ -479,6 +479,16 @@ public function dataGetMemberProperties() 'nullable_type' => true, ], ], + [ + '/* testNamespaceOperatorTypeHint */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '?namespace\Name', + 'nullable_type' => true, + ], + ], ]; }//end dataGetMemberProperties() diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc index e4d5bfca74..4ffd44221f 100644 --- a/tests/Core/File/GetMethodParametersTest.inc +++ b/tests/Core/File/GetMethodParametersTest.inc @@ -38,3 +38,6 @@ function mixedTypeHint(mixed &...$var1) {} /* testPHP8MixedTypeHintNullable */ // Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method. function mixedTypeHintNullable(?Mixed $var1) {} + +/* testNamespaceOperatorTypeHint */ +function namespaceOperatorTypeHint(?namespace\Name $var1) {} diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php index f5d43c1a63..92f51959e6 100644 --- a/tests/Core/File/GetMethodParametersTest.php +++ b/tests/Core/File/GetMethodParametersTest.php @@ -318,6 +318,28 @@ public function testPHP8MixedTypeHintNullable() }//end testPHP8MixedTypeHintNullable() + /** + * Verify recognition of type declarations using the namespace operator. + * + * @return void + */ + public function testNamespaceOperatorTypeHint() + { + $expected = []; + $expected[0] = [ + 'name' => '$var1', + 'content' => '?namespace\Name $var1', + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?namespace\Name', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testNamespaceOperatorTypeHint() + + /** * Test helper. * diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc index c268ece3a9..82c032efa0 100644 --- a/tests/Core/File/GetMethodPropertiesTest.inc +++ b/tests/Core/File/GetMethodPropertiesTest.inc @@ -80,3 +80,6 @@ function mixedTypeHint() :mixed {} /* testPHP8MixedTypeHintNullable */ // Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method. function mixedTypeHintNullable(): ?mixed {} + +/* testNamespaceOperatorTypeHint */ +function namespaceOperatorTypeHint() : ?namespace\Name {} diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php index 36d57e7301..ee11fc2b9f 100644 --- a/tests/Core/File/GetMethodPropertiesTest.php +++ b/tests/Core/File/GetMethodPropertiesTest.php @@ -452,6 +452,29 @@ public function testPHP8MixedTypeHintNullable() }//end testPHP8MixedTypeHintNullable() + /** + * Test a function with return type using the namespace operator. + * + * @return void + */ + public function testNamespaceOperatorTypeHint() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?namespace\Name', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testNamespaceOperatorTypeHint() + + /** * Test helper. * diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.inc b/tests/Core/Tokenizer/BackfillFnTokenTest.inc index 083fe6979c..46c165a2d2 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.inc +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.inc @@ -63,6 +63,9 @@ $a = fn($x) => yield 'k' => $x; /* testNullableNamespace */ $a = fn(?\DateTime $x) : ?\DateTime => $x; +/* testNamespaceOperatorInTypes */ +$fn = fn(namespace\Foo $a) : ?namespace\Foo => $a; + /* testSelfReturnType */ fn(self $a) : self => $a; diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.php b/tests/Core/Tokenizer/BackfillFnTokenTest.php index 55d378333f..9ef8dad442 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.php +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.php @@ -465,6 +465,34 @@ public function testNullableNamespace() }//end testNullableNamespace() + /** + * Test arrow functions that use the namespace operator in the return type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNamespaceOperatorInTypes() + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken('/* testNamespaceOperatorInTypes */', T_FN); + $this->backfillHelper($token); + + $this->assertSame($tokens[$token]['scope_opener'], ($token + 16), 'Scope opener is not the arrow token'); + $this->assertSame($tokens[$token]['scope_closer'], ($token + 19), 'Scope closer is not the semicolon token'); + + $opener = $tokens[$token]['scope_opener']; + $this->assertSame($tokens[$opener]['scope_opener'], ($token + 16), 'Opener scope opener is not the arrow token'); + $this->assertSame($tokens[$opener]['scope_closer'], ($token + 19), 'Opener scope closer is not the semicolon token'); + + $closer = $tokens[$token]['scope_closer']; + $this->assertSame($tokens[$closer]['scope_opener'], ($token + 16), 'Closer scope opener is not the arrow token'); + $this->assertSame($tokens[$closer]['scope_closer'], ($token + 19), 'Closer scope closer is not the semicolon token'); + + }//end testNamespaceOperatorInTypes() + + /** * Test arrow functions that use self/parent/callable/array/static return types. * diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc new file mode 100644 index 0000000000..e2d61bb664 --- /dev/null +++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc @@ -0,0 +1,19 @@ + new namespace\Baz; diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php new file mode 100644 index 0000000000..23cbd9877c --- /dev/null +++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php @@ -0,0 +1,98 @@ + + * @copyright 2020 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 ScopeSettingWithNamespaceOperatorTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the scope opener/closers are set correctly when the namespace keyword is encountered as an operator. + * + * @param string $testMarker The comment which prefaces the target tokens in the test file. + * @param int|string[] $tokenTypes The token type to search for. + * @param int|string[] $open Optional. The token type for the scope opener. + * @param int|string[] $close Optional. The token type for the scope closer. + * + * @dataProvider dataScopeSetting + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testScopeSetting($testMarker, $tokenTypes, $open=T_OPEN_CURLY_BRACKET, $close=T_CLOSE_CURLY_BRACKET) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, $tokenTypes); + $opener = $this->getTargetToken($testMarker, $open); + $closer = $this->getTargetToken($testMarker, $close); + + $this->assertArrayHasKey('scope_opener', $tokens[$target], 'Scope opener missing'); + $this->assertArrayHasKey('scope_closer', $tokens[$target], 'Scope closer missing'); + $this->assertSame($opener, $tokens[$target]['scope_opener'], 'Scope opener not same'); + $this->assertSame($closer, $tokens[$target]['scope_closer'], 'Scope closer not same'); + + $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Scope opener missing for open curly'); + $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Scope closer missing for open curly'); + $this->assertSame($opener, $tokens[$opener]['scope_opener'], 'Scope opener not same for open curly'); + $this->assertSame($closer, $tokens[$opener]['scope_closer'], 'Scope closer not same for open curly'); + + $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Scope opener missing for close curly'); + $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Scope closer missing for close curly'); + $this->assertSame($opener, $tokens[$closer]['scope_opener'], 'Scope opener not same for close curly'); + $this->assertSame($closer, $tokens[$closer]['scope_closer'], 'Scope closer not same for close curly'); + + }//end testScopeSetting() + + + /** + * Data provider. + * + * @see testScopeSetting() + * + * @return array + */ + public function dataScopeSetting() + { + return [ + [ + '/* testClassExtends */', + [T_CLASS], + ], + [ + '/* testClassImplements */', + [T_ANON_CLASS], + ], + [ + '/* testInterfaceExtends */', + [T_INTERFACE], + ], + [ + '/* testFunctionReturnType */', + [T_FUNCTION], + ], + [ + '/* testClosureReturnType */', + [T_CLOSURE], + ], + [ + '/* testArrowFunctionReturnType */', + [T_FN], + [T_FN_ARROW], + [T_SEMICOLON], + ], + ]; + + }//end dataScopeSetting() + + +}//end class