From 15d212ba6c81b711296b8c95b42768529ebefbf5 Mon Sep 17 00:00:00 2001 From: Anastasiia Sherekina Date: Wed, 23 Jul 2025 18:17:46 +0300 Subject: [PATCH 1/2] Add many spaces sniff --- README.md | 3 +- src/PhpCs/FiveLab/ErrorCodes.php | 1 + .../Sniffs/Formatting/ManySpacesSniff.php | 94 ++++++++++++++++ src/PhpStan/ForbiddenNodeTypeRule.php | 2 +- .../Sniffs/Formatting/ManySpacesSniffTest.php | 103 ++++++++++++++++++ .../Resources/many-spaces/success.php | 41 +++++++ .../Resources/many-spaces/wrong.php | 20 ++++ 7 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 src/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniff.php create mode 100644 tests/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniffTest.php create mode 100644 tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/success.php create mode 100644 tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/wrong.php diff --git a/README.md b/README.md index e414913..42df8e4 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ Before create the PR or merge into develop, please run next commands for validat ```shell ./bin/phpunit +./bin/phpstan ./bin/phpcs --config-set show_warnings 0 -./bin/phpcs --standard=vendor/escapestudios/symfony2-coding-standard/Symfony/ src/ ./bin/phpcs --standard=tests/phpcs.xml tests/ +./bin/phpcs --standard=src/phpcs.xml src/ ``` diff --git a/src/PhpCs/FiveLab/ErrorCodes.php b/src/PhpCs/FiveLab/ErrorCodes.php index 9580076..12a51ef 100644 --- a/src/PhpCs/FiveLab/ErrorCodes.php +++ b/src/PhpCs/FiveLab/ErrorCodes.php @@ -34,4 +34,5 @@ final class ErrorCodes public const ARRAYS_DOC_VECTOR = 'ArraysDocVector'; public const PHPDOC_NOT_ALLOWED = 'PhpDocNotAllowed'; public const MISSED_SPACE = 'MissedSpace'; + public const MANY_SPACES = 'ManySpaces'; } diff --git a/src/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniff.php b/src/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniff.php new file mode 100644 index 0000000..9b6ea5b --- /dev/null +++ b/src/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniff.php @@ -0,0 +1,94 @@ +getTokens(); + $token = $tokens[$stackPtr]; + $content = $tokens[$stackPtr]['content']; + + // only spaces + if (!\preg_match('/^ +$/', $content)) { + return; + } + + $nextPtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + $prevPtr = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + + if (false === $prevPtr || \str_contains($tokens[$stackPtr - 1]['content'], \chr(10))) { + return; + } + + if (T_SEMICOLON !== $tokens[$nextPtr]['code'] && 1 === \strlen($token['content'])) { + return; + } + + if (T_SEMICOLON === $tokens[$nextPtr]['code']) { + $phpcsFile->addError( + 'Too many spaces. There must be no space before a semicolon.', + $stackPtr, + ErrorCodes::MANY_SPACES + ); + + return; + } + + if (T_DOUBLE_ARROW === $tokens[$nextPtr]['code']) { + return; + } + + if (T_MATCH_ARROW === $tokens[$nextPtr]['code']) { + return; + } + + if (T_CONST === $tokens[$stackPtr - 3]['code'] || T_CONST === $tokens[$stackPtr - 5]['code']) { + return; + } + + if (T_ENUM_CASE === $tokens[$stackPtr - 3]['code']) { + return; + } + + $functionPtr = $phpcsFile->findPrevious([T_FUNCTION, T_CLOSURE, T_FN], $stackPtr); + + if (\array_key_exists('parenthesis_opener', $tokens[$functionPtr]) && \array_key_exists('parenthesis_closer', $tokens[$functionPtr])) { + $open = $tokens[$functionPtr]['parenthesis_opener']; + $close = $tokens[$functionPtr]['parenthesis_closer']; + + if ($stackPtr > $open && $stackPtr < $close) { + return; + } + } + + $phpcsFile->addError( + 'Too many spaces. Must be one space.', + $stackPtr, + ErrorCodes::MANY_SPACES + ); + } +} diff --git a/src/PhpStan/ForbiddenNodeTypeRule.php b/src/PhpStan/ForbiddenNodeTypeRule.php index 8bbb074..3cf9eb1 100644 --- a/src/PhpStan/ForbiddenNodeTypeRule.php +++ b/src/PhpStan/ForbiddenNodeTypeRule.php @@ -38,7 +38,7 @@ class ForbiddenNodeTypeRule implements Rule */ public function __construct(string $nodeType, string $message = null) { - if (!\is_a($nodeType, Node::class, true)) { // @phpstan-ignore phpstanApi.runtimeReflection, function.alreadyNarrowedType + if (!\is_a($nodeType, Node::class, true)) { // @phpstan-ignore phpstanApi.runtimeReflection throw new \InvalidArgumentException(\sprintf( 'The node type class must implement "%s" interface but "%s" got given.', Node::class, diff --git a/tests/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniffTest.php b/tests/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniffTest.php new file mode 100644 index 0000000..7ae4887 --- /dev/null +++ b/tests/PhpCs/FiveLab/Sniffs/Formatting/ManySpacesSniffTest.php @@ -0,0 +1,103 @@ + [ + __DIR__.'/Resources/many-spaces/success.php', + ], + + 'wrong' => [ + __DIR__.'/Resources/many-spaces/wrong.php', + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 3, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 4, + ], + [ + 'message' => 'Too many spaces. There must be no space before a semicolon.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 5, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 7, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 8, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 9, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 10, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 11, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 12, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 13, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 14, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 17, + ], + [ + 'message' => 'Too many spaces. Must be one space.', + 'source' => 'FiveLab.Formatting.ManySpaces.ManySpaces', + 'line' => 20, + ], + ], + ]; + } +} diff --git a/tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/success.php b/tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/success.php new file mode 100644 index 0000000..467431d --- /dev/null +++ b/tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/success.php @@ -0,0 +1,41 @@ + $b) { +} + +$ar = [ + 'a' => 8, + 'bla' => 5, + 'fddfdf0' => 65, +]; + +return [ + 'success' => [], + 'bba' => 1, +]; + +$ds = match('dfdfdf0') { + 'ffd' => 'ffd', + 'fddddd' => 'ddd', +}; diff --git a/tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/wrong.php b/tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/wrong.php new file mode 100644 index 0000000..7184790 --- /dev/null +++ b/tests/PhpCs/FiveLab/Sniffs/Formatting/Resources/many-spaces/wrong.php @@ -0,0 +1,20 @@ + $b) {} +if ($a === $b) {} +if ($a > $b) {} +if ($a === $b) {} +if ($a && $b) {} +if ($a || $b) {} +if ($a && $b) {} +if ($a || $b) {} + +$arr = [ + $d => 1 +]; + +return $a; From 10f7c5a286af3c8755b098c24aa268e4a9c5e8cf Mon Sep 17 00:00:00 2001 From: Anastasiia Sherekina Date: Wed, 23 Jul 2025 18:20:44 +0300 Subject: [PATCH 2/2] Fix code style --- src/PhpStan/ForbiddenNodeTypeRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpStan/ForbiddenNodeTypeRule.php b/src/PhpStan/ForbiddenNodeTypeRule.php index 3cf9eb1..8bbb074 100644 --- a/src/PhpStan/ForbiddenNodeTypeRule.php +++ b/src/PhpStan/ForbiddenNodeTypeRule.php @@ -38,7 +38,7 @@ class ForbiddenNodeTypeRule implements Rule */ public function __construct(string $nodeType, string $message = null) { - if (!\is_a($nodeType, Node::class, true)) { // @phpstan-ignore phpstanApi.runtimeReflection + if (!\is_a($nodeType, Node::class, true)) { // @phpstan-ignore phpstanApi.runtimeReflection, function.alreadyNarrowedType throw new \InvalidArgumentException(\sprintf( 'The node type class must implement "%s" interface but "%s" got given.', Node::class,