diff --git a/.editorconfig b/.editorconfig index d7ec37b..b88269d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,6 @@ end_of_line = crlf [*.yml] indent_size = 2 + +[*.neon] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index 59e7ce9..33d633c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,9 +11,11 @@ # Ignore files for distribution archives, generated using `git archive` .editorconfig export-ignore -.git export-ignore +.github export-ignore .gitattributes export-ignore .gitignore export-ignore phpcs.xml export-ignore phpunit.xml.dist export-ignore /CakePHP/Tests export-ignore +phpstan.neon export-ignore +.phive export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a98bd5..c099c7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ on: jobs: testsuite: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - php-version: ['8.1', '8.2', '8.3'] + php-version: ['8.1', '8.2', '8.3', '8.4'] dependencies: ['highest'] include: - php-version: '8.1' @@ -40,20 +40,26 @@ jobs: cs-stan: name: Coding Standard & Static Analysis - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.1' - tools: cs2pr + tools: phive, cs2pr coverage: none - name: Composer install uses: ramsey/composer-install@v3 + - name: Install PHP tools with phive. + run: "phive install --trust-gpg-keys 'CF1A108D0E7AE720,51C67305FFC2E5C0,12CE0F1D262429A5'" + - name: Run PHP CodeSniffer run: vendor/bin/phpcs --report=checkstyle | cs2pr + + - name: Run PHPStan + run: tools/phpstan analyse --error-format=github diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 0000000..65df421 --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,4 @@ + + + + diff --git a/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php b/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php index 52ea811..03d4ec1 100644 --- a/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php +++ b/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php @@ -67,7 +67,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->addError( 'Chaining methods (@return $this) should not have any return-type-hint.', $startIndex, - 'InvalidSelf' + 'InvalidSelf', ); return; @@ -76,7 +76,7 @@ public function process(File $phpcsFile, $stackPtr) $fix = $phpcsFile->addFixableError( 'Chaining methods (@return $this) should not have any return-type-hint (Remove "self").', $startIndex, - 'InvalidSelf' + 'InvalidSelf', ); if (!$fix) { return; @@ -175,7 +175,7 @@ protected function assertNotThisOrStatic(File $phpCsFile, int $stackPointer): vo $phpCsFile->addError( 'Class name repeated, expected `self` or `$this`.', $classNameIndex, - 'InvalidClass' + 'InvalidClass', ); } } @@ -228,9 +228,14 @@ protected function getClassNameWithNamespace(File $phpCsFile): ?string return null; } + $classPointer = $phpCsFile->findPrevious(TokenHelper::$typeKeywordTokenCodes, $lastToken); + if (!$classPointer) { + return null; + } + return ClassHelper::getFullyQualifiedName( $phpCsFile, - $phpCsFile->findPrevious(TokenHelper::$typeKeywordTokenCodes, $lastToken) + $classPointer, ); } } diff --git a/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php b/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php index 41e90c1..c04c154 100644 --- a/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php +++ b/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php @@ -61,7 +61,7 @@ public function process(File $phpcsFile, $stackPtr) $commentBorder = $phpcsFile->findNext( [T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG], $searchToken, - $commentClose + 1 + $commentClose + 1, ); if ($commentBorder !== false) { $tokensToIndent[$commentBorder] = $codeIndentation + 1; diff --git a/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php b/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php index 236d344..c6a3fa2 100644 --- a/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php +++ b/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php @@ -59,14 +59,14 @@ public function process(File $phpcsFile, $stackPtr) $docCommentEnd = $phpcsFile->findPrevious( [T_DOC_COMMENT_CLOSE_TAG, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET], $stackPtr - 1, - null + null, ); if ($docCommentEnd === false || $tokens[$docCommentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { $phpcsFile->addError( 'Missing doc comment for function %s()', $stackPtr, 'Missing', - [$phpcsFile->getDeclarationName($stackPtr)] + [$phpcsFile->getDeclarationName($stackPtr)], ); return; @@ -77,14 +77,14 @@ public function process(File $phpcsFile, $stackPtr) $attribute = $phpcsFile->findNext( [T_ATTRIBUTE], $lastEndToken + 1, - $stackPtr + $stackPtr, ); if ($attribute !== false) { if ($tokens[$lastEndToken]['line'] !== $tokens[$attribute]['line'] - 1) { $phpcsFile->addError( 'There must be no blank lines after the function comment or attribute', $lastEndToken, - 'SpacingAfter' + 'SpacingAfter', ); return; @@ -98,7 +98,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->addError( 'There must be no blank lines after the function comment or attribute', $lastEndToken, - 'SpacingAfter' + 'SpacingAfter', ); } @@ -152,7 +152,7 @@ protected function processThrows(File $phpcsFile, int $stackPtr, int $commentSta if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) { $matches = []; preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[$tag + 2]['content'], $matches); - $exception = $matches[1]; + $exception = $matches[1] ?? null; } if ($exception === null) { diff --git a/CakePHP/Sniffs/Commenting/TypeHintSniff.php b/CakePHP/Sniffs/Commenting/TypeHintSniff.php index affdc84..da82ecf 100644 --- a/CakePHP/Sniffs/Commenting/TypeHintSniff.php +++ b/CakePHP/Sniffs/Commenting/TypeHintSniff.php @@ -32,6 +32,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; /** * Verifies order of types in type hints @@ -128,7 +129,7 @@ public function process(File $phpcsFile, $stackPtr) '%s type hint is not formatted properly, expected "%s"', $tag, 'IncorrectFormat', - [$tokens[$tag]['content'], $sortedTypeHint] + [$tokens[$tag]['content'], $sortedTypeHint], ); if (!$fix) { continue; @@ -140,7 +141,7 @@ public function process(File $phpcsFile, $stackPtr) '%s %s %s', $sortedTypeHint, $valueNode->variableName, - $valueNode->description + $valueNode->description, )); if ($tagComment[-1] === ' ') { // tags above variables in code have a trailing space @@ -152,13 +153,13 @@ public function process(File $phpcsFile, $stackPtr) $sortedTypeHint, $valueNode->isVariadic ? '...' : '', $valueNode->parameterName, - $valueNode->description + $valueNode->description, )); } elseif ($valueNode instanceof ReturnTagValueNode) { $newComment = trim(sprintf( '%s %s', $sortedTypeHint, - $valueNode->description + $valueNode->description, )); } @@ -278,10 +279,10 @@ protected function getSortedTypeHint(array $types): string protected function renderUnionTypes(array $typeNodes): string { // Remove parenthesis added by phpstan around union and intersection types - return preg_replace( + return (string)preg_replace( ['/ ([\|&]) /', '/<\(/', '/\)>/', '/\), /', '/, \(/'], ['${1}', '<', '>', ', ', ', '], - implode('|', $typeNodes) + implode('|', $typeNodes), ); } @@ -294,13 +295,16 @@ protected static function getValueNode(string $tagName, string $tagComment): Php { static $phpDocParser; if (!$phpDocParser) { - $constExprParser = new ConstExprParser(); - $phpDocParser = new PhpDocParser(new TypeParser($constExprParser), $constExprParser); + $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]); + + $constExprParser = new ConstExprParser($config); + $phpDocParser = new PhpDocParser($config, new TypeParser($config, $constExprParser), $constExprParser); } static $phpDocLexer; if (!$phpDocLexer) { - $phpDocLexer = new Lexer(); + $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]); + $phpDocLexer = new Lexer($config); } return $phpDocParser->parseTagValue(new TokenIterator($phpDocLexer->tokenize($tagComment)), $tagName); diff --git a/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php b/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php index 7aa7bfd..916da55 100644 --- a/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php +++ b/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php @@ -72,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr) $fix = $phpcsFile->addFixableError( 'Missing blank line before return statement', $stackPtr, - 'BlankLineBeforeReturn' + 'BlankLineBeforeReturn', ); if ($fix === true) { $phpcsFile->fixer->beginChangeset(); diff --git a/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php b/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php deleted file mode 100644 index e9dafce..0000000 --- a/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php +++ /dev/null @@ -1,52 +0,0 @@ -getTokens(); - $spaces = 0; - - if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { - $spaces = strlen($tokens[$stackPtr + 1]['content']); - } - - if ($spaces !== 1) { - $keyword = $tokens[$stackPtr]['code'] === T_FN ? 'fn' : 'function'; - $error = "Expected 1 space after closure's $keyword keyword; %s found"; - $data = [$spaces]; - $phpcsFile->addError($error, $stackPtr, 'SpaceAfterFunction', $data); - } - } -} diff --git a/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php index 17329df..c7dd3c0 100644 --- a/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -63,6 +63,10 @@ public function __construct() protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope) { $methodName = $phpcsFile->getDeclarationName($stackPtr); + if ($methodName === null) { + return; + } + $className = $phpcsFile->getDeclarationName($currScope); $errorData = [$className . '::' . $methodName]; diff --git a/CakePHP/Sniffs/PHP/SingleQuoteSniff.php b/CakePHP/Sniffs/PHP/SingleQuoteSniff.php index de97077..a07660b 100644 --- a/CakePHP/Sniffs/PHP/SingleQuoteSniff.php +++ b/CakePHP/Sniffs/PHP/SingleQuoteSniff.php @@ -56,7 +56,7 @@ public function process(File $phpcsFile, $stackPtr) $fix = $phpcsFile->addFixableError( 'Use single instead of double quotes for simple strings.', $stackPtr, - 'UseSingleQuote' + 'UseSingleQuote', ); if ($fix) { $content = substr($content, 1, -1); diff --git a/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php b/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php index c488f28..064f04b 100644 --- a/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php +++ b/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php @@ -53,6 +53,9 @@ public function process(File $phpCsFile, $stackPointer) $closingParenthesisIndex = $tokens[$openingParenthesisIndex]['parenthesis_closer']; $semicolonIndex = $phpCsFile->findNext(T_SEMICOLON, $closingParenthesisIndex + 1); + if (!$semicolonIndex) { + return; + } $nextContentIndex = $phpCsFile->findNext(T_WHITESPACE, $semicolonIndex + 1, null, true); @@ -65,7 +68,7 @@ public function process(File $phpCsFile, $stackPointer) $fix = $phpCsFile->addFixableError( 'Every function/method needs a newline afterwards', $closingParenthesisIndex, - 'Abstract' + 'Abstract', ); if ($fix) { $phpCsFile->fixer->addNewline($semicolonIndex); @@ -84,6 +87,9 @@ public function process(File $phpCsFile, $stackPointer) } $nextContentIndex = $phpCsFile->findNext(T_WHITESPACE, $closingBraceIndex + 1, null, true); + if (!$nextContentIndex) { + return; + } // Do not mess with the end of the class if ($tokens[$nextContentIndex]['code'] === T_CLOSE_CURLY_BRACKET) { @@ -108,7 +114,7 @@ protected function assertNewLineAtTheEnd(File $phpCsFile, int $closingBraceIndex $fix = $phpCsFile->addFixableError( 'Every function/method needs a newline afterwards', $closingBraceIndex, - 'Concrete' + 'Concrete', ); if ($fix) { $phpCsFile->fixer->addNewline($closingBraceIndex); @@ -147,6 +153,9 @@ protected function assertNewLineAtTheBeginning(File $phpCsFile, int $stackPointe } $prevContentIndex = $phpCsFile->findPrevious(T_WHITESPACE, $firstTokenInLineIndex - 1, null, true); + if (!$prevContentIndex) { + return; + } // Do not mess with the start of the class if ($tokens[$prevContentIndex]['code'] === T_OPEN_CURLY_BRACKET) { @@ -160,7 +169,7 @@ protected function assertNewLineAtTheBeginning(File $phpCsFile, int $stackPointe $fix = $phpCsFile->addFixableError( 'Every function/method needs a newline before', $firstTokenInLineIndex, - 'Concrete' + 'Concrete', ); if ($fix) { $phpCsFile->fixer->addNewline($prevContentIndex); diff --git a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc b/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc deleted file mode 100644 index 836703f..0000000 --- a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc +++ /dev/null @@ -1,39 +0,0 @@ - echo 'It Fails'; - $visitor = fn ($expression) => echo 'It works'; - } -} - -$foo = 'bar'; -$bar = 'foo'; - -$zum = function()use ($foo, $bar) { - return $foo; -}; - -$zum = function ()use ($foo, $bar) { - return $foo; -}; diff --git a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.php b/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.php deleted file mode 100644 index abe1c68..0000000 --- a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.php +++ /dev/null @@ -1,28 +0,0 @@ - 1, - 25 => 1, - 33 => 1, - ]; - } - - /** - * @inheritDoc - */ - public function getWarningList() - { - return []; - } -} diff --git a/CakePHP/ruleset.xml b/CakePHP/ruleset.xml index c37562e..234e573 100644 --- a/CakePHP/ruleset.xml +++ b/CakePHP/ruleset.xml @@ -96,6 +96,24 @@ + + + + + + + + + + + + + + + + + + @@ -115,17 +133,17 @@ - - - - - + + + + + - + @@ -141,6 +159,7 @@ */tests/* + @@ -200,10 +219,11 @@ */tests/* - + - + + @@ -214,12 +234,19 @@ - + + + + + + + - + + diff --git a/README.md b/README.md index 996d158..ee0b0d9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CakePHP Code Sniffer -![Build Status](https://github.com/cakephp/cakephp-codesniffer/actions/workflows/ci.yml/badge.svg?branch=5.x) +![Build Status](https://github.com/cakephp/cakephp-codesniffer/actions/workflows/ci.yml/badge.svg?branch=5.next) [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/cakephp-codesniffer.svg?style=flat-square)](https://packagist.org/packages/cakephp/cakephp-codesniffer) [![Latest Stable Version](https://img.shields.io/packagist/v/cakephp/cakephp-codesniffer.svg?style=flat-square)](https://packagist.org/packages/cakephp/cakephp-codesniffer) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) diff --git a/composer.json b/composer.json index 1347fc2..e7f83ff 100644 --- a/composer.json +++ b/composer.json @@ -19,8 +19,8 @@ }, "require": { "php": ">=8.1.0", - "phpstan/phpdoc-parser": "^1.4.5", - "slevomat/coding-standard": "^8.15", + "phpstan/phpdoc-parser": "^2.1.0", + "slevomat/coding-standard": "^8.16", "squizlabs/php_codesniffer": "^3.9" }, "require-dev": { @@ -45,6 +45,7 @@ ], "cs-check": "phpcs --colors --parallel=16 -p -s CakePHP/", "cs-fix": "phpcbf --colors --parallel=16 -p CakePHP/", + "stan": "tools/phpstan analyse", "docs": "php docs/generate.php", "explain": "phpcs -e --standard=CakePHP" } diff --git a/docs/README.md b/docs/README.md index 01afebf..c1384cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ # CakePHP ruleset -The CakePHP standard contains 147 sniffs +The CakePHP standard contains 155 sniffs -CakePHP (20 sniffs) +CakePHP (19 sniffs) ------------------- - CakePHP.Classes.ReturnTypeHint - CakePHP.Commenting.DocBlockAlignment @@ -13,7 +13,6 @@ CakePHP (20 sniffs) - CakePHP.ControlStructures.ElseIfDeclaration - CakePHP.ControlStructures.WhileStructures - CakePHP.Formatting.BlankLineBeforeReturn -- CakePHP.Functions.ClosureDeclaration - CakePHP.NamingConventions.ValidFunctionName - CakePHP.NamingConventions.ValidTraitName - CakePHP.PHP.DisallowShortOpenTag @@ -95,11 +94,12 @@ PSR12 (17 sniffs) - PSR12.Properties.ConstantVisibility - PSR12.Traits.UseDeclaration -SlevomatCodingStandard (43 sniffs) +SlevomatCodingStandard (52 sniffs) ---------------------------------- - SlevomatCodingStandard.Arrays.TrailingArrayComma - SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing - SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment +- SlevomatCodingStandard.Classes.BackedEnumTypeSpacing - SlevomatCodingStandard.Classes.ClassConstantVisibility - SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces - SlevomatCodingStandard.Classes.ModernClassNameReference @@ -114,8 +114,16 @@ SlevomatCodingStandard (43 sniffs) - SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses - SlevomatCodingStandard.ControlStructures.NewWithParentheses - SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator +- SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator - SlevomatCodingStandard.Exceptions.DeadCatch - SlevomatCodingStandard.Functions.ArrowFunctionDeclaration +- SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall +- SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse +- SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration +- SlevomatCodingStandard.Functions.NamedArgumentSpacing +- SlevomatCodingStandard.Functions.RequireTrailingCommaInCall +- SlevomatCodingStandard.Functions.RequireTrailingCommaInClosureUse +- SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration - SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses - SlevomatCodingStandard.Namespaces.DisallowGroupUse - SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation @@ -130,6 +138,7 @@ SlevomatCodingStandard (43 sniffs) - SlevomatCodingStandard.PHP.UselessParentheses - SlevomatCodingStandard.PHP.UselessSemicolon - SlevomatCodingStandard.TypeHints.DeclareStrictTypes +- SlevomatCodingStandard.TypeHints.DNFTypeHintFormat - SlevomatCodingStandard.TypeHints.LongTypeHints - SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue - SlevomatCodingStandard.TypeHints.ParameterTypeHint @@ -137,7 +146,6 @@ SlevomatCodingStandard (43 sniffs) - SlevomatCodingStandard.TypeHints.PropertyTypeHint - SlevomatCodingStandard.TypeHints.ReturnTypeHint - SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing -- SlevomatCodingStandard.TypeHints.UnionTypeHintFormat - SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable - SlevomatCodingStandard.Variables.UnusedVariable @@ -174,4 +182,4 @@ Squiz (28 sniffs) Zend (1 sniff) -------------- -- Zend.NamingConventions.ValidVariableName \ No newline at end of file +- Zend.NamingConventions.ValidVariableName diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..f4fa105 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: 8 + paths: + - CakePHP/Sniffs/ + bootstrapFiles: + - tests/phpstan_bootstrap.php + ignoreErrors: + - identifier: missingType.iterableValue diff --git a/tests/phpstan_bootstrap.php b/tests/phpstan_bootstrap.php new file mode 100644 index 0000000..3cc86c5 --- /dev/null +++ b/tests/phpstan_bootstrap.php @@ -0,0 +1,8 @@ +