diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php index ffa346ae..0c15b831 100644 --- a/PHPCSUtils/Utils/ObjectDeclarations.php +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -13,7 +13,9 @@ use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Internal\Cache; use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\FunctionDeclarations; use PHPCSUtils\Utils\GetTokensAsString; /** @@ -356,4 +358,242 @@ private static function findNames(File $phpcsFile, $stackPtr, $keyword, array $a return $names; } + + /** + * Retrieve all constants declared in an OO structure. + * + * @since 1.1.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack position of the OO keyword. + * + * @return array Array with names of the found constants as keys and the stack pointers + * to the T_CONST token for each constant as values. + * If no constants are found or a parse error is encountered, + * an empty array is returned. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or + * is not an OO keyword token. + */ + public static function getDeclaredConstants(File $phpcsFile, $stackPtr) + { + return self::analyzeOOStructure($phpcsFile, $stackPtr)['constants']; + } + + /** + * Retrieve all cases declared in an enum. + * + * @since 1.1.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack position of the OO keyword. + * + * @return array Array with names of the found cases as keys and the stack pointers + * to the T_ENUM_CASE token for each case as values. + * If no cases are found or a parse error is encountered, + * an empty array is returned. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or + * is not a T_ENUM token. + */ + public static function getDeclaredEnumCases(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_ENUM) { + throw new RuntimeException('$stackPtr must be of type T_ENUM'); + } + + return self::analyzeOOStructure($phpcsFile, $stackPtr)['cases']; + } + + /** + * Retrieve all properties declared in an OO structure. + * + * Note: interfaces and enums cannot contain properties. This method does not take this into + * account to allow sniffs to flag this kind of incorrect PHP code. + * + * @since 1.1.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack position of the OO keyword. + * + * @return array Array with names of the found properties as keys and the stack pointers + * to the T_VARIABLE token for each property as values. + * If no properties are found or a parse error is encountered, + * an empty array is returned. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or + * is not an OO keyword token. + */ + public static function getDeclaredProperties(File $phpcsFile, $stackPtr) + { + return self::analyzeOOStructure($phpcsFile, $stackPtr)['properties']; + } + + /** + * Retrieve all methods declared in an OO structure. + * + * @since 1.1.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack pointer to the OO keyword. + * + * @return array Array with names of the found methods as keys and the stack pointers + * to the T_FUNCTION keyword for each method as values. + * If no methods are found or a parse error is encountered, + * an empty array is returned. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or + * is not an OO keyword token. + */ + public static function getDeclaredMethods(File $phpcsFile, $stackPtr) + { + return self::analyzeOOStructure($phpcsFile, $stackPtr)['methods']; + } + + /** + * Retrieve all constants, cases, properties and methods in an OO structure. + * + * @since 1.1.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack position of the OO keyword. + * + * @return array> Multi-dimensional array with four keys: + * - "constants" + * - "cases" + * - "properties" + * - "methods" + * Each index holds an associative array with the name of the "thing" + * as the key and the stack pointer to the related token as the value. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or + * is not an OO keyword token. + */ + private static function analyzeOOStructure(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || isset(Tokens::$ooScopeTokens[$tokens[$stackPtr]['code']]) === false + ) { + throw new RuntimeException( + '$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM' + ); + } + + // Set defaults. + $found = [ + 'constants' => [], + 'cases' => [], + 'properties' => [], + 'methods' => [], + ]; + + if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) { + return $found; + } + + if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { + return Cache::get($phpcsFile, __METHOD__, $stackPtr); + } + + for ($i = ($tokens[$stackPtr]['scope_opener'] + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) { + // Skip over potentially large docblocks. + if (isset($tokens[$i]['comment_closer']) === true) { + $i = $tokens[$i]['comment_closer']; + continue; + } + + // Skip over attributes. + if (isset($tokens[$i]['attribute_closer']) === true) { + $i = $tokens[$i]['attribute_closer']; + continue; + } + + // Skip over trait imports with conflict resolution. + if ($tokens[$i]['code'] === \T_USE + && isset($tokens[$i]['scope_closer']) === true + ) { + $i = $tokens[$i]['scope_closer']; + continue; + } + + // Defensive coding against parse errors. + if ($tokens[$i]['code'] === \T_CLOSURE + && isset($tokens[$i]['scope_closer']) === true + ) { + $i = $tokens[$i]['scope_closer']; + continue; + } + + switch ($tokens[$i]['code']) { + case \T_CONST: + $assignmentPtr = $phpcsFile->findNext([\T_EQUAL, \T_SEMICOLON, \T_CLOSE_CURLY_BRACKET], ($i + 1)); + if ($assignmentPtr === false || $tokens[$assignmentPtr]['code'] !== \T_EQUAL) { + // Probably a parse error. Ignore. + continue 2; + } + + $namePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($assignmentPtr - 1), ($i + 1), true); + if ($namePtr === false || $tokens[$namePtr]['code'] !== \T_STRING) { + // Probably a parse error. Ignore. + continue 2; + } + + $found['constants'][$tokens[$namePtr]['content']] = $i; + + // Skip to the assignment pointer, no need to double walk. + $i = $assignmentPtr; + break; + + case \T_ENUM_CASE: + $namePtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); + if ($namePtr === false || $tokens[$namePtr]['code'] !== \T_STRING) { + // Probably a parse error. Ignore. + continue 2; + } + + $name = $tokens[$namePtr]['content']; + $found['cases'][$name] = $i; + + // Skip to the name pointer, no need to double walk. + $i = $namePtr; + break; + + case \T_VARIABLE: + $name = $tokens[$i]['content']; + $found['properties'][$name] = $i; + break; + + case \T_FUNCTION: + $name = self::getName($phpcsFile, $i); + if (\is_string($name) && $name !== '') { + $found['methods'][$name] = $i; + + if (\strtolower($name) === '__construct') { + // Check for constructor property promotion. + $parameters = FunctionDeclarations::getParameters($phpcsFile, $i); + foreach ($parameters as $param) { + if (isset($param['property_visibility'])) { + $found['properties'][$param['name']] = $param['token']; + } + } + } + } + + if (isset($tokens[$i]['scope_closer']) === true) { + // Skip over the contents of the method, including the parameters. + $i = $tokens[$i]['scope_closer']; + } elseif (isset($tokens[$i]['parenthesis_closer']) === true) { + // Skip over the contents of an abstract/interface method, including the parameters. + $i = $tokens[$i]['parenthesis_closer']; + } + break; + } + } + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $found); + return $found; + } } diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/AnalyzeOOStructureTest.inc b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/AnalyzeOOStructureTest.inc new file mode 100644 index 00000000..8be0a291 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/AnalyzeOOStructureTest.inc @@ -0,0 +1,347 @@ + 'Red', + Suit::Clubs, Suit::Spades => 'Black', + }; + } + + /* markerEnumMethod2 */ + public function offsetGet($val) : mixed { + // Do something. + } + + /* markerEnumMethod3 */ + public function offsetExists($val): bool { + // Do something. + } + + /* markerEnumMethod4 */ + public function offsetSet($offset, $val): void { + throw new Exception(); + } + + /* markerEnumMethod5 */ + public function offsetUnset($val): void { + throw new Exception(); + } +} + +/* testClassConstructorNoParams */ +class ConstructorNoParams { + /* markerCPP1_Constructor */ + public function __construct() {} +} + +/* testInterfaceConstructorWithParamsNotProperties */ +interface ConstructorWithParamsNotProperties { + /* markerCPP2_Constructor */ + public function __Construct(int $paramA, Union|null $paramB, false|(D&N&F) $paramC = false); +} + +/* testClassConstructorWithProperties */ +class ConstructorWithProperties { + /* markerCPP3_Constructor */ + public function __CONSTRUCT( + /* markerCPP3_Property1 */ + private int $propA, + /* markerCPP3_Property2 */ + protected readonly string $propB, + /* markerCPP3_Property3 */ + readonly Onion|Union $propC, + /* markerCPP3_Property4 */ + public (D&N)|F $propD, + ) {} +} + +/* testTraitConstructorWithParamsAndProperties */ +trait ConstructorWithParamsAndProperties { + /* markerCPP4_Property1 */ + public int $declared; + + /* markerCPP4_Property2 */ + private self $instance; + + /* markerCPP4_Constructor */ + public function __construct( + /* markerCPP4_Property3 */ + public ?string $propA, + /* markerCPP4_Property4 */ + protected \MyClass $propB, + /* markerCPP4_Property5 */ + private namespace\Relative|Partially\Qualified $propC, + ?int $paramA, + string $paramB, + ) {} +} diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredConstantsTest.php b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredConstantsTest.php new file mode 100644 index 00000000..ab70eeda --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredConstantsTest.php @@ -0,0 +1,226 @@ +expectPhpcsException('$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + ObjectDeclarations::getDeclaredConstants(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non-OO token is passed. + * + * @return void + */ + public function testNotTargetToken() + { + $this->expectPhpcsException('$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + + $stackPtr = $this->getTargetToken('/* testUnacceptableToken */', \T_FUNCTION); + ObjectDeclarations::getDeclaredConstants(self::$phpcsFile, $stackPtr); + } + + /** + * Test retrieving the constants declared in an OO structure. + * + * @dataProvider dataGetDeclaredConstants + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function return value. + * + * @return void + */ + public function testGetDeclaredConstants($testMarker, $expected) + { + // Translate the constant markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_CONST]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + $result = ObjectDeclarations::getDeclaredConstants(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetDeclaredConstants() For the array format. + * + * @return array>> + */ + public static function dataGetDeclaredConstants() + { + return [ + 'empty class' => [ + 'testMarker' => '/* testEmptyClass */', + 'expected' => [], + ], + 'empty interface' => [ + 'testMarker' => '/* testEmptyInterface */', + 'expected' => [], + ], + 'empty trait' => [ + 'testMarker' => '/* testEmptyTrait */', + 'expected' => [], + ], + 'empty enum' => [ + 'testMarker' => '/* testEmptyEnum */', + 'expected' => [], + ], + 'empty anonymous class' => [ + 'testMarker' => '/* testEmptyAnonClass */', + 'expected' => [], + ], + 'class with constants, properties, methods and everything else' => [ + 'testMarker' => '/* testClass */', + 'expected' => [ + 'CONST' => '/* markerClassConst1 */', + 'CASE' => '/* markerClassConst2 */', + 'FINAL' => '/* markerClassConst3 */', + 'FUNCTION' => '/* markerClassConst4 */', + 'UNION_TYPED' => '/* markerClassConst5 */', + 'DNF_TYPED' => '/* markerClassConst6 */', + ], + ], + 'anonymous class with constants' => [ + 'testMarker' => '/* testAnonClass */', + 'expected' => [], + ], + 'interface with constants and methods' => [ + 'testMarker' => '/* testInterface */', + 'expected' => [ + 'IM' => '/* markerInterfaceConst1 */', + 'WALKING' => '/* markerInterfaceConst2 */', + 'ON' => '/* markerInterfaceConst3 */', + 'SUNSHINE' => '/* markerInterfaceConst4 */', + ], + ], + 'trait with constants, properties and methods' => [ + 'testMarker' => '/* testTrait */', + 'expected' => [ + 'DO' => '/* markerTraitConst1 */', + 'RE' => '/* markerTraitConst2 */', + 'mi' => '/* markerTraitConst3 */', + ], + ], + 'anon class with constants, properties and methods in unconventional order' => [ + 'testMarker' => '/* testAnonClassUnconventionalOrder */', + 'expected' => [ + 'AndIveMadeUp' => '/* markerAnonOrderConst1 */', + 'WAITING_ON_LOVES' => '/* markerAnonOrderConst2 */', + ], + ], + 'enum with constants, cases and methods' => [ + 'testMarker' => '/* testEnum */', + 'expected' => [ + 'SUIT' => '/* markerEnumConst1 */', + 'ANOTHER' => '/* markerEnumConst2 */', + ], + ], + 'class with constructor, no properties, no params' => [ + 'testMarker' => '/* testClassConstructorNoParams */', + 'expected' => [], + ], + 'interface with constructor, no properties, has params' => [ + 'testMarker' => '/* testInterfaceConstructorWithParamsNotProperties */', + 'expected' => [], + ], + 'class with constructor, with properties, no params' => [ + 'testMarker' => '/* testClassConstructorWithProperties */', + 'expected' => [], + ], + 'trait with constructor, with properties, with params' => [ + 'testMarker' => '/* testTraitConstructorWithParamsAndProperties */', + 'expected' => [], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testGetDeclaredConstantsResultIsCached() + { + $methodName = 'PHPCSUtils\\Utils\\ObjectDeclarations::analyzeOOStructure'; + $cases = $this->dataGetDeclaredConstants(); + $testMarker = $cases['class with constants, properties, methods and everything else']['testMarker']; + $expected = $cases['class with constants, properties, methods and everything else']['expected']; + + // Translate the constant markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_CONST]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = ObjectDeclarations::getDeclaredConstants(self::$phpcsFile, $stackPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, $stackPtr); + $resultSecondRun = ObjectDeclarations::getDeclaredConstants(self::$phpcsFile, $stackPtr); + + 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'); + } +} diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredEnumCasesTest.php b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredEnumCasesTest.php new file mode 100644 index 00000000..55528b9b --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredEnumCasesTest.php @@ -0,0 +1,157 @@ +expectPhpcsException('$stackPtr must be of type T_ENUM'); + ObjectDeclarations::getDeclaredEnumCases(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non-OO token is passed. + * + * @return void + */ + public function testNotTargetToken() + { + $this->expectPhpcsException('$stackPtr must be of type T_ENUM'); + + $stackPtr = $this->getTargetToken('/* testUnacceptableToken */', \T_FUNCTION); + ObjectDeclarations::getDeclaredEnumCases(self::$phpcsFile, $stackPtr); + } + + /** + * Test retrieving the cases declared in an enum. + * + * @dataProvider dataGetDeclaredEnumCases + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function return value. + * + * @return void + */ + public function testGetDeclaredEnumCases($testMarker, $expected) + { + // Translate the case markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_ENUM_CASE]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + $result = ObjectDeclarations::getDeclaredEnumCases(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetDeclaredEnumCases() For the array format. + * + * @return array>> + */ + public static function dataGetDeclaredEnumCases() + { + return [ + 'empty enum' => [ + 'testMarker' => '/* testEmptyEnum */', + 'expected' => [], + ], + 'enum with constants, cases and methods' => [ + 'testMarker' => '/* testEnum */', + 'expected' => [ + 'Hearts' => '/* markerEnumCase1 */', + 'Diamonds' => '/* markerEnumCase2 */', + 'Clubs' => '/* markerEnumCase3 */', + 'Spades' => '/* markerEnumCase4 */', + ], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testGetDeclaredEnumCasesResultIsCached() + { + $methodName = 'PHPCSUtils\\Utils\\ObjectDeclarations::analyzeOOStructure'; + $cases = $this->dataGetDeclaredEnumCases(); + $testMarker = $cases['enum with constants, cases and methods']['testMarker']; + $expected = $cases['enum with constants, cases and methods']['expected']; + + // Translate the case markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_ENUM_CASE]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = ObjectDeclarations::getDeclaredEnumCases(self::$phpcsFile, $stackPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, $stackPtr); + $resultSecondRun = ObjectDeclarations::getDeclaredEnumCases(self::$phpcsFile, $stackPtr); + + 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'); + } +} diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredMethodsTest.php b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredMethodsTest.php new file mode 100644 index 00000000..d5827556 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredMethodsTest.php @@ -0,0 +1,255 @@ +expectPhpcsException('$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non-OO token is passed. + * + * @return void + */ + public function testNotTargetToken() + { + $this->expectPhpcsException('$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + + $stackPtr = $this->getTargetToken('/* testUnacceptableToken */', \T_FUNCTION); + ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, $stackPtr); + } + + /** + * Test retrieving the methods declared in an OO structure. + * + * @dataProvider dataGetDeclaredMethods + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function return value. + * + * @return void + */ + public function testGetDeclaredMethods($testMarker, $expected) + { + // Translate the method markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_FUNCTION]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + $result = ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetDeclaredMethods() For the array format. + * + * @return array>> + */ + public static function dataGetDeclaredMethods() + { + return [ + 'empty class' => [ + 'testMarker' => '/* testEmptyClass */', + 'expected' => [], + ], + 'empty interface' => [ + 'testMarker' => '/* testEmptyInterface */', + 'expected' => [], + ], + 'empty trait' => [ + 'testMarker' => '/* testEmptyTrait */', + 'expected' => [], + ], + 'empty enum' => [ + 'testMarker' => '/* testEmptyEnum */', + 'expected' => [], + ], + 'empty anonymous class' => [ + 'testMarker' => '/* testEmptyAnonClass */', + 'expected' => [], + ], + 'class with constants, properties, methods and everything else' => [ + 'testMarker' => '/* testClass */', + 'expected' => [ + 'testDocblockSkipping' => '/* markerClassMethod1 */', + 'testAttributesSkipping' => '/* markerClassMethod2 */', + 'testDocblockAndAttributeSkipping' => '/* markerClassMethod3 */', + 'testFunctionKeywordUsedInParams' => '/* markerClassMethod4 */', + 'function' => '/* markerClassMethod5A */', + 'const' => '/* markerClassMethod5B */', + 'case' => '/* markerClassMethod5C */', + 'skipOverContentsOfFunctionA' => '/* markerClassMethod6 */', + 'skipOverContentsOfFunctionB' => '/* markerClassMethod7 */', + 'skipOverContentsOfFunctionC' => '/* markerClassMethod8 */', + '__construct' => '/* markerClassMethod9 */', + 'PrivateFunction' => '/* markerClassMethod10 */', + 'FinalPublic' => '/* markerClassMethod11 */', + 'implementMe' => '/* markerClassMethod12 */', + ], + ], + 'anon class with methods, nested within a method of another class' => [ + 'testMarker' => '/* testAnonClass */', + 'expected' => [ + '__isset' => '/* markerAnonNestedMethod1 */', + '__get' => '/* markerAnonNestedMethod2 */', + '__set' => '/* markerAnonNestedMethod3 */', + '__unset' => '/* markerAnonNestedMethod4 */', + 'doSomething' => '/* markerAnonNestedMethod5 */', + ], + ], + 'interface with constants and methods' => [ + 'testMarker' => '/* testInterface */', + 'expected' => [ + 'oh_oh' => '/* markerInterfaceMethod1 */', + 'im' => '/* markerInterfaceMethod2 */', + 'walking' => '/* markerInterfaceMethod3 */', + 'on' => '/* markerInterfaceMethod4 */', + 'sunshine' => '/* markerInterfaceMethod5 */', + ], + ], + 'trait with constants, properties and methods' => [ + 'testMarker' => '/* testTrait */', + 'expected' => [ + 'ti' => '/* markerTraitMethod1 */', + 'DO' => '/* markerTraitMethod2 */', + 'doReMiFa' => '/* markerTraitMethod3 */', + 'solLaTiDo' => '/* markerTraitMethod4 */', + ], + ], + 'anon class with constants, properties and methods in unconventional order' => [ + 'testMarker' => '/* testAnonClassUnconventionalOrder */', + 'expected' => [ + 'hereIGoAgain' => '/* markerAnonOrderMethod1 */', + 'LikeADrifter' => '/* markerAnonOrderMethod2 */', + 'Iaint' => '/* markerAnonOrderMethod3 */', + 'AndImGonnaHoldOn' => '/* markerAnonOrderMethod4 */', + ], + ], + 'enum with constants, cases and methods' => [ + 'testMarker' => '/* testEnum */', + 'expected' => [ + 'color' => '/* markerEnumMethod1 */', + 'offsetGet' => '/* markerEnumMethod2 */', + 'offsetExists' => '/* markerEnumMethod3 */', + 'offsetSet' => '/* markerEnumMethod4 */', + 'offsetUnset' => '/* markerEnumMethod5 */', + ], + ], + 'class with constructor, no properties, no params' => [ + 'testMarker' => '/* testClassConstructorNoParams */', + 'expected' => [ + '__construct' => '/* markerCPP1_Constructor */', + ], + ], + 'interface with constructor, no properties, has params' => [ + 'testMarker' => '/* testInterfaceConstructorWithParamsNotProperties */', + 'expected' => [ + '__Construct' => '/* markerCPP2_Constructor */', + ], + ], + 'class with constructor, with properties, no params' => [ + 'testMarker' => '/* testClassConstructorWithProperties */', + 'expected' => [ + '__CONSTRUCT' => '/* markerCPP3_Constructor */', + ], + ], + 'trait with constructor, with properties, with params' => [ + 'testMarker' => '/* testTraitConstructorWithParamsAndProperties */', + 'expected' => [ + '__construct' => '/* markerCPP4_Constructor */', + ], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testGetDeclaredMethodsResultIsCached() + { + $methodName = 'PHPCSUtils\\Utils\\ObjectDeclarations::analyzeOOStructure'; + $cases = $this->dataGetDeclaredMethods(); + $testMarker = $cases['class with constants, properties, methods and everything else']['testMarker']; + $expected = $cases['class with constants, properties, methods and everything else']['expected']; + + // Translate the method markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_FUNCTION]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, $stackPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, $stackPtr); + $resultSecondRun = ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, $stackPtr); + + 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'); + } +} diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredPropertiesTest.php b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredPropertiesTest.php new file mode 100644 index 00000000..78b38ea1 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/GetDeclaredPropertiesTest.php @@ -0,0 +1,233 @@ +expectPhpcsException('$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + ObjectDeclarations::getDeclaredProperties(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non-OO token is passed. + * + * @return void + */ + public function testNotTargetToken() + { + $this->expectPhpcsException('$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); + + $stackPtr = $this->getTargetToken('/* testUnacceptableToken */', \T_FUNCTION); + ObjectDeclarations::getDeclaredProperties(self::$phpcsFile, $stackPtr); + } + + /** + * Test retrieving the properties declared in an OO structure. + * + * @dataProvider dataGetDeclaredProperties + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function return value. + * + * @return void + */ + public function testGetDeclaredProperties($testMarker, $expected) + { + // Translate the method marker to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_VARIABLE]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + $result = ObjectDeclarations::getDeclaredProperties(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetDeclaredProperties() For the array format. + * + * @return array>> + */ + public static function dataGetDeclaredProperties() + { + return [ + 'empty class' => [ + 'testMarker' => '/* testEmptyClass */', + 'expected' => [], + ], + 'empty interface' => [ + 'testMarker' => '/* testEmptyInterface */', + 'expected' => [], + ], + 'empty trait' => [ + 'testMarker' => '/* testEmptyTrait */', + 'expected' => [], + ], + 'empty enum' => [ + 'testMarker' => '/* testEmptyEnum */', + 'expected' => [], + ], + 'empty anonymous class' => [ + 'testMarker' => '/* testEmptyAnonClass */', + 'expected' => [], + ], + 'class with constants, properties, methods and everything else' => [ + 'testMarker' => '/* testClass */', + 'expected' => [ + '$prop' => '/* markerClassProperty1 */', + '$property' => '/* markerClassProperty2 */', + '$promotion' => '/* markerClassProperty3 */', + ], + ], + 'anonymous class with properties' => [ + 'testMarker' => '/* testAnonClass */', + 'expected' => [ + '$sink' => '/* markerAnonNestedProperty1 */', + ], + ], + 'interface with constants and methods' => [ + 'testMarker' => '/* testInterface */', + 'expected' => [ + '$invalid' => '/* markerInterfaceIllegalProperty */', + ], + ], + 'trait with constants, properties and methods' => [ + 'testMarker' => '/* testTrait */', + 'expected' => [ + '$fa' => '/* markerTraitProperty1 */', + '$sol' => '/* markerTraitProperty2 */', + '$LA' => '/* markerTraitProperty3 */', + ], + ], + 'anon class with constants, properties and methods in unconventional order' => [ + 'testMarker' => '/* testAnonClassUnconventionalOrder */', + 'expected' => [ + '$goingDownTheOnlyRoad' => '/* markerAnonOrderProperty1 */', + '$inNeedOfRescue' => '/* markerAnonOrderProperty2 */', + ], + ], + 'enum with constants, cases and methods' => [ + 'testMarker' => '/* testEnum */', + 'expected' => [ + '$invalid' => '/* markerEnumIllegalProperty */', + ], + ], + 'class with constructor, no properties, no params' => [ + 'testMarker' => '/* testClassConstructorNoParams */', + 'expected' => [], + ], + 'interface with constructor, no properties, has params' => [ + 'testMarker' => '/* testInterfaceConstructorWithParamsNotProperties */', + 'expected' => [], + ], + 'class with constructor, with properties, no params' => [ + 'testMarker' => '/* testClassConstructorWithProperties */', + 'expected' => [ + '$propA' => '/* markerCPP3_Property1 */', + '$propB' => '/* markerCPP3_Property2 */', + '$propC' => '/* markerCPP3_Property3 */', + '$propD' => '/* markerCPP3_Property4 */', + ], + ], + 'trait with constructor, with properties, with params' => [ + 'testMarker' => '/* testTraitConstructorWithParamsAndProperties */', + 'expected' => [ + '$declared' => '/* markerCPP4_Property1 */', + '$instance' => '/* markerCPP4_Property2 */', + '$propA' => '/* markerCPP4_Property3 */', + '$propB' => '/* markerCPP4_Property4 */', + '$propC' => '/* markerCPP4_Property5 */', + ], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testGetDeclaredPropertiesResultIsCached() + { + // The test case used is specifically selected as the raw and the clean param values will be the same. + $methodName = 'PHPCSUtils\\Utils\\ObjectDeclarations::analyzeOOStructure'; + $cases = $this->dataGetDeclaredProperties(); + $testMarker = $cases['class with constants, properties, methods and everything else']['testMarker']; + $expected = $cases['class with constants, properties, methods and everything else']['expected']; + + // Translate the property markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_VARIABLE]); + } + + $stackPtr = $this->getTargetToken($testMarker, Tokens::$ooScopeTokens); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = ObjectDeclarations::getDeclaredProperties(self::$phpcsFile, $stackPtr); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, $stackPtr); + $resultSecondRun = ObjectDeclarations::getDeclaredProperties(self::$phpcsFile, $stackPtr); + + 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'); + } +} diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/ParseError1Test.inc b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/ParseError1Test.inc new file mode 100644 index 00000000..c7bc14d9 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/ParseError1Test.inc @@ -0,0 +1,7 @@ +markTestSkipped('Parse error which doesn\'t involve an enum'); + } + + /** + * Test retrieving the methods declared in an OO structure. + * + * @return void + */ + public function testGetDeclaredMethods() + { + $expected = [ + 'name' => '/* markerFunction1 */', + 'another' => '/* markerFunction2 */', + ]; + + // Translate the method markers to token pointers. + foreach ($expected as $name => $marker) { + $expected[$name] = $this->getTargetToken($marker, [\T_FUNCTION]); + } + + $stackPtr = $this->getTargetToken('/* testParseError */', Tokens::$ooScopeTokens); + $result = ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } +} diff --git a/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/ParseErrorTestCase.php b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/ParseErrorTestCase.php new file mode 100644 index 00000000..ab129a95 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/AnalyzeOOStructure/ParseErrorTestCase.php @@ -0,0 +1,75 @@ +getTargetToken('/* testParseError */', Tokens::$ooScopeTokens); + $this->assertSame([], ObjectDeclarations::getDeclaredConstants(self::$phpcsFile, $stackPtr)); + } + + /** + * Test retrieving the cases declared in an enum. + * + * @return void + */ + public function testGetDeclaredEnumCases() + { + $stackPtr = $this->getTargetToken('/* testParseError */', Tokens::$ooScopeTokens); + $this->assertSame([], ObjectDeclarations::getDeclaredEnumCases(self::$phpcsFile, $stackPtr)); + } + + /** + * Test retrieving the properties declared in an OO structure. + * + * @return void + */ + public function testGetDeclaredProperties() + { + $stackPtr = $this->getTargetToken('/* testParseError */', Tokens::$ooScopeTokens); + $this->assertSame([], ObjectDeclarations::getDeclaredProperties(self::$phpcsFile, $stackPtr)); + } + + /** + * Test retrieving the methods declared in an OO structure. + * + * @return void + */ + public function testGetDeclaredMethods() + { + $stackPtr = $this->getTargetToken('/* testParseError */', Tokens::$ooScopeTokens); + $this->assertSame([], ObjectDeclarations::getDeclaredMethods(self::$phpcsFile, $stackPtr)); + } +}