diff --git a/PHPCSUtils/Internal/AttributeHelper.php b/PHPCSUtils/Internal/AttributeHelper.php new file mode 100644 index 00000000..fc57a9e6 --- /dev/null +++ b/PHPCSUtils/Internal/AttributeHelper.php @@ -0,0 +1,227 @@ + + * + * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. + * @throws \PHPCSUtils\Exceptions\TypeError If the $type parameter is not a string. + * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. + * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not of a token type accepted for $type. + * @throws \PHPCSUtils\Exceptions\ValueError For T_VARIABLE tokens: if the token passed does not point + * to an OO property token or a parameter in a function declaration. + */ + public static function getOpeners(File $phpcsFile, $stackPtr, $type) + { + $tokens = $phpcsFile->getTokens(); + + if (\is_int($stackPtr) === false) { + throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); + } + + if (isset($tokens[$stackPtr]) === false) { + throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); + } + + if (\is_string($type) === false) { + throw TypeError::create(3, '$type', 'string', $type); + } + + $isOOProperty = false; + $isFunctionParam = false; + switch ($type) { + case 'constant': + if ($tokens[$stackPtr]['code'] !== \T_CONST) { + throw UnexpectedTokenType::create(2, '$stackPtr', 'T_CONST', $tokens[$stackPtr]['type']); + } + break; + + case 'function': + if (isset(Collections::functionDeclarationTokens()[$tokens[$stackPtr]['code']]) === false) { + $acceptedTokens = 'T_FUNCTION, T_CLOSURE or T_FN'; + throw UnexpectedTokenType::create(2, '$stackPtr', $acceptedTokens, $tokens[$stackPtr]['type']); + } + break; + + case 'OO': + if (isset(Tokens::$ooScopeTokens[$tokens[$stackPtr]['code']]) === false) { + $acceptedTokens = 'T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'; + throw UnexpectedTokenType::create(2, '$stackPtr', $acceptedTokens, $tokens[$stackPtr]['type']); + } + break; + + case 'variable': + if ($tokens[$stackPtr]['code'] !== \T_VARIABLE) { + throw UnexpectedTokenType::create(2, '$stackPtr', 'T_VARIABLE', $tokens[$stackPtr]['type']); + } + + $isOOProperty = Scopes::isOOProperty($phpcsFile, $stackPtr); + $isFunctionParam = Parentheses::lastOwnerIn($phpcsFile, $stackPtr, Collections::functionDeclarationTokens()); + + if ($isOOProperty === false && $isFunctionParam === false) { + $message = 'must be the pointer to an OO property or a parameter in a function declaration'; + throw ValueError::create(2, '$stackPtr', $message); + } + + // Allow for multi-property declarations. + if ($isOOProperty === true) { + do { + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($tokens[$prevNonEmpty]['code'] !== \T_COMMA) { + break; + } + + $stackPtr = $phpcsFile->findPrevious(T_VARIABLE, ($prevNonEmpty - 1), null, false, null, true); + } while ($stackPtr !== false); + + if ($stackPtr === false) { + $message = 'must be the pointer to an OO property or a parameter in a function declaration'; + throw ValueError::create(2, '$stackPtr', $message); + } + } + break; + + default: + throw ValueError::create(3, '$type', 'must be one of the following: constant, function, OO, variable'); + } + + if (Cache::isCached($phpcsFile, __METHOD__, "$stackPtr-$type") === true) { + return Cache::get($phpcsFile, __METHOD__, "$stackPtr-$type"); + } + + $allowedBetween = Tokens::$emptyTokens; + switch ($type) { + case 'constant': + if (Scopes::isOOConstant($phpcsFile, $stackPtr) === true) { + $allowedBetween += Collections::constantModifierKeywords(); + } + break; + + case 'function': + $allowedBetween += [\T_STATIC => \T_STATIC]; + if (Scopes::isOOMethod($phpcsFile, $stackPtr) === true) { + $allowedBetween += Tokens::$methodPrefixes; + } + break; + + case 'OO': + if ($tokens[$stackPtr]['code'] === \T_CLASS) { + $allowedBetween += Collections::classModifierKeywords(); + } elseif ($tokens[$stackPtr]['code'] === \T_ANON_CLASS) { + $allowedBetween[\T_READONLY] = \T_READONLY; + } + break; + + case 'variable': + $allowedBetween += [\T_NULLABLE => \T_NULLABLE]; + if ($isOOProperty === true) { + $allowedBetween += Collections::propertyModifierKeywords(); + $allowedBetween += Collections::propertyTypeTokens(); + } elseif ($isFunctionParam !== false) { + $allowedBetween += Collections::parameterTypeTokens(); + $allowedBetween += [ + \T_BITWISE_AND => \T_BITWISE_AND, + \T_ELLIPSIS => \T_ELLIPSIS, + ]; + + if ($tokens[$isFunctionParam]['code'] === \T_FUNCTION + && Scopes::isOOMethod($phpcsFile, $isFunctionParam) === true + ) { + $functionName = FunctionDeclarations::getName($phpcsFile, $isFunctionParam); + if (empty($functionName) === false && \strtolower($functionName) === '__construct') { + $allowedBetween += Collections::propertyModifierKeywords(); + } + } + } + + break; + } + + $seenAttributes = []; + + for ($i = ($stackPtr - 1); $i >= 0; $i--) { + if (isset($tokens[$i]['comment_opener'])) { + // Skip over docblocks. + $i = $tokens[$i]['comment_opener']; + continue; + } + + if (isset($allowedBetween[$tokens[$i]['code']])) { + continue; + } + + if (isset($tokens[$i]['attribute_opener'])) { + $seenAttributes[] = $tokens[$i]['attribute_opener']; + $i = $tokens[$i]['attribute_opener']; + continue; + } + + // In all other cases, we've reached the end of our search. + break; + } + + if ($seenAttributes !== []) { + $seenAttributes = \array_reverse($seenAttributes); + } + + Cache::set($phpcsFile, __METHOD__, "$stackPtr-$type", $seenAttributes); + return $seenAttributes; + } +} diff --git a/PHPCSUtils/Utils/Constants.php b/PHPCSUtils/Utils/Constants.php index d07620d2..82e3e2da 100644 --- a/PHPCSUtils/Utils/Constants.php +++ b/PHPCSUtils/Utils/Constants.php @@ -16,6 +16,7 @@ use PHPCSUtils\Exceptions\TypeError; use PHPCSUtils\Exceptions\UnexpectedTokenType; use PHPCSUtils\Exceptions\ValueError; +use PHPCSUtils\Internal\AttributeHelper; use PHPCSUtils\Internal\Cache; use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Scopes; @@ -191,4 +192,26 @@ public static function getProperties(File $phpcsFile, $stackPtr) Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue); return $returnValue; } + + /** + * Retrieve the stack pointers to the attribute openers for any attribute block + * which applies to the constant declaration. + * + * @since 1.2.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the `T_CONST` token + * to acquire the attributes for. + * + * @return array Array with the stack pointers to the applicable attribute openers + * or an empty array if there are no attributes attached to the constant declaration. + * + * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. + * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. + * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_CONST` token. + */ + public static function getAttributeOpeners(File $phpcsFile, $stackPtr) + { + return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'constant'); + } } diff --git a/PHPCSUtils/Utils/FunctionDeclarations.php b/PHPCSUtils/Utils/FunctionDeclarations.php index ca1890c0..17b704fe 100644 --- a/PHPCSUtils/Utils/FunctionDeclarations.php +++ b/PHPCSUtils/Utils/FunctionDeclarations.php @@ -16,6 +16,7 @@ use PHPCSUtils\Exceptions\TypeError; use PHPCSUtils\Exceptions\UnexpectedTokenType; use PHPCSUtils\Exceptions\ValueError; +use PHPCSUtils\Internal\AttributeHelper; use PHPCSUtils\Internal\Cache; use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\GetTokensAsString; @@ -665,6 +666,29 @@ public static function getParameters(File $phpcsFile, $stackPtr) return $vars; } + /** + * Retrieve the stack pointers to the attribute openers for any attribute block + * which applies to the function declaration. + * + * @since 1.2.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the function token to + * acquire the attributes for. + * + * @return array Array with the stack pointers to the applicable attribute openers + * or an empty array if there are no attributes attached to the function declaration. + * + * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. + * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. + * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a T_FUNCTION, T_CLOSURE + * or T_FN token. + */ + public static function getAttributeOpeners(File $phpcsFile, $stackPtr) + { + return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'function'); + } + /** * Checks if a given function is a PHP magic function. * diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php index 478fae32..be527080 100644 --- a/PHPCSUtils/Utils/ObjectDeclarations.php +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -15,6 +15,7 @@ use PHPCSUtils\Exceptions\OutOfBoundsStackPtr; use PHPCSUtils\Exceptions\TypeError; use PHPCSUtils\Exceptions\UnexpectedTokenType; +use PHPCSUtils\Internal\AttributeHelper; use PHPCSUtils\Internal\Cache; use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\FunctionDeclarations; @@ -375,6 +376,28 @@ private static function findNames(File $phpcsFile, $stackPtr, $keyword, array $a return $names; } + /** + * Retrieve the stack pointers to the attribute openers for any attribute block which applies to the OO declaration. + * + * @since 1.2.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the OO token to + * acquire the attributes for. + * + * @return array Array with the stack pointers to the applicable attribute openers + * or an empty array if there are no attributes attached to the OO declaration. + * + * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. + * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. + * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_CLASS`, `T_ANON_CLASS`, + * `T_TRAIT`, `T_ENUM` or `T_INTERFACE` token. + */ + public static function getAttributeOpeners(File $phpcsFile, $stackPtr) + { + return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'OO'); + } + /** * Retrieve all constants declared in an OO structure. * diff --git a/PHPCSUtils/Utils/Variables.php b/PHPCSUtils/Utils/Variables.php index b7469489..cfc68d1e 100644 --- a/PHPCSUtils/Utils/Variables.php +++ b/PHPCSUtils/Utils/Variables.php @@ -16,6 +16,7 @@ use PHPCSUtils\Exceptions\TypeError; use PHPCSUtils\Exceptions\UnexpectedTokenType; use PHPCSUtils\Exceptions\ValueError; +use PHPCSUtils\Internal\AttributeHelper; use PHPCSUtils\Internal\Cache; use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Scopes; @@ -270,6 +271,31 @@ public static function getMemberProperties(File $phpcsFile, $stackPtr) return $returnValue; } + /** + * Retrieve the stack pointers to the attribute openers for any attribute block which applies to an OO property + * or function declaration parameters. + * + * @since 1.2.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the variable token to + * acquire the attributes for. + * + * @return array Array with the stack pointers to the applicable attribute openers + * or an empty array if there are no attributes attached to the OO property + * or function declaration parameter. + * + * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. + * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. + * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_VARIABLE` token. + * @throws \PHPCSUtils\Exceptions\ValueError If the token passed does not point to an OO property token + * or a parameter in a function declaration. + */ + public static function getAttributeOpeners(File $phpcsFile, $stackPtr) + { + return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'variable'); + } + /** * Verify if a given variable name is the name of a PHP reserved variable. * diff --git a/Tests/Internal/AttributeHelper/GetOpenersForConstantsTest.inc b/Tests/Internal/AttributeHelper/GetOpenersForConstantsTest.inc new file mode 100644 index 00000000..d27a7de9 --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForConstantsTest.inc @@ -0,0 +1,56 @@ +expectException('PHPCSUtils\Exceptions\TypeError'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type integer, boolean given'); + + Constants::getAttributeOpeners(self::$phpcsFile, false); + } + + /** + * Test receiving an exception when passing a non-existent token pointer. + * + * @return void + */ + public function testNonExistentToken() + { + $this->expectException('PHPCSUtils\Exceptions\OutOfBoundsStackPtr'); + $this->expectExceptionMessage( + 'Argument #2 ($stackPtr) must be a stack pointer which exists in the $phpcsFile object, 100000 given' + ); + + Constants::getAttributeOpeners(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non T_CONST token is passed. + * + * @return void + */ + public function testNotAcceptedTypeException() + { + $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type T_CONST;'); + + $targetPtr = $this->getTargetToken('/* testNotAConstToken */', \T_STRING); + Constants::getAttributeOpeners(self::$phpcsFile, $targetPtr); + } + + /** + * Test the getAttributeOpeners() method. + * + * @dataProvider dataGetAttributeOpeners + * + * @param string $identifier Comment which precedes the test case. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetAttributeOpeners($identifier, $expected) + { + $targetPtr = $this->getTargetToken($identifier, \T_CONST); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + $result = Constants::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * Note: token positions are offsets in relation to the position of the T_CONST token! + * + * @see testGetAttributeOpeners() + * + * @return array>> + */ + public static function dataGetAttributeOpeners() + { + $php8Names = parent::usesPhp8NameTokens(); + + return [ + 'global const, with 1 attribute block' => [ + 'identifier' => '/* testGlobalConstantWithAttribute */', + 'expected' => [-12], + ], + 'global const, no attributes' => [ + 'identifier' => '/* testGlobalConstantNoAttribute */', + 'expected' => [], + ], + + 'class const, no modifiers, 1 attribute block' => [ + 'identifier' => '/* testInClassNoModifiersWithAttribute */', + 'expected' => [-5], + ], + 'class const, final protected, no attributes' => [ + 'identifier' => '/* testInClassConstAsNameNoAttribute */', + 'expected' => [], + ], + 'class const, private, 1 attribute block' => [ + 'identifier' => '/* testInClass */', + 'expected' => [-18], + ], + + 'enum const, final, 3 attribute blocks' => [ + 'identifier' => '/* testInEnum */', + 'expected' => [ + -24, + -21, + -15, + ], + ], + 'trait const, final public, 3 attribute blocks' => [ + 'identifier' => '/* testInTrait */', + 'expected' => [ + ($php8Names === true ? -49 : -50), + ($php8Names === true ? -36 : -37), + -13, + ], + ], + 'interface const, private, 1 multi-line attribute block' => [ + 'identifier' => '/* testInInterface */', + 'expected' => [-18], + ], + 'anon class const, protected final, 1 attribute block, same line, nested in ternary' => [ + 'identifier' => '/* testInAnonClass */', + 'expected' => [-10], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testResultIsCached() + { + $methodName = 'PHPCSUtils\\Internal\\AttributeHelper::getOpeners'; + $cases = self::dataGetAttributeOpeners(); + $identifier = $cases['class const, private, 1 attribute block']['identifier']; + $expected = $cases['class const, private, 1 attribute block']['expected']; + + $targetPtr = $this->getTargetToken($identifier, \T_CONST); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = Constants::getAttributeOpeners(self::$phpcsFile, $targetPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, "{$targetPtr}-constant"); + $resultSecondRun = Constants::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + if ($origStatus === false) { + Cache::clear(); + } + Cache::$enabled = $origStatus; + + $this->assertSame($expected, $resultFirstRun, 'First result did not match expectation'); + $this->assertTrue($isCached, 'Cache::isCached() could not find the cached value'); + $this->assertSame($resultFirstRun, $resultSecondRun, 'Second result did not match first'); + } + + /** + * Test helper to translate token offsets to absolute positions in an "expected" array. + * + * @param int $targetPtr The token pointer to the target token from which + * the offset is calculated. + * @param array $expected The expected function output containing offsets. + * + * @return array + */ + private function updateExpectedTokenPositions($targetPtr, $expected) + { + foreach ($expected as $key => $value) { + $expected[$key] += $targetPtr; + } + + return $expected; + } +} diff --git a/Tests/Internal/AttributeHelper/GetOpenersForFunctionsTest.inc b/Tests/Internal/AttributeHelper/GetOpenersForFunctionsTest.inc new file mode 100644 index 00000000..07775429 --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForFunctionsTest.inc @@ -0,0 +1,68 @@ + do_something(); +$arrow = /* testArrowFnNoAttribute */ fn() => do_something(); +$arrow = #[AttributeA] + static /* testStaticArrowFnAttribute */ fn() => do_something(); + +class ClassWithMethod { + /* testInClassNoModifiersWithAttribute */ + #[AttributeA] + function noModifiers() {} + + final protected /* testInClassNoAttribute */ function noAttribute() {} + + #[WithAttribute(foo: 'bar'), MyAttribute] + private /* testInClass */ function inClass() {} +} + +enum EnumWithMethod { + #[AttributeA] + final /* testInEnum */ function inEnum() {} +} + +trait TraitWithMethod { + + #[AttributeA] + + /** Short docblock */ + + #[AttributeB, AttributeC, namespace\AttributeD] + + /** + * Docblock + */ + #[AttributeE] + + + abstract static public /* testInTrait */ function inTrait(); +} + +interface InterfaceWithMethod { + #[ + WithAttribute(/* comment */ 'baz') + ] + public /* testInInterface */ function inInterface(); +} + +$anonClass = ( $foo == $bar ? new stdClass() : + new class { + #[WithoutArgument]#[SingleArgument(0)]#[FewArguments('Hello', 'World')] + protected static /* testInAnonClass */ function inAnonClass() {} + } +); diff --git a/Tests/Internal/AttributeHelper/GetOpenersForFunctionsTest.php b/Tests/Internal/AttributeHelper/GetOpenersForFunctionsTest.php new file mode 100644 index 00000000..b30f2b68 --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForFunctionsTest.php @@ -0,0 +1,232 @@ +expectException('PHPCSUtils\Exceptions\TypeError'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type integer, boolean given'); + + FunctionDeclarations::getAttributeOpeners(self::$phpcsFile, false); + } + + /** + * Test receiving an exception when passing a non-existent token pointer. + * + * @return void + */ + public function testNonExistentToken() + { + $this->expectException('PHPCSUtils\Exceptions\OutOfBoundsStackPtr'); + $this->expectExceptionMessage( + 'Argument #2 ($stackPtr) must be a stack pointer which exists in the $phpcsFile object, 100000 given' + ); + + FunctionDeclarations::getAttributeOpeners(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non function keyword token is passed. + * + * @return void + */ + public function testNotAcceptedTypeException() + { + $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type T_FUNCTION, T_CLOSURE or T_FN;'); + + $targetPtr = $this->getTargetToken('/* testNotAFunctionToken */', \T_ECHO); + FunctionDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + } + + /** + * Test the getAttributeOpeners() method. + * + * @dataProvider dataGetAttributeOpeners + * + * @param string $identifier Comment which precedes the test case. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetAttributeOpeners($identifier, $expected) + { + $targetPtr = $this->getTargetToken($identifier, Collections::functionDeclarationTokens()); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + $result = FunctionDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * Note: token positions are offsets in relation to the position of the T_FUNCTION, T_CLOSURE or T_FN token! + * + * @see testGetAttributeOpeners() + * + * @return array>> + */ + public static function dataGetAttributeOpeners() + { + $php8Names = parent::usesPhp8NameTokens(); + + return [ + 'global function, with 1 attribute block' => [ + 'identifier' => '/* testGlobalFunctionWithAttribute */', + 'expected' => [-12], + ], + 'global function, no attributes' => [ + 'identifier' => '/* testGlobalFunctionNoAttribute */', + 'expected' => [], + ], + + 'closure, with 1 attribute block' => [ + 'identifier' => '/* testClosureAttributeSameLine */', + 'expected' => [-6], + ], + 'closure, no attributes' => [ + 'identifier' => '/* testClosureNoAttribute */', + 'expected' => [], + ], + 'closure, static, with 1 attribute block' => [ + 'identifier' => '/* testStaticClosureAttribute */', + 'expected' => [-9], + ], + + 'arrow function, with 1 attribute block' => [ + 'identifier' => '/* testArrowFnAttributeSameLine */', + 'expected' => [-6], + ], + 'arrow function, no attributes' => [ + 'identifier' => '/* testArrowFnNoAttribute */', + 'expected' => [], + ], + 'arrow function, static, with 1 attribute block' => [ + 'identifier' => '/* testStaticArrowFnAttribute */', + 'expected' => [-9], + ], + + 'class method, no modifiers, 1 attribute block' => [ + 'identifier' => '/* testInClassNoModifiersWithAttribute */', + 'expected' => [-5], + ], + 'class method, final protected, no attributes' => [ + 'identifier' => '/* testInClassNoAttribute */', + 'expected' => [], + ], + 'class method, private, 1 attribute block' => [ + 'identifier' => '/* testInClass */', + 'expected' => [-18], + ], + + 'enum method, final, 1 attribute block' => [ + 'identifier' => '/* testInEnum */', + 'expected' => [-9], + ], + 'trait method, abstract static public, 3 attribute blocks' => [ + 'identifier' => '/* testInTrait */', + 'expected' => [ + ($php8Names === true ? -51 : -53), + ($php8Names === true ? -38 : -40), + -15, + ], + ], + 'interface method, public, 1 multi-line attribute block' => [ + 'identifier' => '/* testInInterface */', + 'expected' => [-18], + ], + 'anon class method, protected static, 3 attribute blocks, nested in ternary' => [ + 'identifier' => '/* testInAnonClass */', + 'expected' => [ + -26, + -23, + -17, + ], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testResultIsCached() + { + $methodName = 'PHPCSUtils\\Internal\\AttributeHelper::getOpeners'; + $cases = self::dataGetAttributeOpeners(); + $identifier = $cases['arrow function, static, with 1 attribute block']['identifier']; + $expected = $cases['arrow function, static, with 1 attribute block']['expected']; + + $targetPtr = $this->getTargetToken($identifier, Collections::functionDeclarationTokens()); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = FunctionDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, "{$targetPtr}-function"); + $resultSecondRun = FunctionDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + if ($origStatus === false) { + Cache::clear(); + } + Cache::$enabled = $origStatus; + + $this->assertSame($expected, $resultFirstRun, 'First result did not match expectation'); + $this->assertTrue($isCached, 'Cache::isCached() could not find the cached value'); + $this->assertSame($resultFirstRun, $resultSecondRun, 'Second result did not match first'); + } + + /** + * Test helper to translate token offsets to absolute positions in an "expected" array. + * + * @param int $targetPtr The token pointer to the target token from which + * the offset is calculated. + * @param array $expected The expected function output containing offsets. + * + * @return array + */ + private function updateExpectedTokenPositions($targetPtr, $expected) + { + foreach ($expected as $key => $value) { + $expected[$key] += $targetPtr; + } + + return $expected; + } +} diff --git a/Tests/Internal/AttributeHelper/GetOpenersForOOTest.inc b/Tests/Internal/AttributeHelper/GetOpenersForOOTest.inc new file mode 100644 index 00000000..cf1a37ab --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForOOTest.inc @@ -0,0 +1,68 @@ +expectException('PHPCSUtils\Exceptions\TypeError'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type integer, boolean given'); + + ObjectDeclarations::getAttributeOpeners(self::$phpcsFile, false); + } + + /** + * Test receiving an exception when passing a non-existent token pointer. + * + * @return void + */ + public function testNonExistentToken() + { + $this->expectException('PHPCSUtils\Exceptions\OutOfBoundsStackPtr'); + $this->expectExceptionMessage( + 'Argument #2 ($stackPtr) must be a stack pointer which exists in the $phpcsFile object, 100000 given' + ); + + ObjectDeclarations::getAttributeOpeners(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non OO token is passed. + * + * @return void + */ + public function testNotAcceptedTypeException() + { + $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); + $this->expectExceptionMessage( + 'Argument #2 ($stackPtr) must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM;' + ); + + $targetPtr = $this->getTargetToken('/* testNotAnOOToken */', \T_CONSTANT_ENCAPSED_STRING); + ObjectDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + } + + /** + * Test the getAttributeOpeners() method. + * + * @dataProvider dataGetAttributeOpeners + * + * @param string $identifier Comment which precedes the test case. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetAttributeOpeners($identifier, $expected) + { + $targetPtr = $this->getTargetToken($identifier, Tokens::$ooScopeTokens); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + $result = ObjectDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * Note: token positions are offsets in relation to the position of the OO token! + * + * @see testGetAttributeOpeners() + * + * @return array>> + */ + public static function dataGetAttributeOpeners() + { + $php8Names = parent::usesPhp8NameTokens(); + + return [ + 'class, no modifiers, 1 attribute block' => [ + 'identifier' => '/* testClassNoModifiersWithAttribute */', + 'expected' => [-6], + ], + 'class, abstract readonly, 3 attribute blocks' => [ + 'identifier' => '/* testClassTwoModifiersWithAttribute */', + 'expected' => [ + -25, + -22, + -16, + ], + ], + 'class, readonly final, no attributes' => [ + 'identifier' => '/* testClassNoAttribute */', + 'expected' => [], + ], + 'class, readonly, 1 attribute block, same line' => [ + 'identifier' => '/* testClassOneModifierWithAttributeSameLine */', + 'expected' => [-17], + ], + + 'enum, 1 attribute block' => [ + 'identifier' => '/* testEnumAttribute */', + 'expected' => [-6], + ], + 'trait, 3 attribute blocks' => [ + 'identifier' => '/* testTraitAttribute */', + 'expected' => [ + ($php8Names === true ? -40 : -43), + ($php8Names === true ? -29 : -32), + -8, + ], + ], + 'interface, 1 multi-line attribute block' => [ + 'identifier' => '/* testInterfaceAttribute */', + 'expected' => [-14], + ], + + 'anon class, no modifiers, 1 attribute block, same line' => [ + 'identifier' => '/* testAnonClassAttributeSameLine */', + 'expected' => [-6], + ], + 'anon class, no modifiers, no attributes' => [ + 'identifier' => '/* testAnonClassNoAttribute */', + 'expected' => [], + ], + 'anon class, readonly, 1 attribute block, same line' => [ + 'identifier' => '/* testReadonlyAnonClassAttributeSameLine */', + 'expected' => [-8], + ], + 'anon class, readonly, no attributes' => [ + 'identifier' => '/* testReadonlyAnonClassNoAttributes */', + 'expected' => [], + ], + 'anon class, readonly, 1 attribute block, line before' => [ + 'identifier' => '/* testReadonlyAnonClassAttribute */', + 'expected' => [-9], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testResultIsCached() + { + $methodName = 'PHPCSUtils\\Internal\\AttributeHelper::getOpeners'; + $cases = self::dataGetAttributeOpeners(); + $identifier = $cases['interface, 1 multi-line attribute block']['identifier']; + $expected = $cases['interface, 1 multi-line attribute block']['expected']; + + $targetPtr = $this->getTargetToken($identifier, Tokens::$ooScopeTokens); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = ObjectDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, "{$targetPtr}-OO"); + $resultSecondRun = ObjectDeclarations::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + if ($origStatus === false) { + Cache::clear(); + } + Cache::$enabled = $origStatus; + + $this->assertSame($expected, $resultFirstRun, 'First result did not match expectation'); + $this->assertTrue($isCached, 'Cache::isCached() could not find the cached value'); + $this->assertSame($resultFirstRun, $resultSecondRun, 'Second result did not match first'); + } + + /** + * Test helper to translate token offsets to absolute positions in an "expected" array. + * + * @param int $targetPtr The token pointer to the target token from which + * the offset is calculated. + * @param array $expected The expected function output containing offsets. + * + * @return array + */ + private function updateExpectedTokenPositions($targetPtr, $expected) + { + foreach ($expected as $key => $value) { + $expected[$key] += $targetPtr; + } + + return $expected; + } +} diff --git a/Tests/Internal/AttributeHelper/GetOpenersForVariablesParseError1Test.inc b/Tests/Internal/AttributeHelper/GetOpenersForVariablesParseError1Test.inc new file mode 100644 index 00000000..0494f8ed --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForVariablesParseError1Test.inc @@ -0,0 +1,10 @@ +expectException('PHPCSUtils\Exceptions\ValueError'); + $this->expectExceptionMessage( + 'argument #2 ($stackPtr) must be the pointer to an OO property or a parameter in a function declaration.' + ); + + $targetPtr = $this->getTargetToken('/* testParseError */', \T_VARIABLE); + Variables::getAttributeOpeners(self::$phpcsFile, $targetPtr); + } +} diff --git a/Tests/Internal/AttributeHelper/GetOpenersForVariablesTest.inc b/Tests/Internal/AttributeHelper/GetOpenersForVariablesTest.inc new file mode 100644 index 00000000..57154685 --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForVariablesTest.inc @@ -0,0 +1,142 @@ + $paramA * 10; + +abstract class PropertiesAndParamsWithAttributes { + + #[AttributeA] + var /* testOOPropertyVarNoTypeWithAttribute */ $untyped; + + #[AttributeA] + + /** Short docblock */ + + #[AttributeB, \AttributeC, namespace\AttributeD] + + /** + * Docblock + */ + #[AttributeE] + + + public abstract int /* testOOPropertyPublicAbstractPlainTypeWithMultipleAttributes */ $publicAbstract; // Note: this test should have { get;}, but that will be broken until support for property hooks is added to PHPCS. + + #[AttributeA] + final protected readonly string /* testOOPropertyFinalProtectedReadonlyPlainTypeWithAttribute */ $finalProtectedReadonly; + + #[AttributeA] + private static /* testOOPropertyPrivateStaticNoTypeWithAttribute */ $privateStatic; + + #[WithAttribute(foo: 'bar'), MyAttribute] + public(set) int /* testOOPropertyAsymPublicPlainTypeWithAttribute */ $asymPublicPlainType; + + #[WithoutArgument]#[SingleArgument(0)]#[FewArguments('Hello', 'World')] + final protected(set) ?string /* testOOPropertyFinalAsymProtectedNullableTypeWithAttribute */ $asymProtectedNullableType; + + public(set) ?bool /* testOOPropertyAsymPublicNullableTypeNoAttributes */ $asymPublicNullableType; + + #[AttributeA] private(set) public MyType /* testOOPropertyAsymPrivatePublicOOTypeWithAttributeSameLine */ $asymPrivateOOType; + + #[ + WithAttribute(/* comment */ 'baz') + ] + public final namespace\MyType /* testOOPropertyPublicFinalNamespaceRelativeTypeWithAttribute */ $publicFinalNamespaceRelativeType; + + #[AttributeA] + protected private(set) final Partially\Qualified\MyType + /* testOOPropertyProtectedAsymPrivateFinalPartiallyQualifiedTypeWithAttribute */ $asymPartiallyQualifiedType; + + #[AttributeA] + #[AttributeB, AttributeC, namespace\AttributeD] + #[AttributeE] + readonly \Fully\Qualified\MyType /* testOOPropertyReadonlyFullyQualifiedTypeWithAttribute */ $readonlyFullyQualifiedType; + + #[AttributeA] + public int|string|bool|float|array|object|iterable|null|true|false|self|parent|Type + /* testOOPropertyPublicAllValidTypesWithAttribute */ $publicUnionTypeAllTypes; + + #[WithAttribute(foo: 'bar'), MyAttribute] + final \FQN|namespace\Relative /* testOOPropertyFinalUnionOOTypesWithAttribute */ $finalUnionType; + + #[AttributeA] + static Partially\Qualified&OtherType /* testOOPropertyStaticIntersectionOOTypesWithAttribute */ $staticIntersectionType; + + #[ + WithAttribute(/* comment */ 'baz') + ] + protected(set) static final false|(NameA&\NameB)|null + /* testOOPropertyAsymProtectedStaticFinalDNFTypeWithAttribute */ $asymProtectedStaticFinalDNFType; + + #[AttributeA] + public int $propA = 10, + $propB = 20, + /* testOOMultiPropertyLastWithAttribute */ + $propC = 'target'; + + public function __construct( + #[AttributeA] + public(set) final MyType|false /* testCPPPropAsymPublicStaticUnionTypeWithAttribute */ $propA, + #[AttributeA] final protected ?iterable /* testCPPPropFinalProtectedNullableTypeWithAttribute */ $propB { get; set; }, + private(set) \TypeA&TypeB /* testCPPPropAsymPrivateIntersectionTypeNoAttributes */ $propC, + #[WithoutArgument]#[SingleArgument(0)] + + + #[FewArguments('Hello', 'World')] + readonly string /* testCPPPropReadonlyPlainTypeWithAttribute */ $propD, + #[AttributeA] ?object &... /* testConstructorMethodParamNullableTypeRefAndSpreadWithAttributeSameLine */ $param, + ) {} + + protected function notConstructor( + #[AttributeA] int /* testMethodParamTypedWithAttributeSameLine */ $paramA, + ?float /* testMethodParamNullableTypeNoAttribute */ $paramB, + #[AttributeA, AttributeB(10)] + TypeA|\TypeB & /* testMethodParamUnionTypeRefWithAttribute */ $paramC, + #[ + WithAttribute(/* comment */ 'baz') + ] + (DNF&Type)|false &... /* testMethodParamDNFTypeRefAndSpreadWithAttribute */ $paramD, + ) {} + + #[ExampleAttribute] + public function alsoNotConstructor(#[ExampleAttribute] /* testMethodParamNoTypeSameLine */ $bar) {} +} diff --git a/Tests/Internal/AttributeHelper/GetOpenersForVariablesTest.php b/Tests/Internal/AttributeHelper/GetOpenersForVariablesTest.php new file mode 100644 index 00000000..0f60aaef --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersForVariablesTest.php @@ -0,0 +1,402 @@ +expectException('PHPCSUtils\Exceptions\TypeError'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type integer, boolean given'); + + Variables::getAttributeOpeners(self::$phpcsFile, false); + } + + /** + * Test receiving an exception when passing a non-existent token pointer. + * + * @return void + */ + public function testNonExistentToken() + { + $this->expectException('PHPCSUtils\Exceptions\OutOfBoundsStackPtr'); + $this->expectExceptionMessage( + 'Argument #2 ($stackPtr) must be a stack pointer which exists in the $phpcsFile object, 100000 given' + ); + + Variables::getAttributeOpeners(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non variable token is passed. + * + * @return void + */ + public function testNotAcceptedTypeException() + { + $this->expectException('PHPCSUtils\Exceptions\UnexpectedTokenType'); + $this->expectExceptionMessage('Argument #2 ($stackPtr) must be of type T_VARIABLE;'); + + $targetPtr = $this->getTargetToken('/* testNotAVariableToken */', \T_ECHO); + Variables::getAttributeOpeners(self::$phpcsFile, $targetPtr); + } + + /** + * Test receiving an expected exception when a variable token is passed, which is not + * an OO property, nor a function parameter. + * + * @dataProvider dataNotPropertyOrParamException + * + * @param string $identifier Comment which precedes the test case. + * + * @return void + */ + public function testNotPropertyOrParamException($identifier) + { + $this->expectException('PHPCSUtils\Exceptions\ValueError'); + $this->expectExceptionMessage( + 'argument #2 ($stackPtr) must be the pointer to an OO property or a parameter in a function declaration.' + ); + + $targetPtr = $this->getTargetToken($identifier, \T_VARIABLE); + Variables::getAttributeOpeners(self::$phpcsFile, $targetPtr); + } + + /** + * Data provider. + * + * @see testNotPropertyOrParamException() + * + * @return array> + */ + public static function dataNotPropertyOrParamException() + { + return [ + 'ordinary variable' => [ + 'identifier' => '/* testVariableNotPropertyNorParam1 */', + ], + 'static variable within function' => [ + 'identifier' => '/* testVariableNotPropertyNorParam2 */', + ], + ]; + } + + /** + * Test the getAttributeOpeners() method. + * + * @dataProvider dataGetAttributeOpenersParams + * @dataProvider dataGetAttributeOpenersProps + * + * @param string $identifier Comment which precedes the test case. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetAttributeOpeners($identifier, $expected) + { + $targetPtr = $this->getTargetToken($identifier, \T_VARIABLE); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + $result = Variables::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * Note: token positions are offsets in relation to the position of the T_VARIABLE token! + * + * @see testGetAttributeOpeners() + * + * @return array>> + */ + public static function dataGetAttributeOpenersParams() + { + $php8Names = parent::usesPhp8NameTokens(); + + return [ + 'global function param, no type, with 1 attribute block' => [ + 'identifier' => '/* testFunctionParamWithAttribute */', + 'expected' => [-7], + ], + 'global function param, no type, no attributes' => [ + 'identifier' => '/* testFunctionParamNoAttribute */', + 'expected' => [], + ], + 'global function param, no type, ref, with 1 attribute block' => [ + 'identifier' => '/* testFunctionParamRefWithAttribute */', + 'expected' => [-15], + ], + 'global function param, no type, ref, no attributes' => [ + 'identifier' => '/* testFunctionParamRefNoAttribute */', + 'expected' => [], + ], + 'global function param, no type, spread, with 2 attribute blocks' => [ + 'identifier' => '/* testFunctionParamSpreadWithAttribute */', + 'expected' => [ + -14, + -9, + ], + ], + 'global function param, no type, spread, no attributes' => [ + 'identifier' => '/* testFunctionParamSpreadNoAttribute */', + 'expected' => [], + ], + 'global function param, no type, ref and spread, with 1 attribute block' => [ + 'identifier' => '/* testFunctionParamRefAndSpreadWithAttribute */', + 'expected' => [-19], + ], + + 'closure param, no type, with 1 attribute block' => [ + 'identifier' => '/* testClosureParamWithAttributeSameLine */', + 'expected' => [-6], + ], + 'closure param, no type, no attributes' => [ + 'identifier' => '/* testClosureParamNoAttribute */', + 'expected' => [], + ], + + 'arrow function param, no type, with 1 attribute block' => [ + 'identifier' => '/* testArrowParamWithAttributeSameLine */', + 'expected' => [-6], + ], + 'arrow function param, no type, no attributes' => [ + 'identifier' => '/* testArrowParamNoAttribute */', + 'expected' => [], + ], + + 'constructor prop promotion, public(set) static, union type, with 1 attribute block' => [ + 'identifier' => '/* testCPPPropAsymPublicStaticUnionTypeWithAttribute */', + 'expected' => [-15], + ], + 'constructor prop promotion, final protected, nullable type, with 1 attribute block, same line' => [ + 'identifier' => '/* testCPPPropFinalProtectedNullableTypeWithAttribute */', + 'expected' => [-13], + ], + 'constructor prop promotion, private(set), intersection type, no attributes' => [ + 'identifier' => '/* testCPPPropAsymPrivateIntersectionTypeNoAttributes */', + 'expected' => [], + ], + 'constructor prop promotion, readonly , plain type, with 3 attribute blocks' => [ + 'identifier' => '/* testCPPPropReadonlyPlainTypeWithAttribute */', + 'expected' => [ + -30, + -27, + -17, + ], + ], + 'constructor param, nullable type, ref and spread, with 1 attribute block' => [ + 'identifier' => '/* testConstructorMethodParamNullableTypeRefAndSpreadWithAttributeSameLine */', + 'expected' => [-12], + ], + + 'method param, plain type, with 1 attribute block' => [ + 'identifier' => '/* testMethodParamTypedWithAttributeSameLine */', + 'expected' => [-8], + ], + 'method param, nullable type, ref and spread, no attributes' => [ + 'identifier' => '/* testMethodParamNullableTypeNoAttribute */', + 'expected' => [], + ], + 'method param, union type, ref, with 1 attribute block' => [ + 'identifier' => '/* testMethodParamUnionTypeRefWithAttribute */', + 'expected' => [ + ($php8Names === true ? -19 : -20), + ], + ], + 'method param, DNF type, ref and spread, with 1 attribute block' => [ + 'identifier' => '/* testMethodParamDNFTypeRefAndSpreadWithAttribute */', + 'expected' => [-27], + ], + + 'method param, no type, with 1 attribute block, same line' => [ + 'identifier' => '/* testMethodParamNoTypeSameLine */', + 'expected' => [-6], + ], + ]; + } + + /** + * Data provider. + * + * Note: token positions are offsets in relation to the position of the T_VARIABLE token! + * + * @see testGetAttributeOpeners() + * + * @return array>> + */ + public static function dataGetAttributeOpenersProps() + { + $php8Names = parent::usesPhp8NameTokens(); + + return [ + 'OO prop, var, no type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyVarNoTypeWithAttribute */', + 'expected' => [-9], + ], + 'OO prop, public abstract, plain type, with 3 attribute blocks' => [ + 'identifier' => '/* testOOPropertyPublicAbstractPlainTypeWithMultipleAttributes */', + 'expected' => [ + ($php8Names === true ? -51 : -54), + ($php8Names === true ? -38 : -41), + -15, + ], + ], + 'OO prop, final protected readonly, plain type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyFinalProtectedReadonlyPlainTypeWithAttribute */', + 'expected' => [-15], + ], + 'OO prop, public static, no type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyPrivateStaticNoTypeWithAttribute */', + 'expected' => [-11], + ], + 'OO prop, public(set), plain type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyAsymPublicPlainTypeWithAttribute */', + 'expected' => [-20], + ], + 'OO prop, final protected(set), nullable type, with 3 attribute blocks' => [ + 'identifier' => '/* testOOPropertyFinalAsymProtectedNullableTypeWithAttribute */', + 'expected' => [ + -29, + -26, + -20, + ], + ], + 'OO prop, public(set), nullable type, no attributes' => [ + 'identifier' => '/* testOOPropertyAsymPublicNullableTypeNoAttributes */', + 'expected' => [], + ], + 'OO prop, private(set) public, OO type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyAsymPrivatePublicOOTypeWithAttributeSameLine */', + 'expected' => [-12], + ], + 'OO prop, public final, namespace relative OO type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyPublicFinalNamespaceRelativeTypeWithAttribute */', + 'expected' => [ + ($php8Names === true ? -22 : -24), + ], + ], + 'OO prop, protected private(set) final, partially qualified OO type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyProtectedAsymPrivateFinalPartiallyQualifiedTypeWithAttribute */', + 'expected' => [ + ($php8Names === true ? -16 : -20), + ], + ], + 'OO prop, readonly, fully qualified OO type, with 3 attribute blocks' => [ + 'identifier' => '/* testOOPropertyReadonlyFullyQualifiedTypeWithAttribute */', + 'expected' => [ + ($php8Names === true ? -27 : -34), + ($php8Names === true ? -22 : -29), + ($php8Names === true ? -11 : -16), + ], + ], + 'OO prop, public, all types, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyPublicAllValidTypesWithAttribute */', + 'expected' => [-36], + ], + 'OO prop, final, union type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyFinalUnionOOTypesWithAttribute */', + 'expected' => [ + ($php8Names === true ? -22 : -25), + ], + ], + 'OO prop, static, intersection type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyStaticIntersectionOOTypesWithAttribute */', + 'expected' => [ + ($php8Names === true ? -13 : -15), + ], + ], + 'OO prop, protected(set) static final, DNF type, with 1 attribute block' => [ + 'identifier' => '/* testOOPropertyAsymProtectedStaticFinalDNFTypeWithAttribute */', + 'expected' => [ + ($php8Names === true ? -33 : -34), + ], + ], + 'OO multi-prop, public, plain type, with 1 attribute block' => [ + 'identifier' => '/* testOOMultiPropertyLastWithAttribute */', + 'expected' => [-28], + ], + + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testResultIsCached() + { + $methodName = 'PHPCSUtils\\Internal\\AttributeHelper::getOpeners'; + $cases = self::dataGetAttributeOpenersProps(); + $identifier = $cases['OO prop, public final, namespace relative OO type, with 1 attribute block']['identifier']; + $expected = $cases['OO prop, public final, namespace relative OO type, with 1 attribute block']['expected']; + + $targetPtr = $this->getTargetToken($identifier, \T_VARIABLE); + $expected = $this->updateExpectedTokenPositions($targetPtr, $expected); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = Variables::getAttributeOpeners(self::$phpcsFile, $targetPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, "{$targetPtr}-variable"); + $resultSecondRun = Variables::getAttributeOpeners(self::$phpcsFile, $targetPtr); + + if ($origStatus === false) { + Cache::clear(); + } + Cache::$enabled = $origStatus; + + $this->assertSame($expected, $resultFirstRun, 'First result did not match expectation'); + $this->assertTrue($isCached, 'Cache::isCached() could not find the cached value'); + $this->assertSame($resultFirstRun, $resultSecondRun, 'Second result did not match first'); + } + + /** + * Test helper to translate token offsets to absolute positions in an "expected" array. + * + * @param int $targetPtr The token pointer to the target token from which + * the offset is calculated. + * @param array $expected The expected function output containing offsets. + * + * @return array + */ + private function updateExpectedTokenPositions($targetPtr, $expected) + { + foreach ($expected as $key => $value) { + $expected[$key] += $targetPtr; + } + + return $expected; + } +} diff --git a/Tests/Internal/AttributeHelper/GetOpenersMiscTest.inc b/Tests/Internal/AttributeHelper/GetOpenersMiscTest.inc new file mode 100644 index 00000000..d04cc775 --- /dev/null +++ b/Tests/Internal/AttributeHelper/GetOpenersMiscTest.inc @@ -0,0 +1,3 @@ +expectException('PHPCSUtils\Exceptions\TypeError'); + $this->expectExceptionMessage('Argument #3 ($type) must be of type string, boolean given.'); + + AttributeHelper::getOpeners(self::$phpcsFile, 2, false); + } + + /** + * Test receiving an exception when the passed "type" is not one of the recognized ones. + * + * @return void + */ + public function testInvalidType() + { + $this->expectException('PHPCSUtils\Exceptions\ValueError'); + $this->expectExceptionMessage( + 'The value of argument #3 ($type) must be one of the following: constant, function, OO, variable.' + ); + + AttributeHelper::getOpeners(self::$phpcsFile, 2, 'invalid'); + } +} diff --git a/Tests/Utils/Constants/README-GetAttributeOpenersTests.md b/Tests/Utils/Constants/README-GetAttributeOpenersTests.md new file mode 100644 index 00000000..474e9f89 --- /dev/null +++ b/Tests/Utils/Constants/README-GetAttributeOpenersTests.md @@ -0,0 +1,3 @@ +# Regarding tests for `Constant::getAttributeOpeners()` + +The tests for this method can be found in the `Tests/Internal/AttributeHelper` folder. diff --git a/Tests/Utils/FunctionDeclarations/README-GetAttributeOpenersTests.md b/Tests/Utils/FunctionDeclarations/README-GetAttributeOpenersTests.md new file mode 100644 index 00000000..d9f27020 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/README-GetAttributeOpenersTests.md @@ -0,0 +1,3 @@ +# Regarding tests for `FunctionDeclarations::getAttributeOpeners()` + +The tests for this method can be found in the `Tests/Internal/AttributeHelper` folder. diff --git a/Tests/Utils/ObjectDeclarations/README-GetAttributeOpenersTests.md b/Tests/Utils/ObjectDeclarations/README-GetAttributeOpenersTests.md new file mode 100644 index 00000000..f6601934 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/README-GetAttributeOpenersTests.md @@ -0,0 +1,3 @@ +# Regarding tests for `ObjectDeclarations::getAttributeOpeners()` + +The tests for this method can be found in the `Tests/Internal/AttributeHelper` folder. diff --git a/Tests/Utils/Variables/README-GetAttributeOpenersTests.md b/Tests/Utils/Variables/README-GetAttributeOpenersTests.md new file mode 100644 index 00000000..f31a9605 --- /dev/null +++ b/Tests/Utils/Variables/README-GetAttributeOpenersTests.md @@ -0,0 +1,3 @@ +# Regarding tests for `Variables::getAttributeOpeners()` + +The tests for this method can be found in the `Tests/Internal/AttributeHelper` folder.