diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c099c7a..0aa9861 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/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/Sniffs/NamingConventions/ValidEnumNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php new file mode 100644 index 0000000..95d427e --- /dev/null +++ b/CakePHP/Sniffs/NamingConventions/ValidEnumNameSniff.php @@ -0,0 +1,76 @@ +getDeclarationName($stackPtr); + + 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/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php index 65037bf..a3ed2f6 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/ValidTraitNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php index f3aae2e..6731605 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/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, ]; } diff --git a/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc b/CakePHP/Tests/Commenting/TypeHintUnitTest.1.inc index d57471c..08d6deb 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 61ca897..942b9a6 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 e39db3f..2871a52 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/Tests/NamingConventions/ValidEnumNameUnitTest.1.inc b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.1.inc new file mode 100644 index 0000000..6b72958 --- /dev/null +++ b/CakePHP/Tests/NamingConventions/ValidEnumNameUnitTest.1.inc @@ -0,0 +1,8 @@ + 1, + ]; + + case 'ValidEnumNameUnitTest.2.inc': + // No errors - enums in Enum namespace don't need suffix + return []; + + default: + return []; + } + } + + /** + * @inheritDoc + */ + public function getWarningList($testFile = '') + { + return []; + } +} diff --git a/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc index e4bc565..837502c 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 2816783..9ac89c5 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, + 30 => 1, + 103 => 1, + 112 => 1, + 136 => 1, ]; } /** * @inheritDoc */ - public function getWarningList() + public function getWarningList(): array { return []; } diff --git a/CakePHP/ruleset.xml b/CakePHP/ruleset.xml index 3bcdbf1..1930e80 100644 --- a/CakePHP/ruleset.xml +++ b/CakePHP/ruleset.xml @@ -18,10 +18,9 @@ - @@ -174,6 +173,7 @@ + @@ -255,6 +255,9 @@ + + + diff --git a/composer.json b/composer.json index da7a648..e7a26b8 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": { diff --git a/docs/README.md b/docs/README.md index 3ee6362..9127808 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ # CakePHP ruleset -The CakePHP standard contains 155 sniffs +The CakePHP standard contains 159 sniffs -CakePHP (18 sniffs) +CakePHP (19 sniffs) ------------------- - CakePHP.Classes.ReturnTypeHint - CakePHP.Commenting.DocBlockAlignment @@ -13,6 +13,7 @@ CakePHP (18 sniffs) - CakePHP.ControlStructures.ElseIfDeclaration - CakePHP.ControlStructures.WhileStructures - CakePHP.Formatting.BlankLineBeforeReturn +- CakePHP.NamingConventions.ValidEnumName - CakePHP.NamingConventions.ValidFunctionName - CakePHP.NamingConventions.ValidTraitName - CakePHP.PHP.DisallowShortOpenTag @@ -94,7 +95,7 @@ PSR12 (17 sniffs) - PSR12.Properties.ConstantVisibility - PSR12.Traits.UseDeclaration -SlevomatCodingStandard (53 sniffs) +SlevomatCodingStandard (56 sniffs) ---------------------------------- - SlevomatCodingStandard.Arrays.TrailingArrayComma - SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing @@ -104,6 +105,7 @@ SlevomatCodingStandard (53 sniffs) - SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces - SlevomatCodingStandard.Classes.ModernClassNameReference - SlevomatCodingStandard.Classes.PropertyDeclaration +- SlevomatCodingStandard.Classes.RequireSelfReference - SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment - SlevomatCodingStandard.Commenting.DocCommentSpacing - SlevomatCodingStandard.Commenting.EmptyComment @@ -113,7 +115,9 @@ SlevomatCodingStandard (53 sniffs) - SlevomatCodingStandard.ControlStructures.DisallowYodaComparison - SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses - SlevomatCodingStandard.ControlStructures.NewWithParentheses +- SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator - SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator +- SlevomatCodingStandard.ControlStructures.RequireNullSafeObjectOperator - SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator - SlevomatCodingStandard.Exceptions.DeadCatch - SlevomatCodingStandard.Functions.ArrowFunctionDeclaration