From 8b24cb006b4c365e65ea625e0ccec261abc78d47 Mon Sep 17 00:00:00 2001 From: ADmad Date: Tue, 22 Sep 2026 00:00:24 +0530 Subject: [PATCH] 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 3fd3fb0..df43ced 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 ab796ec..a7b2190 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 bbdd97a..960742e 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 e62f829..8f2898f 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, ]; }