From be92b75d66333276ea3d57b7f4cb4d5332f73ca4 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 6 Feb 2026 15:17:54 +0100 Subject: [PATCH 1/9] Add stricter naming convention sniffs for CakePHP 6.x - Update ValidFunctionNameSniff to flag underscore-prefixed protected/private methods (except Entity accessor/mutator pattern _get*, _set*) - Add ValidPropertyNameSniff to flag underscore-prefixed properties - Remove PSR2.Classes.PropertyDeclaration.Underscore exclusion from ruleset - Add new Slevomat sniffs: - RequireNullCoalesceEqualOperator - RequireNullSafeObjectOperator - RequireSelfReference - NullTypeHintOnLastPosition - Fix $_magicMethods property naming in sniff itself - Add T_ENUM support to ValidFunctionNameSniff --- .../ValidFunctionNameSniff.php | 19 +++-- .../ValidPropertyNameSniff.php | 69 +++++++++++++++++++ .../ValidFunctionNameUnitTest.inc | 18 ++++- .../ValidFunctionNameUnitTest.php | 12 ++-- .../ValidPropertyNameUnitTest.inc | 39 +++++++++++ .../ValidPropertyNameUnitTest.php | 33 +++++++++ CakePHP/ruleset.xml | 9 ++- 7 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php create mode 100644 CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc create mode 100644 CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.php diff --git a/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php index 65037bf7..a3ed2f68 100644 --- a/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -27,9 +27,9 @@ class ValidFunctionNameSniff extends AbstractScopeSniff /** * A list of all PHP magic methods. * - * @var array + * @var array */ - protected array $_magicMethods = [ + protected array $magicMethods = [ 'construct', 'destruct', 'call', @@ -54,7 +54,7 @@ class ValidFunctionNameSniff extends AbstractScopeSniff */ public function __construct() { - parent::__construct([T_CLASS, T_INTERFACE, T_TRAIT], [T_FUNCTION], true); + parent::__construct([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], [T_FUNCTION], true); } /** @@ -71,7 +71,7 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop $errorData = [$className . '::' . $methodName]; // Ignore magic methods - if (preg_match('/^__(' . implode('|', $this->_magicMethods) . ')$/', $methodName)) { + if (preg_match('/^__(' . implode('|', $this->magicMethods) . ')$/', $methodName)) { return; } @@ -89,6 +89,17 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop return; } + + // Check non-public methods for underscore prefix + if ($isPublic === false && $methodName[0] === '_') { + // Allow CakePHP Entity accessor/mutator pattern: _getField(), _setField() + if (preg_match('/^_(get|set)[A-Z]/', $methodName)) { + return; + } + + $error = 'Non-public method name "%s" should not be prefixed with underscore'; + $phpcsFile->addError($error, $stackPtr, 'ProtectedWithUnderscore', $errorData); + } } /** diff --git a/CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php new file mode 100644 index 00000000..38207176 --- /dev/null +++ b/CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php @@ -0,0 +1,69 @@ +getTokens(); + $propName = ltrim($tokens[$stackPtr]['content'], '$'); + + // Only check properties starting with underscore + if ($propName[0] !== '_') { + return; + } + + $props = $phpcsFile->getMemberProperties($stackPtr); + + // Public properties with underscore are also bad, but less common + // Focus on protected/private which was the old convention + if ($props['scope'] !== 'public') { + $error = 'Non-public property "$%s" should not be prefixed with underscore'; + $phpcsFile->addError($error, $stackPtr, 'PropertyWithUnderscore', [$propName]); + } else { + $error = 'Public property "$%s" must not be prefixed with underscore'; + $phpcsFile->addError($error, $stackPtr, 'PublicPropertyWithUnderscore', [$propName]); + } + } + + /** + * @inheritDoc + */ + protected function processVariable(File $phpcsFile, $stackPtr) + { + // We only care about member variables (properties) + } + + /** + * @inheritDoc + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) + { + // We only care about member variables (properties) + } +} diff --git a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc index e4bc5652..837502cd 100644 --- a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc +++ b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc @@ -29,7 +29,7 @@ class FunctionNames protected function _someFunc() { - // code here + // code here - error: underscore prefix not allowed } protected function noUnderscorePrefix() @@ -40,6 +40,22 @@ class FunctionNames }; } + // Entity accessor/mutator patterns - should be allowed + protected function _getName() + { + return $this->name; + } + + protected function _setName($name) + { + $this->name = $name; + } + + protected function _getFullName() + { + return $this->first_name . ' ' . $this->last_name; + } + public function __call($name, $arguments) { } diff --git a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php index 2816783a..ee750aee 100644 --- a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php +++ b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php @@ -9,19 +9,21 @@ class ValidFunctionNameUnitTest extends AbstractSniffTestCase /** * @inheritDoc */ - public function getErrorList() + public function getErrorList(): array { return [ - 6 => 1, - 87 => 1, - 96 => 1, + 6 => 1, // public function _forbidden + 30 => 1, // protected function _someFunc + 103 => 1, // public function _forbidden (interface) + 112 => 1, // public function _forbidden (trait) + 136 => 1, // protected function _someFunc (trait) ]; } /** * @inheritDoc */ - public function getWarningList() + public function getWarningList(): array { return []; } diff --git a/CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc b/CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc new file mode 100644 index 00000000..d612db27 --- /dev/null +++ b/CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc @@ -0,0 +1,39 @@ + 1, // public $_publicUnderscore + 13 => 1, // protected $_protectedUnderscore + 17 => 1, // private $_privateUnderscore + 21 => 1, // protected static $_protectedStatic + 23 => 1, // private static $_privateStatic + 30 => 1, // public $_publicUnderscore (trait) + 34 => 1, // protected $_protectedUnderscore (trait) + 38 => 1, // private $_privateUnderscore (trait) + ]; + } + + /** + * @inheritDoc + */ + public function getWarningList(): array + { + return []; + } +} diff --git a/CakePHP/ruleset.xml b/CakePHP/ruleset.xml index f41ec35f..13225b57 100644 --- a/CakePHP/ruleset.xml +++ b/CakePHP/ruleset.xml @@ -18,10 +18,9 @@ - @@ -256,6 +255,10 @@ + + + + From 5a998250eeff8e5fa857837eb1d1712a102ff499 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 6 Feb 2026 18:42:35 +0100 Subject: [PATCH 2/9] Remove custom ValidPropertyNameSniff, use PSR2 built-in instead --- .../ValidPropertyNameSniff.php | 69 ------------------- .../ValidPropertyNameUnitTest.inc | 39 ----------- .../ValidPropertyNameUnitTest.php | 33 --------- 3 files changed, 141 deletions(-) delete mode 100644 CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php delete mode 100644 CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc delete mode 100644 CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.php diff --git a/CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php deleted file mode 100644 index 38207176..00000000 --- a/CakePHP/Sniffs/NamingConventions/ValidPropertyNameSniff.php +++ /dev/null @@ -1,69 +0,0 @@ -getTokens(); - $propName = ltrim($tokens[$stackPtr]['content'], '$'); - - // Only check properties starting with underscore - if ($propName[0] !== '_') { - return; - } - - $props = $phpcsFile->getMemberProperties($stackPtr); - - // Public properties with underscore are also bad, but less common - // Focus on protected/private which was the old convention - if ($props['scope'] !== 'public') { - $error = 'Non-public property "$%s" should not be prefixed with underscore'; - $phpcsFile->addError($error, $stackPtr, 'PropertyWithUnderscore', [$propName]); - } else { - $error = 'Public property "$%s" must not be prefixed with underscore'; - $phpcsFile->addError($error, $stackPtr, 'PublicPropertyWithUnderscore', [$propName]); - } - } - - /** - * @inheritDoc - */ - protected function processVariable(File $phpcsFile, $stackPtr) - { - // We only care about member variables (properties) - } - - /** - * @inheritDoc - */ - protected function processVariableInString(File $phpcsFile, $stackPtr) - { - // We only care about member variables (properties) - } -} diff --git a/CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc b/CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc deleted file mode 100644 index d612db27..00000000 --- a/CakePHP/Tests/NamingConventions/ValidPropertyNameUnitTest.inc +++ /dev/null @@ -1,39 +0,0 @@ - 1, // public $_publicUnderscore - 13 => 1, // protected $_protectedUnderscore - 17 => 1, // private $_privateUnderscore - 21 => 1, // protected static $_protectedStatic - 23 => 1, // private static $_privateStatic - 30 => 1, // public $_publicUnderscore (trait) - 34 => 1, // protected $_protectedUnderscore (trait) - 38 => 1, // private $_privateUnderscore (trait) - ]; - } - - /** - * @inheritDoc - */ - public function getWarningList(): array - { - return []; - } -} From 6446baa5d74760423186af67f375aaf089b07b29 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 6 Feb 2026 19:17:36 +0100 Subject: [PATCH 3/9] Fix coding standard - remove double spaces --- .../NamingConventions/ValidFunctionNameUnitTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php index ee750aee..9ac89c5c 100644 --- a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php +++ b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php @@ -12,11 +12,11 @@ class ValidFunctionNameUnitTest extends AbstractSniffTestCase public function getErrorList(): array { return [ - 6 => 1, // public function _forbidden - 30 => 1, // protected function _someFunc - 103 => 1, // public function _forbidden (interface) - 112 => 1, // public function _forbidden (trait) - 136 => 1, // protected function _someFunc (trait) + 6 => 1, + 30 => 1, + 103 => 1, + 112 => 1, + 136 => 1, ]; } From 9cf1618076cf82db9528f2c556cef1098d9b6a51 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sat, 14 Mar 2026 11:43:15 +0100 Subject: [PATCH 4/9] add custom enum sniff (#425) --- .../NamingConventions/ValidEnumNameSniff.php | 46 +++++++++++++++++++ .../NamingConventions/ValidTraitNameSniff.php | 2 +- .../ValidEnumNameUnitTest.inc | 8 ++++ .../ValidEnumNameUnitTest.php | 26 +++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php create mode 100644 CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.inc create mode 100644 CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.php diff --git a/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php new file mode 100644 index 00000000..93f6939f --- /dev/null +++ b/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php @@ -0,0 +1,46 @@ +getDeclarationName($stackPtr); + + if (!str_ends_with($enumName, 'Enum')) { + $error = 'Enums must have an "Enum" suffix.'; + $phpcsFile->addError($error, $stackPtr, 'InvalidEnumName'); + } + } +} diff --git a/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php index f3aae2e3..67316050 100644 --- a/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php +++ b/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php @@ -39,7 +39,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $traitName = $tokens[$stackPtr + 2]['content']; - if (substr($traitName, -5) !== 'Trait') { + if (!str_ends_with($traitName, 'Trait')) { $error = 'Traits must have a "Trait" suffix.'; $phpcsFile->addError($error, $stackPtr, 'InvalidTraitName'); } diff --git a/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.inc b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.inc new file mode 100644 index 00000000..6b72958e --- /dev/null +++ b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.inc @@ -0,0 +1,8 @@ + 1, + ]; + } + + /** + * @inheritDoc + */ + public function getWarningList() + { + return []; + } +} From 1950643be45d1178c82f07bc0987b74b71359f7c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 14 Mar 2026 14:56:40 +0100 Subject: [PATCH 5/9] Allow enum suffix omission when in Enum namespace (#428) Relax the ValidEnumNameSniff to allow omitting the "Enum" suffix when the enum is in a namespace containing "Enum" as a segment (e.g., `App\Model\Enum\Status` is now valid without the suffix). This avoids redundant naming like `App\Model\Enum\FooBarEnum`. Refs #425 --- .../NamingConventions/ValidEnumNameSniff.php | 34 +++++++++++++++++-- ...itTest.inc => ValidEnumNameUnitTest.1.inc} | 0 .../ValidEnumNameUnitTest.2.inc | 12 +++++++ .../ValidEnumNameUnitTest.php | 20 ++++++++--- 4 files changed, 59 insertions(+), 7 deletions(-) rename CakePHP/Tests/NamingConventions/{ValidEnumNameUnitTest.inc => ValidEnumNameUnitTest.1.inc} (100%) create mode 100644 CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.2.inc diff --git a/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php index 93f6939f..95d427e0 100644 --- a/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php +++ b/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php @@ -1,4 +1,6 @@ getDeclarationName($stackPtr); - if (!str_ends_with($enumName, 'Enum')) { - $error = 'Enums must have an "Enum" suffix.'; + if (!str_ends_with($enumName, 'Enum') && !$this->isInEnumNamespace($phpcsFile)) { + $error = 'Enums must have an "Enum" suffix (or be in an Enum namespace).'; $phpcsFile->addError($error, $stackPtr, 'InvalidEnumName'); } } + + /** + * Check if the file's namespace contains "Enum" as a segment. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @return bool + */ + protected function isInEnumNamespace(File $phpcsFile): bool + { + $tokens = $phpcsFile->getTokens(); + $namespacePtr = $phpcsFile->findNext(T_NAMESPACE, 0); + + if ($namespacePtr === false) { + return false; + } + + $namespaceEnd = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $namespacePtr); + $namespace = ''; + + for ($i = $namespacePtr + 1; $i < $namespaceEnd; $i++) { + if ($tokens[$i]['code'] === T_STRING || $tokens[$i]['code'] === T_NAME_QUALIFIED) { + $namespace .= $tokens[$i]['content']; + } + } + + // Check if namespace ends with \Enum or contains \Enum\ + return (bool)preg_match('/\\\\Enum(\\\\|$)/', $namespace); + } } diff --git a/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.inc b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.1.inc similarity index 100% rename from CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.inc rename to CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.1.inc diff --git a/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.2.inc b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.2.inc new file mode 100644 index 00000000..f01853e3 --- /dev/null +++ b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.2.inc @@ -0,0 +1,12 @@ + 1, - ]; + switch ($testFile) { + case 'ValidEnumNameUnitTest.1.inc': + return [ + 2 => 1, + ]; + + case 'ValidEnumNameUnitTest.2.inc': + // No errors - enums in Enum namespace don't need suffix + return []; + + default: + return []; + } } /** * @inheritDoc */ - public function getWarningList() + public function getWarningList($testFile = '') { return []; } From a747422481c48c97e908aefd2a1217d775f11175 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 14 Mar 2026 16:50:05 +0100 Subject: [PATCH 6/9] Merge pull request #427 from cakephp/fix-void-null-type-order-conflict Fix conflict between TypeHintSniff and Slevomat null position sniffs --- CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc | 9 +++++++++ CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc.fixed | 9 +++++++++ CakePHP/Tests/Commenting/TypeHintUnitTest.php | 1 + CakePHP/ruleset.xml | 2 -- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc b/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc index d57471c0..08d6debd 100644 --- a/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc +++ b/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc @@ -45,3 +45,12 @@ function test() function intersection($param) { } + +/** + * Void must be last, after null. + * + * @return int|void|null + */ +function voidAfterNull() +{ +} diff --git a/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc.fixed b/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc.fixed index 61ca897a..942b9a60 100644 --- a/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc.fixed +++ b/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc.fixed @@ -45,3 +45,12 @@ function test() function intersection($param) { } + +/** + * Void must be last, after null. + * + * @return int|null|void + */ +function voidAfterNull() +{ +} diff --git a/CakePHP/Tests/Commenting/TypeHintUnitTest.php b/CakePHP/Tests/Commenting/TypeHintUnitTest.php index e39db3fa..2871a524 100644 --- a/CakePHP/Tests/Commenting/TypeHintUnitTest.php +++ b/CakePHP/Tests/Commenting/TypeHintUnitTest.php @@ -32,6 +32,7 @@ public function getWarningList($testFile = '') 27 => 1, 37 => 1, 42 => 1, + 52 => 1, ]; default: diff --git a/CakePHP/ruleset.xml b/CakePHP/ruleset.xml index 13225b57..ef3bd16d 100644 --- a/CakePHP/ruleset.xml +++ b/CakePHP/ruleset.xml @@ -233,7 +233,6 @@ - */tests/* @@ -258,7 +257,6 @@ - From c8f3c35692eed28884e67585d8ed8cd8b16b1f93 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 11 Apr 2026 12:32:54 +0200 Subject: [PATCH 7/9] Disallow partial uses in ReferenceUsedNamesOnly (#431) Partial namespace references like Mockery\MockInterface were not being flagged by ReferenceUsedNamesOnly because Slevomat's allowPartialUses defaults to true. Set it to false so such references are required to be imported via a use statement. --- CakePHP/ruleset.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/CakePHP/ruleset.xml b/CakePHP/ruleset.xml index ef3bd16d..79226df7 100644 --- a/CakePHP/ruleset.xml +++ b/CakePHP/ruleset.xml @@ -173,6 +173,7 @@ + From 8b24cb006b4c365e65ea625e0ccec261abc78d47 Mon Sep 17 00:00:00 2001 From: ADmad Date: Tue, 22 Sep 2026 00:00:24 +0530 Subject: [PATCH 8/9] Update ReturnTypeSniff It now ensures that methods with `@return $this` in the docblock have the return type as `static`. --- .../Sniffs/Classes/ReturnTypeHintSniff.php | 55 ++++++++++++++----- .../Tests/Classes/ReturnTypeHintUnitTest.inc | 11 +++- .../Classes/ReturnTypeHintUnitTest.inc.fixed | 13 ++++- .../Tests/Classes/ReturnTypeHintUnitTest.php | 1 + 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php b/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php index 3fd3fb0b..df43cedf 100644 --- a/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php +++ b/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php @@ -41,40 +41,67 @@ public function process(File $phpcsFile, $stackPtr) $closeParenthesisIndex = $tokens[$openParenthesisIndex]['parenthesis_closer']; $colonIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenthesisIndex + 1, null, true); - if (!$colonIndex) { + + if (!$this->isChainingMethod($phpcsFile, $stackPtr)) { + if ($colonIndex && $tokens[$colonIndex]['code'] === T_COLON) { + $this->assertNotThisOrStatic($phpcsFile, $stackPtr); + } + return; } - $startIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $colonIndex + 1, $colonIndex + 3, true); - if (!$startIndex) { + // We skip for interface methods + if (empty($tokens[$stackPtr]['scope_opener']) || empty($tokens[$stackPtr]['scope_closer'])) { return; } - if (!$this->isChainingMethod($phpcsFile, $stackPtr)) { - $this->assertNotThisOrStatic($phpcsFile, $stackPtr); + // No colon means no return type hint - add static + if (!$colonIndex || $tokens[$colonIndex]['code'] !== T_COLON) { + $fix = $phpcsFile->addFixableError( + 'Chaining methods (@return $this) should have "static" return type.', + $closeParenthesisIndex, + 'MissingStatic', + ); + if (!$fix) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($closeParenthesisIndex, ': static'); + $phpcsFile->fixer->endChangeset(); return; } - // We skip for interface methods - if (empty($tokens[$stackPtr]['scope_opener']) || empty($tokens[$stackPtr]['scope_closer'])) { + $startIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $colonIndex + 1, $colonIndex + 3, true); + if (!$startIndex) { return; } $returnTokenCode = $tokens[$startIndex]['code']; + if ($returnTokenCode === T_STATIC) { + return; + } + if ($returnTokenCode !== T_SELF) { - // Then we can only warn, but not auto-fix - $phpcsFile->addError( - 'Chaining methods (@return $this) should not have any return-type-hint.', + $fix = $phpcsFile->addFixableError( + 'Chaining methods (@return $this) should have "static" return type.', $startIndex, 'InvalidSelf', ); + if (!$fix) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($startIndex, 'static'); + $phpcsFile->fixer->endChangeset(); return; } $fix = $phpcsFile->addFixableError( - 'Chaining methods (@return $this) should not have any return-type-hint (Remove "self").', + 'Chaining methods (@return $this) should have "static" return type instead of "self".', $startIndex, 'InvalidSelf', ); @@ -83,9 +110,7 @@ public function process(File $phpcsFile, $stackPtr) } $phpcsFile->fixer->beginChangeset(); - for ($i = $colonIndex; $i <= $startIndex; $i++) { - $phpcsFile->fixer->replaceToken($i, ''); - } + $phpcsFile->fixer->replaceToken($startIndex, 'static'); $phpcsFile->fixer->endChangeset(); } @@ -173,7 +198,7 @@ protected function assertNotThisOrStatic(File $phpCsFile, int $stackPointer): vo } $phpCsFile->addError( - 'Class name repeated, expected `self` or `$this`.', + 'Class name repeated, expected `static` or `$this`.', $classNameIndex, 'InvalidClass', ); diff --git a/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc b/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc index ab796ec6..a7b21903 100644 --- a/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc +++ b/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc @@ -6,14 +6,21 @@ class Foo /** * @return $this */ - public function correct() + public function correct(): static { } /** * @return $this */ - public function incorrect(): Foo + public function incorrect() + { + } + + /** + * @return $this + */ + public function incorrectClass(): Foo { } diff --git a/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc.fixed b/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc.fixed index bbdd97aa..960742e7 100644 --- a/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc.fixed +++ b/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.inc.fixed @@ -6,21 +6,28 @@ class Foo /** * @return $this */ - public function correct() + public function correct(): static { } /** * @return $this */ - public function incorrect(): Foo + public function incorrect(): static { } /** * @return $this */ - public function incorrectSelf() + public function incorrectClass(): static + { + } + + /** + * @return $this + */ + public function incorrectSelf(): static { } } diff --git a/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.php b/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.php index e62f829d..8f2898f7 100644 --- a/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.php +++ b/CakePHP/Tests/Classes/ReturnTypeHintUnitTest.php @@ -14,6 +14,7 @@ public function getErrorList() return [ 16 => 1, 23 => 1, + 30 => 1, ]; } From 2e64569b41a8710fa66ff25607ebfb043f6e921e Mon Sep 17 00:00:00 2001 From: ADmad Date: Tue, 22 Sep 2026 12:29:35 +0530 Subject: [PATCH 9/9] Update PHP version and dependencies constraints --- .github/workflows/ci.yml | 7 ++++--- composer.json | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c099c7a7..0aa98616 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - master - '5.x' + - '6.x' pull_request: branches: - '*' @@ -15,10 +16,10 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1', '8.2', '8.3', '8.4'] + php-version: ['8.4', '8.5'] dependencies: ['highest'] include: - - php-version: '8.1' + - php-version: '8.4' dependencies: 'lowest' steps: @@ -48,7 +49,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.4' tools: phive, cs2pr coverage: none diff --git a/composer.json b/composer.json index da7a648d..e7a26b8b 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,14 @@ "source": "https://github.com/cakephp/cakephp-codesniffer" }, "require": { - "php": ">=8.1", + "php": ">=8.4", "dealerdirect/phpcodesniffer-composer-installer": "^1.1.2", "phpstan/phpdoc-parser": "^2.1", "slevomat/coding-standard": "^8.23", "squizlabs/php_codesniffer": "^4.0.2" }, "require-dev": { - "phpunit/phpunit": "^10.5.32 || ^11.3.3" + "phpunit/phpunit": "^12.1.3 || ^13.0" }, "autoload": { "psr-4": {