From 44b105c78feb6621435c8f551c444897db091684 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 17 Sep 2025 11:20:13 +0200 Subject: [PATCH 1/8] "squizlabs/php_codesniffer": "^4.0.0" --- .../Sniffs/Classes/MethodDeclarationSniff.php | 2 +- .../Sniffs/Classes/MethodTypeHintSniff.php | 2 +- .../DisallowArrayTypeHintSyntaxSniff.php | 6 +- .../Commenting/DocBlockReturnVoidSniff.php | 2 +- .../Sniffs/Commenting/DocBlockThrowsSniff.php | 7 ++- .../Sniffs/Commenting/TypeHintSniff.php | 2 +- .../Formatting/ArrayDeclarationSniff.php | 56 +++++++++---------- .../Sniffs/Namespaces/UseStatementSniff.php | 4 +- .../WhiteSpace/DocBlockSpacingSniff.php | 9 ++- composer.json | 12 ++-- phpcs.xml | 1 - phpstan.neon | 4 ++ 12 files changed, 61 insertions(+), 46 deletions(-) diff --git a/PhpCollective/Sniffs/Classes/MethodDeclarationSniff.php b/PhpCollective/Sniffs/Classes/MethodDeclarationSniff.php index 768813a..0c9d8ad 100644 --- a/PhpCollective/Sniffs/Classes/MethodDeclarationSniff.php +++ b/PhpCollective/Sniffs/Classes/MethodDeclarationSniff.php @@ -40,7 +40,7 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop $tokens = $phpcsFile->getTokens(); $methodName = $phpcsFile->getDeclarationName($stackPtr); - if ($methodName === null) { + if (!$methodName) { // Ignore closures. return; } diff --git a/PhpCollective/Sniffs/Classes/MethodTypeHintSniff.php b/PhpCollective/Sniffs/Classes/MethodTypeHintSniff.php index d3a9028..c0e7487 100644 --- a/PhpCollective/Sniffs/Classes/MethodTypeHintSniff.php +++ b/PhpCollective/Sniffs/Classes/MethodTypeHintSniff.php @@ -50,7 +50,7 @@ public function process(File $phpcsFile, $stackPtr): void $j = $startIndex; $extractedUseStatement = ''; while (true) { - if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_RETURN_TYPE], $tokens[$j])) { + if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$j])) { break; } diff --git a/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php b/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php index 72a09bd..55434ce 100644 --- a/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php +++ b/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php @@ -169,7 +169,7 @@ public function process(File $phpcsFile, $pointer): void */ protected function fixAnnotation(File $phpcsFile, Annotation $annotation, string $fixedAnnotation): void { - /** @var \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode|mixed $value */ + /** @var \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode $value */ $value = $annotation->getNode()->value; $parameterName = $value->parameterName ?? ''; $variableName = $value->variableName ?? ''; @@ -220,9 +220,9 @@ public function getArrayTypeNodes(Node $node): array /** * @param \PHPStan\PhpDocParser\Ast\Node $node * - * @return \PHPStan\PhpDocParser\Ast\Node|list<\PHPStan\PhpDocParser\Ast\Node>|\PHPStan\PhpDocParser\Ast\NodeTraverser|int|null + * @return int|null */ - public function enterNode(Node $node): Node|array|\PHPStan\PhpDocParser\Ast\NodeTraverser|int|null + public function enterNode(Node $node): int|null { if ($node instanceof ArrayTypeNode) { $this->nodes[] = $node; diff --git a/PhpCollective/Sniffs/Commenting/DocBlockReturnVoidSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockReturnVoidSniff.php index f87875d..b9939f9 100644 --- a/PhpCollective/Sniffs/Commenting/DocBlockReturnVoidSniff.php +++ b/PhpCollective/Sniffs/Commenting/DocBlockReturnVoidSniff.php @@ -320,7 +320,7 @@ protected function assertTypeHint(File $phpcsFile, int $stackPtr, int $docBlockR $typehint = '?' . $typehint; } - if ($documentedReturnType !== 'void' && $typeHintIndex !== 'void') { + if ($documentedReturnType !== 'void' && $typehint !== 'void') { return; } if ($documentedReturnType === $typehint) { diff --git a/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php index 8494e54..ca10094 100644 --- a/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php +++ b/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php @@ -51,7 +51,12 @@ public function process(File $phpCsFile, $stackPointer): void return; } - if ($phpCsFile->getDeclarationName($stackPointer) === null) { + try { + $name = $phpCsFile->getDeclarationName($stackPointer); + } catch (Exception $e) { + return; + } + if (!$name) { return; } diff --git a/PhpCollective/Sniffs/Commenting/TypeHintSniff.php b/PhpCollective/Sniffs/Commenting/TypeHintSniff.php index 3870f33..688358f 100644 --- a/PhpCollective/Sniffs/Commenting/TypeHintSniff.php +++ b/PhpCollective/Sniffs/Commenting/TypeHintSniff.php @@ -126,7 +126,7 @@ public function process(File $phpcsFile, $stackPtr): void continue; } - /** @phpstan-var \PHPStan\PhpDocParser\Ast\Type\GenericTypeNode|\PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode $valueNode */ + /** @phpstan-var \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode $valueNode */ if ($valueNode->type instanceof UnionTypeNode) { $types = $valueNode->type->types; } elseif ($valueNode->type instanceof ArrayTypeNode) { diff --git a/PhpCollective/Sniffs/Formatting/ArrayDeclarationSniff.php b/PhpCollective/Sniffs/Formatting/ArrayDeclarationSniff.php index 4ad02a9..5166a69 100644 --- a/PhpCollective/Sniffs/Formatting/ArrayDeclarationSniff.php +++ b/PhpCollective/Sniffs/Formatting/ArrayDeclarationSniff.php @@ -291,39 +291,37 @@ public function processMultiLineArray(File $phpcsFile, int $stackPtr, int $array continue; } - if ($tokens[$nextToken]['code'] === T_DOUBLE_ARROW) { - $currentEntry['arrow'] = $nextToken; - $keyUsed = true; + $currentEntry['arrow'] = $nextToken; + $keyUsed = true; - // Find the start of index that uses this double arrow. - $indexEnd = (int)$phpcsFile->findPrevious(T_WHITESPACE, ($nextToken - 1), $arrayStart, true); - $indexStart = $phpcsFile->findStartOfStatement($indexEnd); + // Find the start of index that uses this double arrow. + $indexEnd = (int)$phpcsFile->findPrevious(T_WHITESPACE, ($nextToken - 1), $arrayStart, true); + $indexStart = $phpcsFile->findStartOfStatement($indexEnd); - if ($indexStart === $indexEnd) { - $currentEntry['index'] = $indexEnd; - $currentEntry['index_content'] = $tokens[$indexEnd]['content']; - } else { - $currentEntry['index'] = $indexStart; - $currentEntry['index_content'] = $phpcsFile->getTokensAsString($indexStart, ($indexEnd - $indexStart + 1)); - } - - $indexLength = strlen($currentEntry['index_content']); - if ($maxLength < $indexLength) { - $maxLength = $indexLength; - } - - // Find the value of this index. - $nextContent = $phpcsFile->findNext( - Tokens::$emptyTokens, - ($nextToken + 1), - $arrayEnd, - true, - ); + if ($indexStart === $indexEnd) { + $currentEntry['index'] = $indexEnd; + $currentEntry['index_content'] = $tokens[$indexEnd]['content']; + } else { + $currentEntry['index'] = $indexStart; + $currentEntry['index_content'] = $phpcsFile->getTokensAsString($indexStart, ($indexEnd - $indexStart + 1)); + } - $currentEntry['value'] = $nextContent; - $indices[] = $currentEntry; - $lastToken = $nextToken; + $indexLength = strlen($currentEntry['index_content']); + if ($maxLength < $indexLength) { + $maxLength = $indexLength; } + + // Find the value of this index. + $nextContent = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($nextToken + 1), + $arrayEnd, + true, + ); + + $currentEntry['value'] = $nextContent; + $indices[] = $currentEntry; + $lastToken = $nextToken; } $numValues = count($indices); diff --git a/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php b/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php index f92f5ad..f370c0d 100644 --- a/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php +++ b/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php @@ -574,7 +574,7 @@ protected function checkUseForReturnTypeHint(File $phpcsFile, int $stackPtr): vo $extractedUseStatement = ''; $lastSeparatorIndex = null; while (true) { - if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_RETURN_TYPE], $tokens[$j])) { + if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$j])) { break; } @@ -644,7 +644,7 @@ protected function checkPropertyForInstanceOf(File $phpcsFile, int $stackPtr): v $extractedUseStatement = ''; $lastSeparatorIndex = null; while (true) { - if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_RETURN_TYPE], $tokens[$j])) { + if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$j])) { break; } diff --git a/PhpCollective/Sniffs/WhiteSpace/DocBlockSpacingSniff.php b/PhpCollective/Sniffs/WhiteSpace/DocBlockSpacingSniff.php index fcac646..521e2c3 100644 --- a/PhpCollective/Sniffs/WhiteSpace/DocBlockSpacingSniff.php +++ b/PhpCollective/Sniffs/WhiteSpace/DocBlockSpacingSniff.php @@ -23,7 +23,14 @@ class DocBlockSpacingSniff implements Sniff */ public function register(): array { - return [T_CLASS, T_INTERFACE, T_TRAIT, T_FUNCTION, T_PROPERTY]; + $tokens = [T_CLASS, T_INTERFACE, T_TRAIT, T_FUNCTION]; + + // T_PROPERTY was introduced in PHP 8.4 for property hooks + if (defined('T_PROPERTY')) { + $tokens[] = T_PROPERTY; + } + + return $tokens; } /** diff --git a/composer.json b/composer.json index 44713cf..b3779b4 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,12 @@ ], "require": { "php": ">=8.1", - "phpcsstandards/phpcsextra": "^1.2.0", - "slevomat/coding-standard": "^8.16.0", - "squizlabs/php_codesniffer": "^3.11.3" + "phpcsstandards/phpcsextra": "dev-develop", + "slevomat/coding-standard": "dev-phpcs4", + "squizlabs/php_codesniffer": "^4.0.0" }, "require-dev": { - "phpstan/phpstan": "^1.0.0", + "phpstan/phpstan": "^2.0.0", "phpunit/phpunit": "^10.3 || ^11.2 || ^12.0" }, "autoload": { @@ -72,5 +72,7 @@ "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpcs.xml b/phpcs.xml index 45a7df4..e8044a9 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,5 @@ - diff --git a/phpstan.neon b/phpstan.neon index cc80eee..e410672 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,7 @@ parameters: - '%rootDir%/../../../tests/bootstrap.php' ignoreErrors: - identifier: missingType.generics + - identifier: trait.unused + - + message: '#Parameter \#1 \$settings of static method SlevomatCodingStandard\\Helpers\\SniffSettingsHelper::normalizeArray\(\) expects list, array given#' + path: PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php From 6daa646aadd465da69280fdadecd5a7822563c88 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 18 Sep 2025 10:59:19 +0200 Subject: [PATCH 2/8] "squizlabs/php_codesniffer": "^4.0.0" --- .../Sniffs/Commenting/DocBlockThrowsSniff.php | 18 ++++- .../Sniffs/Namespaces/UseStatementSniff.php | 77 ++++++++++++------- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php index ca10094..4bbdeec 100644 --- a/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php +++ b/PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php @@ -144,6 +144,12 @@ protected function extractExceptions(File $phpCsFile, int $stackPointer): array continue; } + // Handle the case where there's no namespace separator, string token, or fully qualified name token + // (e.g., for parenthesis after 'new') + if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_NAME_FULLY_QUALIFIED], $tokens[$contentIndex])) { + continue; + } + $exceptions[] = $this->extractException($phpCsFile, $contentIndex); continue; @@ -228,9 +234,17 @@ protected function extractException(File $phpCsFile, int $contentIndex): array $fullClass = ''; $position = $contentIndex; - while ($this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$position])) { - $fullClass .= $tokens[$position]['content']; + + // Handle T_NAME_FULLY_QUALIFIED token (e.g., \BadMethodCallException) + // or separate tokens (T_NS_SEPARATOR and T_STRING) + if ($this->isGivenKind([T_NAME_FULLY_QUALIFIED], $tokens[$position])) { + $fullClass = $tokens[$position]['content']; ++$position; + } else { + while ($this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$position])) { + $fullClass .= $tokens[$position]['content']; + ++$position; + } } $class = $fullClass = ltrim($fullClass, '\\'); diff --git a/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php b/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php index f370c0d..a803484 100644 --- a/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php +++ b/PhpCollective/Sniffs/Namespaces/UseStatementSniff.php @@ -639,32 +639,45 @@ protected function checkPropertyForInstanceOf(File $phpcsFile, int $stackPtr): v return; } - $lastIndex = null; - $j = $startIndex; - $extractedUseStatement = ''; - $lastSeparatorIndex = null; - while (true) { - if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$j])) { - break; + // Handle T_NAME_FULLY_QUALIFIED token (PHP CodeSniffer v4) + $className = ''; + if ($tokens[$startIndex]['code'] === T_NAME_FULLY_QUALIFIED) { + $extractedUseStatement = ltrim($tokens[$startIndex]['content'], '\\'); + if (strpos($extractedUseStatement, '\\') === false) { + return; // Not a namespaced class } + $lastSeparatorPos = strrpos($extractedUseStatement, '\\'); + $className = substr($extractedUseStatement, $lastSeparatorPos + 1); + $lastIndex = $startIndex; + $lastSeparatorIndex = null; // No separate separator token in v4 + } else { + // Handle separate tokens (T_NS_SEPARATOR and T_STRING) + $lastIndex = null; + $j = $startIndex; + $extractedUseStatement = ''; + $lastSeparatorIndex = null; + while (true) { + if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$j])) { + break; + } - $lastIndex = $j; - $extractedUseStatement .= $tokens[$j]['content']; - if ($this->isGivenKind([T_NS_SEPARATOR], $tokens[$j])) { - $lastSeparatorIndex = $j; + $lastIndex = $j; + $extractedUseStatement .= $tokens[$j]['content']; + if ($this->isGivenKind([T_NS_SEPARATOR], $tokens[$j])) { + $lastSeparatorIndex = $j; + } + ++$j; } - ++$j; - } - if ($lastIndex === null || $lastSeparatorIndex === null) { - return; - } - - $extractedUseStatement = ltrim($extractedUseStatement, '\\'); + if ($lastIndex === null || $lastSeparatorIndex === null) { + return; + } - $className = ''; - for ($k = $lastSeparatorIndex + 1; $k <= $lastIndex; ++$k) { - $className .= $tokens[$k]['content']; + $extractedUseStatement = ltrim($extractedUseStatement, '\\'); + $className = ''; + for ($k = $lastSeparatorIndex + 1; $k <= $lastIndex; ++$k) { + $className .= $tokens[$k]['content']; + } } $error = 'Use statement ' . $extractedUseStatement . ' for class ' . $className . ' should be in use block.'; @@ -677,13 +690,23 @@ protected function checkPropertyForInstanceOf(File $phpcsFile, int $stackPtr): v $addedUseStatement = $this->addUseStatement($phpcsFile, $className, $extractedUseStatement); - for ($k = $lastSeparatorIndex; $k > $startIndex; --$k) { - $phpcsFile->fixer->replaceToken($k, ''); - } - $phpcsFile->fixer->replaceToken($startIndex, ''); + if ($lastSeparatorIndex !== null) { + // Legacy: remove individual tokens + for ($k = $lastSeparatorIndex; $k > $startIndex; --$k) { + $phpcsFile->fixer->replaceToken($k, ''); + } + $phpcsFile->fixer->replaceToken($startIndex, ''); - if ($addedUseStatement['alias'] !== null) { - $phpcsFile->fixer->replaceToken($lastIndex, $addedUseStatement['alias']); + if ($addedUseStatement['alias'] !== null) { + $phpcsFile->fixer->replaceToken($lastIndex, $addedUseStatement['alias']); + } + } else { + // PHP CodeSniffer v4: replace single T_NAME_FULLY_QUALIFIED token + if ($addedUseStatement['alias'] !== null) { + $phpcsFile->fixer->replaceToken($startIndex, $addedUseStatement['alias']); + } else { + $phpcsFile->fixer->replaceToken($startIndex, $className); + } } $phpcsFile->fixer->endChangeset(); From 4238c0e5d388c094eb1ae72b4d7b9fb735701bfb Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 18 Sep 2025 11:08:20 +0200 Subject: [PATCH 3/8] "squizlabs/php_codesniffer": "^4.0.0" --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b3779b4..c2032dd 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require": { "php": ">=8.1", "phpcsstandards/phpcsextra": "dev-develop", - "slevomat/coding-standard": "dev-phpcs4", + "slevomat/coding-standard": "^8.23.0", "squizlabs/php_codesniffer": "^4.0.0" }, "require-dev": { From c8c9e1f2cc574504d0150e17841b58429241f775 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 22 Sep 2025 13:01:30 +0200 Subject: [PATCH 4/8] Update phpcsstandards/phpcsextra version requirement --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c2032dd..ee726c9 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ ], "require": { "php": ">=8.1", - "phpcsstandards/phpcsextra": "dev-develop", + "phpcsstandards/phpcsextra": "^1.4.1", "slevomat/coding-standard": "^8.23.0", "squizlabs/php_codesniffer": "^4.0.0" }, From 1569cbfb9fbebae1693ab78da09a26a7dca0e10e Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 22 Sep 2025 13:12:20 +0200 Subject: [PATCH 5/8] Fix FQCN false positive issue. --- PhpCollective/Sniffs/Commenting/AttributesSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpCollective/Sniffs/Commenting/AttributesSniff.php b/PhpCollective/Sniffs/Commenting/AttributesSniff.php index a9e95fb..74c0ebb 100644 --- a/PhpCollective/Sniffs/Commenting/AttributesSniff.php +++ b/PhpCollective/Sniffs/Commenting/AttributesSniff.php @@ -38,7 +38,7 @@ public function process(File $phpCsFile, $stackPointer): void $tokens = $phpCsFile->getTokens(); - if ($tokens[$nextIndex]['code'] === T_NS_SEPARATOR) { + if ($tokens[$nextIndex]['code'] === T_NS_SEPARATOR || $tokens[$nextIndex]['code'] === T_NAME_FULLY_QUALIFIED) { return; } From c5521561fc239568ff11430c21c5ef594a16f35f Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 22 Sep 2025 22:30:21 +0200 Subject: [PATCH 6/8] Fix array bracket issue with comments. --- .../Arrays/ArrayBracketSpacingSniff.php | 37 +++++++++++++-- .../Arrays/ArrayBracketSpacingSniffTest.php | 3 +- .../Sniffs/Commenting/AttributesSniffTest.php | 22 +++++++++ tests/_data/ArrayBracketSpacing/after.php | 9 ++++ tests/_data/ArrayBracketSpacing/before.php | 9 ++++ tests/_data/Attributes/after.php | 47 +++++++++++++++++++ tests/_data/Attributes/before.php | 47 +++++++++++++++++++ 7 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 tests/PhpCollective/Sniffs/Commenting/AttributesSniffTest.php create mode 100644 tests/_data/Attributes/after.php create mode 100644 tests/_data/Attributes/before.php diff --git a/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniff.php b/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniff.php index 46bf2b4..e7f77d3 100644 --- a/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniff.php +++ b/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniff.php @@ -70,6 +70,15 @@ public function process(File $phpcsFile, $stackPtr): void // Any extra blank lines should be removed if ($closerLine - $lastContentLine > 1) { $error = 'Extra blank lines found before array closing bracket'; + + // Check if the last content is a comment - these are problematic for auto-fixing + if ($tokens[$lastContentPtr]['code'] === T_COMMENT) { + // Report as non-fixable error when last element is a comment + $phpcsFile->addError($error, $closerPtr, 'ExtraBlankLineBeforeCloser'); + + return; + } + $fix = $phpcsFile->addFixableError($error, $closerPtr, 'ExtraBlankLineBeforeCloser'); if ($fix === true) { @@ -90,13 +99,33 @@ public function process(File $phpcsFile, $stackPtr): void } } - // Remove all tokens between last content and closer + // Find whitespace tokens between last content and closer + $whitespaceTokens = []; for ($i = $lastContentPtr + 1; $i < $closerPtr; $i++) { - $phpcsFile->fixer->replaceToken($i, ''); + if ($tokens[$i]['code'] === T_WHITESPACE) { + $whitespaceTokens[] = $i; + } } - // Add a single newline with proper indentation after the last content - $phpcsFile->fixer->addContent($lastContentPtr, "\n" . $indent); + // Find the position to insert the newline + // If there's already whitespace, replace it; otherwise add new + $nextToken = $lastContentPtr + 1; + + if ($nextToken < $closerPtr && $tokens[$nextToken]['code'] === T_WHITESPACE) { + // There's whitespace right after the last content + // Replace it with a single newline and indent + $phpcsFile->fixer->replaceToken($nextToken, "\n" . $indent); + + // Remove any additional whitespace tokens + for ($i = $nextToken + 1; $i < $closerPtr; $i++) { + if ($tokens[$i]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + } else { + // No whitespace immediately after, need to add it + $phpcsFile->fixer->addContent($lastContentPtr, "\n" . $indent); + } $phpcsFile->fixer->endChangeset(); } diff --git a/tests/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniffTest.php b/tests/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniffTest.php index 0ce2750..403a37c 100644 --- a/tests/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniffTest.php +++ b/tests/PhpCollective/Sniffs/Arrays/ArrayBracketSpacingSniffTest.php @@ -17,7 +17,8 @@ class ArrayBracketSpacingSniffTest extends TestCase */ public function testArrayBracketSpacingSniffer(): void { - $this->assertSnifferFindsErrors(new ArrayBracketSpacingSniff(), 4); + // 4 original errors + 1 new error from array with comments + $this->assertSnifferFindsErrors(new ArrayBracketSpacingSniff(), 5); } /** diff --git a/tests/PhpCollective/Sniffs/Commenting/AttributesSniffTest.php b/tests/PhpCollective/Sniffs/Commenting/AttributesSniffTest.php new file mode 100644 index 0000000..2ac16e8 --- /dev/null +++ b/tests/PhpCollective/Sniffs/Commenting/AttributesSniffTest.php @@ -0,0 +1,22 @@ +assertSnifferFindsErrors(new AttributesSniff(), 4); + } +} diff --git a/tests/_data/ArrayBracketSpacing/after.php b/tests/_data/ArrayBracketSpacing/after.php index 37ac3ad..9c85702 100644 --- a/tests/_data/ArrayBracketSpacing/after.php +++ b/tests/_data/ArrayBracketSpacing/after.php @@ -43,5 +43,14 @@ public function test(): void 'item1', 'item2', ]; + + // Array with comment as last element followed by blank lines + // This should report an error but NOT be auto-fixable + $array7 = [ + 'key' => 'value', + //'conditions' => array('ConversationUsers.status <'=>ConversationUser::STATUS_REMOVED), + //'group' => array('ConversationUser.conversation_id HAVING SUM(...)'), //HAVING COUNT(*) = '.count($users).' + + ]; } } diff --git a/tests/_data/ArrayBracketSpacing/before.php b/tests/_data/ArrayBracketSpacing/before.php index ff7bb47..d21c43d 100644 --- a/tests/_data/ArrayBracketSpacing/before.php +++ b/tests/_data/ArrayBracketSpacing/before.php @@ -48,5 +48,14 @@ public function test(): void 'item2', ]; + + // Array with comment as last element followed by blank lines + // This should report an error but NOT be auto-fixable + $array7 = [ + 'key' => 'value', + //'conditions' => array('ConversationUsers.status <'=>ConversationUser::STATUS_REMOVED), + //'group' => array('ConversationUser.conversation_id HAVING SUM(...)'), //HAVING COUNT(*) = '.count($users).' + + ]; } } diff --git a/tests/_data/Attributes/after.php b/tests/_data/Attributes/after.php new file mode 100644 index 0000000..2a1624c --- /dev/null +++ b/tests/_data/Attributes/after.php @@ -0,0 +1,47 @@ + Date: Tue, 23 Sep 2025 01:20:10 +0200 Subject: [PATCH 7/8] Fix indentation issue. --- .../DisallowArrayTypeHintSyntaxSniff.php | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php b/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php index 55434ce..b402bb1 100644 --- a/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php +++ b/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php @@ -24,7 +24,6 @@ use SlevomatCodingStandard\Helpers\AnnotationHelper; use SlevomatCodingStandard\Helpers\AnnotationTypeHelper; use SlevomatCodingStandard\Helpers\DocCommentHelper; -use SlevomatCodingStandard\Helpers\FixerHelper; use SlevomatCodingStandard\Helpers\FunctionHelper; use SlevomatCodingStandard\Helpers\NamespaceHelper; use SlevomatCodingStandard\Helpers\SniffSettingsHelper; @@ -143,23 +142,46 @@ public function process(File $phpcsFile, $pointer): void new IdentifierTypeNode($genericIdentifier), [$this->fixArrayNode($arrayTypeNode->type)], ); - $this->fixAnnotation($phpcsFile, $annotation, $genericTypeNode); + $this->fixAnnotation($phpcsFile, $annotation, AnnotationTypeHelper::print($genericTypeNode)); continue; } - $phpcsFile->fixer->beginChangeset(); - FixerHelper::change( + $this->applyFixWithoutTabConversion( $phpcsFile, $parsedDocComment->getOpenPointer(), $parsedDocComment->getClosePointer(), $fixedDocComment, ); - $phpcsFile->fixer->endChangeset(); } } } + /** + * Apply a fix without converting spaces to tabs + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $startPointer + * @param int $endPointer + * @param string $content + * + * @return void + */ + protected function applyFixWithoutTabConversion(File $phpcsFile, int $startPointer, int $endPointer, string $content): void + { + $phpcsFile->fixer->beginChangeset(); + + // Remove all tokens between start and end + for ($i = $startPointer; $i <= $endPointer; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // Add the new content without tab conversion + $phpcsFile->fixer->replaceToken($startPointer, $content); + + $phpcsFile->fixer->endChangeset(); + } + /** * @param \PHP_CodeSniffer\Files\File $phpcsFile * @param \SlevomatCodingStandard\Helpers\Annotation $annotation From 9161f22ff501fc96a9e2f050427acdd7f5144854 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Tue, 30 Sep 2025 01:20:56 +0200 Subject: [PATCH 8/8] Change minimum stability from 'dev' to 'stable' --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ee726c9..0c7bb3a 100644 --- a/composer.json +++ b/composer.json @@ -73,6 +73,6 @@ "dealerdirect/phpcodesniffer-composer-installer": true } }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true }