From 114217e62b0113af64019bd4da82f0cd2fbb8705 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Feb 2024 04:09:52 +0100 Subject: [PATCH] :sparkles: New `PHPCSUtils\Utils\Constants` class ... to contain utilities methods for analysing constants declared using the `const` keyword. In the future, the scope of the methods might be expanded to also covered constants declared using `define()`, but that's for later. Initially, the class comes with the following method: * `getProperties(File $phpcsFile, $stackPtr): array` to retrieve an array of information about a constant declaration, like the visibility, whether visibility was explicitly declared, whether the constant was declared as final, what the type is for the constant etc. Includes extensive unit tests. --- PHPCSUtils/Utils/Constants.php | 180 ++++ .../GetPropertiesParseError1Test.inc | 6 + .../GetPropertiesParseError1Test.php | 40 + .../GetPropertiesParseError2Test.inc | 5 + .../GetPropertiesParseError2Test.php | 40 + .../GetPropertiesParseError3Test.inc | 6 + .../GetPropertiesParseError3Test.php | 52 ++ Tests/Utils/Constants/GetPropertiesTest.inc | 182 ++++ Tests/Utils/Constants/GetPropertiesTest.php | 774 ++++++++++++++++++ 9 files changed, 1285 insertions(+) create mode 100644 PHPCSUtils/Utils/Constants.php create mode 100644 Tests/Utils/Constants/GetPropertiesParseError1Test.inc create mode 100644 Tests/Utils/Constants/GetPropertiesParseError1Test.php create mode 100644 Tests/Utils/Constants/GetPropertiesParseError2Test.inc create mode 100644 Tests/Utils/Constants/GetPropertiesParseError2Test.php create mode 100644 Tests/Utils/Constants/GetPropertiesParseError3Test.inc create mode 100644 Tests/Utils/Constants/GetPropertiesParseError3Test.php create mode 100644 Tests/Utils/Constants/GetPropertiesTest.inc create mode 100644 Tests/Utils/Constants/GetPropertiesTest.php diff --git a/PHPCSUtils/Utils/Constants.php b/PHPCSUtils/Utils/Constants.php new file mode 100644 index 00000000..84e1d063 --- /dev/null +++ b/PHPCSUtils/Utils/Constants.php @@ -0,0 +1,180 @@ + Array with information about the constant declaration. + * The format of the return value is: + * ```php + * array( + * 'scope' => string, // Public, private, or protected. + * 'scope_token' => integer|false, // The stack pointer to the scope keyword or + * // FALSE if the scope was not explicitly specified. + * 'is_final' => boolean, // TRUE if the final keyword was found. + * 'final_token' => integer|false, // The stack pointer to the final keyword + * // or FALSE if the const is not declared final. + * 'type' => string, // The type of the const (empty if no type specified). + * 'type_token' => integer|false, // The stack pointer to the start of the type + * // or FALSE if there is no type. + * 'type_end_token' => integer|false, // The stack pointer to the end of the type + * // or FALSE if there is no type. + * 'nullable_type' => boolean, // TRUE if the type is preceded by the + * // nullability operator. + * 'name_token' => integer, // The stack pointer to the constant name. + * // Note: for group declarations this points to the + * // name of the first constant. + * 'equal_token' => integer, // The stack pointer to the equal sign. + * // Note: for group declarations this points to the + * // equal sign of the first constant. + * ); + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a `T_CONST` token. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not an OO constant. + */ + public static function getProperties(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CONST) { + throw new RuntimeException('$stackPtr must be of type T_CONST'); + } + + if (Scopes::isOOConstant($phpcsFile, $stackPtr) === false) { + throw new RuntimeException('$stackPtr is not an OO constant'); + } + + if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { + return Cache::get($phpcsFile, __METHOD__, $stackPtr); + } + + $assignmentPtr = $phpcsFile->findNext([\T_EQUAL, \T_SEMICOLON, \T_CLOSE_CURLY_BRACKET], ($stackPtr + 1)); + if ($assignmentPtr === false || $tokens[$assignmentPtr]['code'] !== \T_EQUAL) { + // Probably a parse error. Don't cache the result. + throw new RuntimeException('$stackPtr is not an OO constant'); + } + + $namePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($assignmentPtr - 1), ($stackPtr + 1), true); + + $returnValue = [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => $namePtr, + 'equal_token' => $assignmentPtr, + ]; + + for ($i = ($stackPtr - 1);; $i--) { + // Skip over potentially large docblocks. + if ($tokens[$i]['code'] === \T_DOC_COMMENT_CLOSE_TAG + && isset($tokens[$i]['comment_opener']) + ) { + $i = $tokens[$i]['comment_opener']; + continue; + } + + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { + continue; + } + + switch ($tokens[$i]['code']) { + case \T_PUBLIC: + $returnValue['scope'] = 'public'; + $returnValue['scope_token'] = $i; + break; + + case \T_PROTECTED: + $returnValue['scope'] = 'protected'; + $returnValue['scope_token'] = $i; + break; + + case \T_PRIVATE: + $returnValue['scope'] = 'private'; + $returnValue['scope_token'] = $i; + break; + + case \T_FINAL: + $returnValue['is_final'] = true; + $returnValue['final_token'] = $i; + break; + + default: + // Any other token means that the start of the statement has been reached. + break 2; + } + } + + $type = ''; + $typeToken = false; + $typeEndToken = false; + $constantTypeTokens = Collections::constantTypeTokens(); + + // Now, let's check for a type. + for ($i = ($stackPtr + 1); $i < $namePtr; $i++) { + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { + continue; + } + + if ($tokens[$i]['code'] === \T_NULLABLE) { + $returnValue['nullable_type'] = true; + continue; + } + + if (isset($constantTypeTokens[$tokens[$i]['code']]) === true) { + $typeEndToken = $i; + if ($typeToken === false) { + $typeToken = $i; + } + + $type .= $tokens[$i]['content']; + } + } + + if ($type !== '' && $returnValue['nullable_type'] === true) { + $type = '?' . $type; + } + + $returnValue['type'] = $type; + $returnValue['type_token'] = $typeToken; + $returnValue['type_end_token'] = $typeEndToken; + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue); + return $returnValue; + } +} diff --git a/Tests/Utils/Constants/GetPropertiesParseError1Test.inc b/Tests/Utils/Constants/GetPropertiesParseError1Test.inc new file mode 100644 index 00000000..4192e73f --- /dev/null +++ b/Tests/Utils/Constants/GetPropertiesParseError1Test.inc @@ -0,0 +1,6 @@ +expectPhpcsException('$stackPtr is not an OO constant'); + + $const = $this->getTargetToken('/* testParseErrorLiveCoding */', \T_CONST); + Constants::getProperties(self::$phpcsFile, $const); + } +} diff --git a/Tests/Utils/Constants/GetPropertiesParseError2Test.inc b/Tests/Utils/Constants/GetPropertiesParseError2Test.inc new file mode 100644 index 00000000..7950b74e --- /dev/null +++ b/Tests/Utils/Constants/GetPropertiesParseError2Test.inc @@ -0,0 +1,5 @@ +expectPhpcsException('$stackPtr is not an OO constant'); + + $const = $this->getTargetToken('/* testParseErrorLiveCoding */', \T_CONST); + Constants::getProperties(self::$phpcsFile, $const); + } +} diff --git a/Tests/Utils/Constants/GetPropertiesParseError3Test.inc b/Tests/Utils/Constants/GetPropertiesParseError3Test.inc new file mode 100644 index 00000000..fa7cec26 --- /dev/null +++ b/Tests/Utils/Constants/GetPropertiesParseError3Test.inc @@ -0,0 +1,6 @@ +getTargetToken('/* testParseErrorMissingName */', \T_CONST); + $expected = [ + 'scope' => 'private', + 'scope_token' => ($const - 2), + 'is_final' => false, + 'final_token' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => false, + 'equal_token' => ($const + 2), + ]; + $result = Constants::getProperties(self::$phpcsFile, $const); + + $this->assertSame($expected, $result); + } +} diff --git a/Tests/Utils/Constants/GetPropertiesTest.inc b/Tests/Utils/Constants/GetPropertiesTest.inc new file mode 100644 index 00000000..2af138d0 --- /dev/null +++ b/Tests/Utils/Constants/GetPropertiesTest.inc @@ -0,0 +1,182 @@ +expectPhpcsException('$stackPtr must be of type T_CONST'); + + $define = $this->getTargetToken('/* testNotAConstToken */', \T_STRING); + Constants::getProperties(self::$phpcsFile, $define); + } + + /** + * Test receiving an expected exception when a non OO constant is passed. + * + * @dataProvider dataNotOOConstantException + * + * @param string $identifier Comment which precedes the test case. + * + * @return void + */ + public function testNotOOConstantException($identifier) + { + $this->expectPhpcsException('$stackPtr is not an OO constant'); + + $const = $this->getTargetToken($identifier, \T_CONST); + Constants::getProperties(self::$phpcsFile, $const); + } + + /** + * Data provider. + * + * @see testNotOOConstantException() + * + * @return array> + */ + public static function dataNotOOConstantException() + { + return [ + 'global constant' => ['/* testGlobalConstantCannotHaveModifiersOrType */'], + 'constant declared in OO method (illegal)' => ['/* testConstInMethodIsNotOO */'], + ]; + } + + /** + * Test the getProperties() method. + * + * @dataProvider dataGetProperties + * + * @param string $identifier Comment which precedes the test case. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetProperties($identifier, $expected) + { + $const = $this->getTargetToken($identifier, \T_CONST); + $expected = $this->updateExpectedTokenPositions($const, $expected); + $result = Constants::getProperties(self::$phpcsFile, $const); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * Note: all indexes containing token positions should contain either `false` (no position) + * or the _offset_ of the token in relation to the `T_CONST` token which is passed + * to the getProperties() method. + * + * @see testGetProperties() + * + * @return array>> + */ + public static function dataGetProperties() + { + $php8Names = parent::usesPhp8NameTokens(); + + return [ + 'no modifiers, no type, with docblock' => [ + 'identifier' => '/* testNoModifiersNoTypesWithDocblock */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 2, + 'equal_token' => 4, + ], + ], + + // Testing modifier keyword recognition. + 'final, no type' => [ + 'identifier' => '/* testFinalNoTypesConstAsName */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => true, + 'final_token' => -2, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 2, + 'equal_token' => 4, + ], + ], + 'public, no type' => [ + 'identifier' => '/* testPublicNoTypes */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 2, + 'equal_token' => 4, + ], + ], + 'protected, no type, with comment' => [ + 'identifier' => '/* testProtectedNoTypesWithComment */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -4, + 'is_final' => false, + 'final_token' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 2, + 'equal_token' => 4, + ], + ], + 'private, no type, comments and whitespace' => [ + 'identifier' => '/* testPrivateNoTypesWithCommentAndWhitespace */', + 'expected' => [ + 'scope' => 'private', + 'scope_token' => -6, + 'is_final' => false, + 'final_token' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 5, + 'equal_token' => 7, + ], + ], + 'final public, no type' => [ + 'identifier' => '/* testFinalPublicNoTypes */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => true, + 'final_token' => -4, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 2, + 'equal_token' => 4, + ], + ], + 'protected final, no type' => [ + 'identifier' => '/* testProtectedFinalNoTypes */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -4, + 'is_final' => true, + 'final_token' => -2, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + 'name_token' => 2, + 'equal_token' => 4, + ], + ], + + // Testing typed constants. + 'no modifiers, typed: true' => [ + 'identifier' => '/* testTypedTrue */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'true', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'final, typed: false' => [ + 'identifier' => '/* testTypedFalse */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => true, + 'final_token' => -2, + 'type' => 'false', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'public, typed: null' => [ + 'identifier' => '/* testTypedNull */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => 'null', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'final protected, typed: bool, with comment' => [ + 'identifier' => '/* testTypedBoolWihComment */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -2, + 'is_final' => true, + 'final_token' => -4, + 'type' => 'bool', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'private, typed: ?int, with docblock' => [ + 'identifier' => '/* testTypedNullableIntWithDocblock */', + 'expected' => [ + 'scope' => 'private', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => '?int', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => true, + 'name_token' => 5, + 'equal_token' => 7, + ], + ], + 'no modifiers, typed: float, with attribute' => [ + 'identifier' => '/* testTypedFloatWithAttribute */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'float', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'public final, typed: ?string, with comment' => [ + 'identifier' => '/* testTypedNullableStringWithComment */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -6, + 'is_final' => true, + 'final_token' => -2, + 'type' => '?string', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => true, + 'name_token' => 5, + 'equal_token' => 7, + ], + ], + 'private final, typed: array' => [ + 'identifier' => '/* testTypedArray */', + 'expected' => [ + 'scope' => 'private', + 'scope_token' => -4, + 'is_final' => true, + 'final_token' => -2, + 'type' => 'array', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'no modifiers, typed: object, extra whitespace' => [ + 'identifier' => '/* testTypedObjectWithExtraWhitespace */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'object', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => false, + 'name_token' => 6, + 'equal_token' => 11, + ], + ], + 'no modifiers, typed: ?iterable, lowercase constant name' => [ + 'identifier' => '/* testTypedNullableIterableLowercaseName */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => '?iterable', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => true, + 'name_token' => 5, + 'equal_token' => 7, + ], + ], + 'no modifiers, typed: mixed' => [ + 'identifier' => '/* testTypedMixed */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'mixed', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'no modifiers, typed: nullable unqualified name, comment in type' => [ + 'identifier' => '/* testTypedClassUnqualifiedWithComment */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => '?MyClass', + 'type_token' => 6, + 'type_end_token' => 6, + 'nullable_type' => true, + 'name_token' => 8, + 'equal_token' => 10, + ], + ], + 'public, typed: fully qualified name, with docblock' => [ + 'identifier' => '/* testTypedClassFullyQualifiedWithDocblock */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => '\MyClass', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 2 : 3, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 4 : 5, + 'equal_token' => ($php8Names === true) ? 6 : 7, + ], + ], + 'protected, typed: namespace relative name' => [ + 'identifier' => '/* testTypedClassNamespaceRelative */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => 'namespace\MyClass', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 2 : 4, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 4 : 6, + 'equal_token' => ($php8Names === true) ? 6 : 8, + ], + ], + 'private, typed: partially qualified, with multi-attribute' => [ + 'identifier' => '/* testTypedClassPartiallyQualifiedWithMultipleAttributes */', + 'expected' => [ + 'scope' => 'private', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => 'Partial\MyClass', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 2 : 4, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 4 : 6, + 'equal_token' => ($php8Names === true) ? 6 : 8, + ], + ], + 'no modifiers, typed: ?parent, with comments, messy' => [ + 'identifier' => '/* testTypedNullableParentMessy */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => '?parent', + 'type_token' => 9, + 'type_end_token' => 9, + 'nullable_type' => true, + 'name_token' => 11, + 'equal_token' => 13, + ], + ], + 'public, typed: bool, multi-constant, single line' => [ + 'identifier' => '/* testMultiConstSingleLine */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => 'bool', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'public, typed: ?array, multi-constant, multi-line' => [ + 'identifier' => '/* testMultiConstMultiLine */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => '?array', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => true, + 'name_token' => 8, + 'equal_token' => 10, + ], + ], + + // Types which are only legal in enums. + 'final, typed: self' => [ + 'identifier' => '/* testEnumConstTypedSelf */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => true, + 'final_token' => -2, + 'type' => 'self', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'no modifiers, typed: static' => [ + 'identifier' => '/* testEnumConstTypedNullableStatic */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => '?static', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => true, + 'name_token' => 5, + 'equal_token' => 7, + ], + ], + + // Illegal types, but that's not the concern of this method. + 'protected, typed: ?callable (not supported in PHP)' => [ + 'identifier' => '/* testTypedNullableCallable */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => '?callable', + 'type_token' => 3, + 'type_end_token' => 3, + 'nullable_type' => true, + 'name_token' => 5, + 'equal_token' => 7, + ], + ], + 'final public, typed: void (not supported in PHP)' => [ + 'identifier' => '/* testTypedVoid */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -2, + 'is_final' => true, + 'final_token' => -4, + 'type' => 'void', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + 'private, typed: never (not supported in PHP)' => [ + 'identifier' => '/* testTypedNever */', + 'expected' => [ + 'scope' => 'private', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => 'never', + 'type_token' => 2, + 'type_end_token' => 2, + 'nullable_type' => false, + 'name_token' => 4, + 'equal_token' => 6, + ], + ], + + // Union types. + 'no modifiers, typed: true|null' => [ + 'identifier' => '/* testTypedUnionTrueNull */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'true|null', + 'type_token' => 2, + 'type_end_token' => 4, + 'nullable_type' => false, + 'name_token' => 8, + 'equal_token' => 10, + ], + ], + 'final, typed: array|object, with multi-line atribute' => [ + 'identifier' => '/* testTypedUnionArrayObjectWithMultilineAttribute */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => true, + 'final_token' => -2, + 'type' => 'array|object', + 'type_token' => 2, + 'type_end_token' => 4, + 'nullable_type' => false, + 'name_token' => 6, + 'equal_token' => 8, + ], + ], + 'no modifiers, typed: string|array|int, with whitespace in type' => [ + 'identifier' => '/* testTypedUnionStringArrayInt */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'string|array|int', + 'type_token' => 2, + 'type_end_token' => 10, + 'nullable_type' => false, + 'name_token' => 12, + 'equal_token' => 14, + ], + ], + 'public final, typed: ?float|bool|array, illegal nullable union type' => [ + 'identifier' => '/* testTypedUnionFloatBoolArrayIllegalNullable */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => -4, + 'is_final' => true, + 'final_token' => -2, + 'type' => '?float|bool|array', + 'type_token' => 3, + 'type_end_token' => 10, + 'nullable_type' => true, + 'name_token' => 12, + 'equal_token' => 14, + ], + ], + 'no modifiers, typed: iterable|false' => [ + 'identifier' => '/* testTypedUnionIterableFalse */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => false, + 'final_token' => false, + 'type' => 'iterable|false', + 'type_token' => 2, + 'type_end_token' => 4, + 'nullable_type' => false, + 'name_token' => 6, + 'equal_token' => 8, + ], + ], + 'final protected, typed: Unqualified|namespace\Relative, with whitespace and comments in type' => [ + 'identifier' => '/* testTypedUnionUnqualifiedNamespaceRelativeWithWhiteSpaceAndComments */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -2, + 'is_final' => true, + 'final_token' => -4, + 'type' => 'Unqualified|namespace\Relative', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 8 : 10, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 10 : 12, + 'equal_token' => ($php8Names === true) ? 12 : 14, + ], + ], + 'private, typed: \Fully\Qualified|Partially\Qualified' => [ + 'identifier' => '/* testTypedUnionFullyQualifiedPartiallyQualified */', + 'expected' => [ + 'scope' => 'private', + 'scope_token' => -4, + 'is_final' => false, + 'final_token' => false, + 'type' => '\Fully\Qualified|Partially\Qualified', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 4 : 9, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 6 : 11, + 'equal_token' => ($php8Names === true) ? 8 : 13, + ], + ], + + // Intersection types. + 'final, typed: Unqualified|namespace\Relative, with whitespace and comments in type' => [ + 'identifier' => '/* testTypedIntersectUnqualifiedNamespaceRelative */', + 'expected' => [ + 'scope' => 'public', + 'scope_token' => false, + 'is_final' => true, + 'final_token' => -2, + 'type' => 'Unqualified&namespace\Relative', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 4 : 6, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 6 : 8, + 'equal_token' => ($php8Names === true) ? 8 : 10, + ], + ], + 'protected, typed: \Fully\Qualified|Partially\Qualified' => [ + 'identifier' => '/* testTypedIntersectFullyQualifiedPartiallyQualified */', + 'expected' => [ + 'scope' => 'protected', + 'scope_token' => -2, + 'is_final' => false, + 'final_token' => false, + 'type' => '\Fully\Qualified&Partially\Qualified', + 'type_token' => 2, + 'type_end_token' => ($php8Names === true) ? 4 : 9, + 'nullable_type' => false, + 'name_token' => ($php8Names === true) ? 6 : 11, + 'equal_token' => ($php8Names === true) ? 8 : 13, + ], + ], + ]; + } + + /** + * Verify that the build-in caching is used when caching is enabled. + * + * @return void + */ + public function testResultIsCached() + { + $methodName = 'PHPCSUtils\\Utils\\Constants::getProperties'; + $cases = self::dataGetProperties(); + $identifier = $cases['public final, typed: ?float|bool|array, illegal nullable union type']['identifier']; + $expected = $cases['public final, typed: ?float|bool|array, illegal nullable union type']['expected']; + + $const = $this->getTargetToken($identifier, \T_CONST); + $expected = $this->updateExpectedTokenPositions($const, $expected); + + // Verify the caching works. + $origStatus = Cache::$enabled; + Cache::$enabled = true; + + $resultFirstRun = Constants::getProperties(self::$phpcsFile, $const); + $isCached = Cache::isCached(self::$phpcsFile, $methodName, $const); + $resultSecondRun = Constants::getProperties(self::$phpcsFile, $const); + + 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) + { + if (\is_int($expected['scope_token']) === true) { + $expected['scope_token'] += $targetPtr; + } + if (\is_int($expected['final_token']) === true) { + $expected['final_token'] += $targetPtr; + } + if (\is_int($expected['type_token']) === true) { + $expected['type_token'] += $targetPtr; + } + if (\is_int($expected['type_end_token']) === true) { + $expected['type_end_token'] += $targetPtr; + } + if (\is_int($expected['name_token']) === true) { + $expected['name_token'] += $targetPtr; + } + if (\is_int($expected['equal_token'])) { + $expected['equal_token'] += $targetPtr; + } + + return $expected; + } +}