From eeff65518a9ee2a74e1b573f878f874680277dd1 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 1 Mar 2025 19:40:26 +0100 Subject: [PATCH 1/9] Tokenizer/PHP: document handling of `\true`/`\false`/`\null` The handling as documented here is 100% in line with the way PHP 8.0+ handles this natively. --- .../PHP/NamespacedNameSingleTokenTest.inc | 9 +++++ .../PHP/NamespacedNameSingleTokenTest.php | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.inc b/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.inc index 886b6ed04e..97945a3b13 100644 --- a/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.inc +++ b/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.inc @@ -133,6 +133,15 @@ class MyClass /* testInstanceOfPartiallyQualified */ $is = $obj instanceof Partially\ClassName; + + /* testFullyQualifiedNull */ + $a = \NULL; + + /* testFullyQualifiedFalse */ + $a = \false; + + /* testFullyQualifiedTrue */ + $a = \True; } } diff --git a/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.php b/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.php index 575da62f00..f2ef8fb155 100644 --- a/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.php +++ b/tests/Core/Tokenizers/PHP/NamespacedNameSingleTokenTest.php @@ -868,6 +868,45 @@ public static function dataIdentifierTokenization() ], ], ], + 'fully qualified "null"' => [ + 'testMarker' => '/* testFullyQualifiedNull */', + 'expectedTokens' => [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\NULL', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + 'fully qualified "false"' => [ + 'testMarker' => '/* testFullyQualifiedFalse */', + 'expectedTokens' => [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\false', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + 'fully qualified "true"' => [ + 'testMarker' => '/* testFullyQualifiedTrue */', + 'expectedTokens' => [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\True', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], 'function call, namespace relative, with whitespace (invalid in PHP 8)' => [ 'testMarker' => '/* testInvalidInPHP8Whitespace */', 'expectedTokens' => [ From fb3d96148d08a9b7afe54ea39b546ad89cba60b2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Apr 2025 18:42:47 +0200 Subject: [PATCH 2/9] Tokenizer/PHP: add more tests with FQN true/false/null This is already handled correctly in all relevant places, these tests just safeguard that. --- .../Tokenizers/PHP/BackfillFnTokenTest.inc | 9 +++++++ .../Tokenizers/PHP/BackfillFnTokenTest.php | 25 +++++++++++++------ tests/Core/Tokenizers/PHP/BitwiseOrTest.inc | 9 +++++++ tests/Core/Tokenizers/PHP/BitwiseOrTest.php | 3 +++ tests/Core/Tokenizers/PHP/DNFTypesTest.inc | 6 +++++ tests/Core/Tokenizers/PHP/DNFTypesTest.php | 9 +++++++ .../Tokenizers/PHP/TypeIntersectionTest.inc | 9 +++++++ .../Tokenizers/PHP/TypeIntersectionTest.php | 3 +++ 8 files changed, 65 insertions(+), 8 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.inc b/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.inc index e6d17388be..d2fa116310 100644 --- a/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.inc +++ b/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.inc @@ -107,6 +107,15 @@ fn(array $a) : True => $a; /* testNullReturnType */ fn(array $a) : null => $a; +/* testFQNFalseReturnType */ +fn(array $a) : \FALSE => $a; + +/* testFQNTrueReturnType */ +fn(array $a) : \true => $a; + +/* testFQNNullReturnType */ +fn(array $a) : \Null => $a; + /* testUnionParamType */ $arrowWithUnionParam = fn(int|float $param) : SomeClass => new SomeClass($param); diff --git a/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.php b/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.php index cbe3cfd42f..3501c3c87d 100644 --- a/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillFnTokenTest.php @@ -455,30 +455,39 @@ public function testKeywordReturnTypes($testMarker) public static function dataKeywordReturnTypes() { return [ - 'self' => [ + 'self' => [ 'testMarker' => '/* testSelfReturnType */', ], - 'parent' => [ + 'parent' => [ 'testMarker' => '/* testParentReturnType */', ], - 'callable' => [ + 'callable' => [ 'testMarker' => '/* testCallableReturnType */', ], - 'array' => [ + 'array' => [ 'testMarker' => '/* testArrayReturnType */', ], - 'static' => [ + 'static' => [ 'testMarker' => '/* testStaticReturnType */', ], - 'false' => [ + 'false' => [ 'testMarker' => '/* testFalseReturnType */', ], - 'true' => [ + 'true' => [ 'testMarker' => '/* testTrueReturnType */', ], - 'null' => [ + 'null' => [ 'testMarker' => '/* testNullReturnType */', ], + 'FQN false' => [ + 'testMarker' => '/* testFQNFalseReturnType */', + ], + 'FQN true' => [ + 'testMarker' => '/* testFQNTrueReturnType */', + ], + 'FQN null' => [ + 'testMarker' => '/* testFQNNullReturnType */', + ], ]; }//end dataKeywordReturnTypes() diff --git a/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc b/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc index c2c4508e43..d733fb402b 100644 --- a/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc +++ b/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc @@ -187,6 +187,15 @@ function trueTypeReturn($param): array|true|null {} /* testTypeUnionPHP82TrueLast */ $closure = function ($param): array|true {} +/* testTypeUnionFQNTrue */ +function FQNTrueTypeParam(\True|null $param) {} + +/* testTypeUnionFQNFalse */ +function FQNFalseTypeReturn($param): array|\false|null {} + +/* testTypeUnionFQNNull */ +$closure = function ($param): array|\NULL {}; + /* testLiveCoding */ // Intentional parse error. This has to be the last test in the file. return function( type| diff --git a/tests/Core/Tokenizers/PHP/BitwiseOrTest.php b/tests/Core/Tokenizers/PHP/BitwiseOrTest.php index def661648c..35ce86df7d 100644 --- a/tests/Core/Tokenizers/PHP/BitwiseOrTest.php +++ b/tests/Core/Tokenizers/PHP/BitwiseOrTest.php @@ -155,6 +155,9 @@ public static function dataTypeUnion() 'type for function param with true type first' => ['/* testTypeUnionPHP82TrueFirst */'], 'return type for function with true type middle' => ['/* testTypeUnionPHP82TrueMiddle */'], 'return type for closure with true type last' => ['/* testTypeUnionPHP82TrueLast */'], + 'type for function param with FQN true' => ['/* testTypeUnionFQNTrue */'], + 'return type for function with FQN false' => ['/* testTypeUnionFQNFalse */'], + 'return type for closure with FQN null' => ['/* testTypeUnionFQNNull */'], ]; }//end dataTypeUnion() diff --git a/tests/Core/Tokenizers/PHP/DNFTypesTest.inc b/tests/Core/Tokenizers/PHP/DNFTypesTest.inc index c1a38c79e5..6a0078b74c 100644 --- a/tests/Core/Tokenizers/PHP/DNFTypesTest.inc +++ b/tests/Core/Tokenizers/PHP/DNFTypesTest.inc @@ -225,6 +225,12 @@ abstract class DNFTypes { // Illegal type: segments which are strict subsets of others are disallowed, but that's not the concern of the tokenizer. public function identifierNamesReturnFQ( ) /* testDNFTypeReturnFullyQualified */ : (\Fully\Qualified\NameA&\Fully\Qualified\NameB)|\Fully\Qualified\NameB {} + + public function FQNTrueTypeParam(/* testDNFTypeFQNTrue */ \True|(A&D) $param) {} + + public function FQNFalseTypeReturn($param) /* testDNFTypeFQNFalse */ : (A&D)|\false|null { + $closure = function ($param): /* testDNFTypeFQNNull */ (A&D)|\NULL {}; + } } function globalFunctionWithSpreadAndReference( diff --git a/tests/Core/Tokenizers/PHP/DNFTypesTest.php b/tests/Core/Tokenizers/PHP/DNFTypesTest.php index 1e0a78f2b2..1670151f7a 100644 --- a/tests/Core/Tokenizers/PHP/DNFTypesTest.php +++ b/tests/Core/Tokenizers/PHP/DNFTypesTest.php @@ -496,6 +496,15 @@ public static function dataDNFTypeParentheses() 'OO method return type: fully qualified classes' => [ 'testMarker' => '/* testDNFTypeReturnFullyQualified */', ], + 'OO method param type: fully qualified true' => [ + 'testMarker' => '/* testDNFTypeFQNTrue */', + ], + 'OO method return type: fully qualified false' => [ + 'testMarker' => '/* testDNFTypeFQNFalse */', + ], + 'closure return type: fully qualified null' => [ + 'testMarker' => '/* testDNFTypeFQNNull */', + ], 'function param type: with reference' => [ 'testMarker' => '/* testDNFTypeWithReference */', ], diff --git a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc index fadc0df85a..13f90863fe 100644 --- a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc +++ b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc @@ -162,6 +162,15 @@ function &fn(/* testTypeIntersectionNonArrowFunctionDeclaration */ Foo&Bar $some /* testTypeIntersectionWithInvalidTypes */ function (int&string $var) {}; +/* testTypeIntersectionInvalidFQNTrue */ +function FQNTrueTypeParam(\True&MyClass $param) {} + +/* testTypeIntersectionInvalidFQNFalse */ +function FQNFalseTypeReturn($param): \SomeOtherClass&\false&MyClass {} + +/* testTypeIntersectionInvalidFQNNull */ +$closure = function ($param): MyClass&\NULL {}; + /* testLiveCoding */ // Intentional parse error. This has to be the last test in the file. return function( Foo& diff --git a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php index b191d7ebd4..eaed941c08 100644 --- a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php +++ b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php @@ -151,6 +151,9 @@ public static function dataTypeIntersection() 'return type for arrow function' => ['/* testTypeIntersectionArrowReturnType */'], 'type for function parameter, return by ref' => ['/* testTypeIntersectionNonArrowFunctionDeclaration */'], 'type for function parameter with invalid types' => ['/* testTypeIntersectionWithInvalidTypes */'], + 'type for method parameter, includes (invalid) FQN true' => ['/* testTypeIntersectionInvalidFQNTrue */'], + 'return type for method, includes (invalid) FQN false' => ['/* testTypeIntersectionInvalidFQNFalse */'], + 'return type for closure, includes (invalid) FQN null' => ['/* testTypeIntersectionInvalidFQNNull */'], ]; }//end dataTypeIntersection() From 7004de64d80a574923459f9145ed933f836a594e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Apr 2025 18:03:54 +0200 Subject: [PATCH 3/9] Generic/UnconditionalIfStatement: handle FQN `true`/`false`/`null` This sniff checks for `if`/`elseif` which only contain "true" or "false", i.e. don't actually compare to anything. This commit fixes the sniff to still function correctly when it encounters "fully qualified true/false" after the merge of 1020. Includes tests. --- .../Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php | 7 +++++++ .../CodeAnalysis/UnconditionalIfStatementUnitTest.1.inc | 5 +++++ .../CodeAnalysis/UnconditionalIfStatementUnitTest.php | 8 +++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php index 7471be83f2..091c461ec4 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php @@ -77,6 +77,13 @@ public function process(File $phpcsFile, $stackPtr) if (isset(Tokens::EMPTY_TOKENS[$code]) === true) { continue; + } + + if ($code === T_NAME_FULLY_QUALIFIED) { + $compareReadyKeyword = strtolower($tokens[$next]['content']); + if ($compareReadyKeyword !== '\true' && $compareReadyKeyword !== '\false') { + $goodCondition = true; + } } else if ($code !== T_TRUE && $code !== T_FALSE) { $goodCondition = true; } diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.1.inc b/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.1.inc index 58b604f738..dfeed37d70 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.1.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.1.inc @@ -11,3 +11,8 @@ if (true) { if (file_exists(__FILE__) === true) { } + +// Check handling of case and FQN state. +if (\true) { +} else if (\FALSE) { +} diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.php index 5b4805ff8f..932f435fbf 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.php +++ b/src/Standards/Generic/Tests/CodeAnalysis/UnconditionalIfStatementUnitTest.php @@ -50,9 +50,11 @@ public function getWarningList($testFile='') switch ($testFile) { case 'UnconditionalIfStatementUnitTest.1.inc': return [ - 3 => 1, - 5 => 1, - 7 => 1, + 3 => 1, + 5 => 1, + 7 => 1, + 16 => 1, + 17 => 1, ]; default: From 7e5f04299e3ce5600defa7b938951accfc84538d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Apr 2025 05:41:32 +0200 Subject: [PATCH 4/9] Generic/DisallowYodaConditions: handle FQN `true`/`false`/`null` This commit fixes the sniff to handle "fully qualified true/false" the same as unqualified true/false after the merge of 1020. Includes tests. --- .../DisallowYodaConditionsSniff.php | 26 ++++++++++++++++++- .../DisallowYodaConditionsUnitTest.inc | 19 +++++++++++--- .../DisallowYodaConditionsUnitTest.php | 4 +++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Standards/Generic/Sniffs/ControlStructures/DisallowYodaConditionsSniff.php b/src/Standards/Generic/Sniffs/ControlStructures/DisallowYodaConditionsSniff.php index 6d0ffdac1d..5c62af9295 100644 --- a/src/Standards/Generic/Sniffs/ControlStructures/DisallowYodaConditionsSniff.php +++ b/src/Standards/Generic/Sniffs/ControlStructures/DisallowYodaConditionsSniff.php @@ -55,12 +55,24 @@ public function process(File $phpcsFile, $stackPtr) T_LNUMBER, T_DNUMBER, T_CONSTANT_ENCAPSED_STRING, + T_NAME_FULLY_QUALIFIED, ]; if (in_array($tokens[$previousIndex]['code'], $relevantTokens, true) === false) { return; } + // Special case: T_NAME_FULLY_QUALIFIED is only a "relevant" token when it is for a FQN true/false/null. + if ($tokens[$previousIndex]['code'] === T_NAME_FULLY_QUALIFIED) { + $compareReadyKeyword = strtolower($tokens[$previousIndex]['content']); + if ($compareReadyKeyword !== '\true' + && $compareReadyKeyword !== '\false' + && $compareReadyKeyword !== '\null' + ) { + return; + } + } + if ($tokens[$previousIndex]['code'] === T_CLOSE_SHORT_ARRAY) { $previousIndex = $tokens[$previousIndex]['bracket_opener']; if ($this->isArrayStatic($phpcsFile, $previousIndex) === false) { @@ -164,6 +176,7 @@ public function isArrayStatic(File $phpcsFile, $arrayToken) T_COMMA => T_COMMA, T_TRUE => T_TRUE, T_FALSE => T_FALSE, + T_NULL => T_NULL, ]; for ($i = ($start + 1); $i < $end; $i++) { @@ -172,10 +185,21 @@ public function isArrayStatic(File $phpcsFile, $arrayToken) continue; } + // Special case: T_NAME_FULLY_QUALIFIED is only a "static" token when it is for a FQN true/false/null. + if ($tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED) { + $compareReadyKeyword = strtolower($tokens[$i]['content']); + if ($compareReadyKeyword === '\true' + || $compareReadyKeyword === '\false' + || $compareReadyKeyword === '\null' + ) { + continue; + } + } + if (isset($staticTokens[$tokens[$i]['code']]) === false) { return false; } - } + }//end for return true; diff --git a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc index b9cad4d3ea..4457268a88 100644 --- a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc +++ b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc @@ -7,8 +7,8 @@ if ($value == true) {} if (true === $value) {} if (true == $value) {} -if($value === true){} -if($value == true){} +if($value === false){} +if($value == false){} if(false === $value){} if(!false == $value || true !== $value){} @@ -139,8 +139,8 @@ if (is_array($val) && array('foo', 'bar') === array($foo, $bar) && ['foo', 'bar'] === [$foo, $bar] && array('foo' => true, 'bar' => false) === array(getContents()) - && ['foo' => true, 'bar' => false] === array(getContents()) - && array(getContents()) === ['foo' => true, 'bar' => false] + && ['foo' => true, 'bar' => \false, 'baz' => null] === array(getContents()) + && array(getContents()) === ['foo' => \true, 'bar' => false, 'baz' => \null] ) { } @@ -192,3 +192,14 @@ if(Partially\qualified('foo') === 10){} if(1.5 === Partially\qualified(true)){} if(namespace\relative(false) === null){} if('string' === namespace\relative(null)){} + +// Handle FQN true/false/null the same as plain true/false/null. +if ($value === \true) {} +if (\true === $value) {} + +if($value == \FALSE){} +if(\FALSE === $value){} +if(!\false == $value || true !== $value){} + +if($value === \Null){} +if(\Null == $value){} diff --git a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php index 510bd7d52e..ff5761fae2 100644 --- a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php +++ b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php @@ -75,6 +75,10 @@ public function getErrorList() 190 => 1, 192 => 1, 194 => 1, + 198 => 1, + 201 => 1, + 202 => 2, + 205 => 1, ]; }//end getErrorList() From 9d768e2fb4c876a0ec9f2e1fb99ab97bafe72031 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 5 Apr 2025 21:00:58 +0200 Subject: [PATCH 5/9] Generic/[Lower|Upper]CaseConstant: minor clean up The PHP 8.0 namespaced name tokens were already added to the sniff in PR 119 (to make the merge with the old 4.0 branch easier). This commit now removes the `T_NS_SEPARATOR` and `T_NAMESPACE` tokens which should no longer need to be taken into account what with the change in the namespaced names tokenization in PHPCS 4.0. The change is already covered by pre-existing tests. --- src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php index 4defc405e4..79a43441c9 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php @@ -43,8 +43,6 @@ class LowerCaseConstantSniff implements Sniff T_NAME_QUALIFIED => T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, T_NAME_RELATIVE => T_NAME_RELATIVE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_NAMESPACE => T_NAMESPACE, T_TYPE_UNION => T_TYPE_UNION, T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, T_TYPE_OPEN_PARENTHESIS => T_TYPE_OPEN_PARENTHESIS, From 5281f749e7092717fd874479affc509d9fd7d6dd Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 1 Mar 2025 19:41:36 +0100 Subject: [PATCH 6/9] Generic/[Lower|Upper]CaseConstant: handle FQN `true`/`false`/`null` These are the only two sniffs explicitly targetting `true`, `false` and `null`. This commit allows these sniffs to still function correctly on "fully qualified true/false/null" after the merge of 1020. Includes tests. --- .../Sniffs/PHP/LowerCaseConstantSniff.php | 37 ++++++++++++++- .../Tests/PHP/LowerCaseConstantUnitTest.1.inc | 4 ++ .../PHP/LowerCaseConstantUnitTest.1.inc.fixed | 4 ++ .../Tests/PHP/LowerCaseConstantUnitTest.php | 3 ++ .../Tests/PHP/UpperCaseConstantUnitTest.inc | 4 ++ .../PHP/UpperCaseConstantUnitTest.inc.fixed | 4 ++ .../Tests/PHP/UpperCaseConstantUnitTest.php | 45 ++++++++++--------- 7 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php index 79a43441c9..5490a7407b 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php @@ -60,6 +60,9 @@ public function register() { $targets = $this->targets; + // Allow for "fully qualified" true/false/null. + $targets[] = T_NAME_FULLY_QUALIFIED; + // Register scope modifiers to filter out property type declarations. $targets += Tokens::SCOPE_MODIFIERS; $targets[] = T_VAR; @@ -95,6 +98,13 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); + // If this is a fully qualified name, check if it is FQN true/false/null. + if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED + && $this->isFQNTrueFalseNull($phpcsFile, $stackPtr) === false + ) { + return; + } + // Skip over potential type declarations for constants. if ($tokens[$stackPtr]['code'] === T_CONST) { // Constant must always have a value assigned to it, so we can just look for the assignment @@ -178,7 +188,10 @@ public function process(File $phpcsFile, $stackPtr) } for ($i = $param['default_token']; $i < $paramEnd; $i++) { - if (isset($this->targets[$tokens[$i]['code']]) === true) { + if (isset($this->targets[$tokens[$i]['code']]) === true + || ($tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED + && $this->isFQNTrueFalseNull($phpcsFile, $i) === true) + ) { $this->processConstant($phpcsFile, $i); } } @@ -194,6 +207,28 @@ public function process(File $phpcsFile, $stackPtr) }//end process() + /** + * Check if a fully qualified name is a fully qualified true/false/null. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the T_NAME_FULLY_QUALIFIED token in the + * stack passed in $tokens. + * + * @return bool + */ + protected function isFQNTrueFalseNull(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Check for fully qualified true/false/null only. + $compareReadyKeyword = strtolower($tokens[$stackPtr]['content']); + return ($compareReadyKeyword === '\true' + || $compareReadyKeyword === '\false' + || $compareReadyKeyword === '\null'); + + }//end isFQNTrueFalseNull() + + /** * Processes a non-type declaration constant. * diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc index a6a75a7e87..752cd345e5 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc @@ -160,3 +160,7 @@ class SkipOverPHP84FinalProperties { final MyType|FALSE $propA; private static final NULL|MyClass $propB; } + +$a = \NULL; +$a = \falSe; +$a = \True; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed index 2cc52294cd..b44ae194e3 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed @@ -160,3 +160,7 @@ class SkipOverPHP84FinalProperties { final MyType|FALSE $propA; private static final NULL|MyClass $propB; } + +$a = \null; +$a = \false; +$a = \true; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php index d225e72efa..3ea63ef1a7 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php @@ -66,6 +66,9 @@ public function getErrorList($testFile='') 129 => 1, 149 => 1, 153 => 1, + 164 => 1, + 165 => 1, + 166 => 1, ]; default: diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc index 7d81893c66..6e02a1bec7 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc @@ -105,3 +105,7 @@ class SkipOverPHP84FinalProperties { final MyType|false $propA; private static final null|MyClass $propB; } + +$a = \null; +$a = \falSe; +$a = \True; diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed index 26e20f290e..922efce458 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed @@ -105,3 +105,7 @@ class SkipOverPHP84FinalProperties { final MyType|false $propA; private static final null|MyClass $propB; } + +$a = \NULL; +$a = \FALSE; +$a = \TRUE; diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php index 6a90c1805e..e9f1716da2 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php @@ -31,27 +31,30 @@ final class UpperCaseConstantUnitTest extends AbstractSniffTestCase public function getErrorList() { return [ - 7 => 1, - 10 => 1, - 15 => 1, - 16 => 1, - 23 => 1, - 26 => 1, - 31 => 1, - 32 => 1, - 39 => 1, - 42 => 1, - 47 => 1, - 48 => 1, - 70 => 1, - 71 => 1, - 85 => 1, - 87 => 1, - 88 => 1, - 90 => 2, - 92 => 2, - 93 => 1, - 98 => 2, + 7 => 1, + 10 => 1, + 15 => 1, + 16 => 1, + 23 => 1, + 26 => 1, + 31 => 1, + 32 => 1, + 39 => 1, + 42 => 1, + 47 => 1, + 48 => 1, + 70 => 1, + 71 => 1, + 85 => 1, + 87 => 1, + 88 => 1, + 90 => 2, + 92 => 2, + 93 => 1, + 98 => 2, + 109 => 1, + 110 => 1, + 111 => 1, ]; }//end getErrorList() From aded45f3a7bbadf35f3de36c3f45b7b22b5995eb Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Apr 2025 18:27:24 +0200 Subject: [PATCH 7/9] Squiz/ComparisonOperatorUsage: handle FQN `true`/`false`/`null` This commit fixes the sniff to handle "fully qualified true/false" the same as unqualified true/false after the merge of 1020. Includes tests. --- .../Sniffs/Operators/ComparisonOperatorUsageSniff.php | 7 +++++++ .../Tests/Operators/ComparisonOperatorUsageUnitTest.inc | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php b/src/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php index 346b9a6517..ed36226762 100644 --- a/src/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php +++ b/src/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php @@ -174,6 +174,13 @@ public function process(File $phpcsFile, $stackPtr) $foundBooleans++; } + if ($tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED) { + $compareReadyKeyword = strtolower($tokens[$i]['content']); + if ($compareReadyKeyword === '\true' || $compareReadyKeyword === '\false') { + $foundBooleans++; + } + } + if ($tokens[$i]['code'] === T_BOOLEAN_AND || $tokens[$i]['code'] === T_BOOLEAN_OR ) { diff --git a/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc b/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc index 49084540d7..1e18526738 100644 --- a/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc +++ b/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc @@ -146,3 +146,9 @@ if (unqualified($argTags > 0)) {} if (Partially\qualified($argTags > 0)) {} if (\Fully\qualified($argTags > 0)) {} if (namespace\relative($argTags > 0)) {} + +// Verify that FQN true/false are handled the same as unqualified. +if (true) {} +if (\true) {} +for ($var1 = 10; FALSE; $var1--) {} +for ($var1 = 10; \FALSE; $var1--) {} From 34e035725f7346a9081d94809225767e7aa3294d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Apr 2025 18:08:49 +0200 Subject: [PATCH 8/9] PSR12/NullableTypeDeclaration: add tests with FQN true/false/null The sniff already handles this correctly. --- .../PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc | 3 +++ .../Tests/Functions/NullableTypeDeclarationUnitTest.inc.fixed | 3 +++ .../PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.php | 1 + 3 files changed, 7 insertions(+) diff --git a/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc b/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc index d0a12f4697..b0296dfcc4 100644 --- a/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc +++ b/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc @@ -93,3 +93,6 @@ function fooH(): ? // Fatal error: null cannot be marked as nullable, but that's not the concern of this sniff. function fooI(): ? null {} + +// Ensure nullable FQN true/false are handled correctly. +function fqnTrueFalseNull(? \FALSE $paramA, ? \Null $paramB) : ? \true {} diff --git a/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc.fixed index a8ffe2136c..68a21dfc86 100644 --- a/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.inc.fixed @@ -90,3 +90,6 @@ function fooH(): ?false {} // Fatal error: null cannot be marked as nullable, but that's not the concern of this sniff. function fooI(): ?null {} + +// Ensure nullable FQN true/false are handled correctly. +function fqnTrueFalseNull(?\FALSE $paramA, ?\Null $paramB) : ?\true {} diff --git a/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.php b/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.php index 6b15a963c6..416d79fb3a 100644 --- a/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.php +++ b/src/Standards/PSR12/Tests/Functions/NullableTypeDeclarationUnitTest.php @@ -49,6 +49,7 @@ protected function getErrorList() 90 => 1, 91 => 1, 95 => 1, + 98 => 3, ]; }//end getErrorList() From 87f139ae591faa532be6c0b2fc452a312ae81280 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Apr 2025 18:12:42 +0200 Subject: [PATCH 9/9] Squiz/VariableComment: add tests with FQN true/false/null The sniff already handles finding the docblock correctly, even when confronted with these types. --- .../Commenting/VariableCommentUnitTest.inc | 18 ++++++++++++++++++ .../VariableCommentUnitTest.inc.fixed | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc index 08b15e2c44..3889d9d35c 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc @@ -499,3 +499,21 @@ class AllowMoreForSelectivelyIgnoringDisallowedTags { } // phpcs:enable Squiz.Commenting.VariableComment + +class StandaloneFQNNullTrueFalseTypes +{ + /** + * @var null + */ + public \Null $variableName = null; + + /** + * @var true + */ + protected \true $variableName = true; + + /** + * @var false + */ + private \FALSE $variableName = false; +} diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed index 36fe4688cb..de2b109f6b 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed @@ -499,3 +499,21 @@ class AllowMoreForSelectivelyIgnoringDisallowedTags { } // phpcs:enable Squiz.Commenting.VariableComment + +class StandaloneFQNNullTrueFalseTypes +{ + /** + * @var null + */ + public \Null $variableName = null; + + /** + * @var true + */ + protected \true $variableName = true; + + /** + * @var false + */ + private \FALSE $variableName = false; +}