diff --git a/README.md b/README.md index 35b8ece0c..30d3a0c2d 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/PHPCSStandards - [SlevomatCodingStandard.ControlStructures.DisallowEmpty](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowempty) - [SlevomatCodingStandard.ControlStructures.DisallowNullSafeObjectOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallownullsafeobjectoperator) - [SlevomatCodingStandard.ControlStructures.DisallowShortTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowshortternaryoperator-) 🔧 + - [SlevomatCodingStandard.ControlStructures.DisallowTrailingCommaInMatchExpression](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowtrailingcommainmatchexpression-) 🔧 - [SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperatorSniff](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowtrailingmultilineternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.DisallowYodaComparison](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowyodacomparison-) 🔧 - [SlevomatCodingStandard.ControlStructures.EarlyExit](doc/control-structures.md#slevomatcodingstandardcontrolstructuresearlyexit-) 🔧 @@ -110,6 +111,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/PHPCSStandards - [SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequireshortternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireSingleLineCondition](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequiresinglelinecondition-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequireternaryoperator-) 🔧 + - [SlevomatCodingStandard.ControlStructures.RequireTrailingCommaInMatchExpression](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequiretrailingcommainmatchexpression-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireYodaComparison](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequireyodacomparison-) 🔧 - [SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn](doc/control-structures.md#slevomatcodingstandardcontrolstructuresuselessifconditionwithreturn-) 🔧 - [SlevomatCodingStandard.ControlStructures.UselessTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresuselessternaryoperator-) 🔧 diff --git a/SlevomatCodingStandard/Sniffs/ControlStructures/DisallowTrailingCommaInMatchExpressionSniff.php b/SlevomatCodingStandard/Sniffs/ControlStructures/DisallowTrailingCommaInMatchExpressionSniff.php new file mode 100644 index 000000000..c53f80f19 --- /dev/null +++ b/SlevomatCodingStandard/Sniffs/ControlStructures/DisallowTrailingCommaInMatchExpressionSniff.php @@ -0,0 +1,64 @@ + + */ + public function register(): array + { + return [ + T_MATCH, + ]; + } + + public function process(File $phpcsFile, int $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + + $scopeCloser = $tokens[$stackPtr]['scope_closer']; + $pointerBeforeScopeCloser = TokenHelper::findPreviousEffective($phpcsFile, $scopeCloser - 1); + + if ($tokens[$pointerBeforeScopeCloser]['code'] !== T_COMMA) { + return; + } + + if ($this->onlySingleLine && $tokens[$pointerBeforeScopeCloser]['line'] !== $tokens[$scopeCloser]['line']) { + return; + } + + $fix = $phpcsFile->addFixableError( + 'Trailing comma after the last branch of a match expression is disallowed.', + $pointerBeforeScopeCloser, + self::CODE_DISALLOWED_TRAILING_COMMA, + ); + + if (!$fix) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + FixerHelper::replace($phpcsFile, $pointerBeforeScopeCloser, ''); + + if ($tokens[$pointerBeforeScopeCloser]['line'] === $tokens[$scopeCloser]['line']) { + FixerHelper::removeBetween($phpcsFile, $pointerBeforeScopeCloser, $scopeCloser); + } + + $phpcsFile->fixer->endChangeset(); + } + +} diff --git a/SlevomatCodingStandard/Sniffs/ControlStructures/RequireTrailingCommaInMatchExpressionSniff.php b/SlevomatCodingStandard/Sniffs/ControlStructures/RequireTrailingCommaInMatchExpressionSniff.php new file mode 100644 index 000000000..7c621ce8f --- /dev/null +++ b/SlevomatCodingStandard/Sniffs/ControlStructures/RequireTrailingCommaInMatchExpressionSniff.php @@ -0,0 +1,64 @@ + + */ + public function register(): array + { + return [ + T_MATCH, + ]; + } + + public function process(File $phpcsFile, int $stackPtr): void + { + $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80800); + if (!$this->enable) { + return; + } + + $tokens = $phpcsFile->getTokens(); + $scopeCloser = $tokens[$stackPtr]['scope_closer']; + $pointerBeforeScopeCloser = TokenHelper::findPreviousEffective($phpcsFile, $scopeCloser - 1); + + if ($tokens[$scopeCloser]['line'] === $tokens[$pointerBeforeScopeCloser]['line']) { + return; + } + + if ($tokens[$pointerBeforeScopeCloser]['code'] === T_COMMA) { + return; + } + + $fix = $phpcsFile->addFixableError( + 'Multi-line match expressions must have a trailing comma after the last branch.', + $pointerBeforeScopeCloser, + self::CODE_MISSING_TRAILING_COMMA, + ); + + if (!$fix) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + FixerHelper::add($phpcsFile, $pointerBeforeScopeCloser, ','); + $phpcsFile->fixer->endChangeset(); + } + +} diff --git a/doc/control-structures.md b/doc/control-structures.md index ee6156c8f..79a25450c 100644 --- a/doc/control-structures.md +++ b/doc/control-structures.md @@ -126,6 +126,24 @@ $t = $someCondition : $otherwiseThis; ``` +#### SlevomatCodingStandard.ControlStructures.RequireTrailingCommaInMatchExpression 🔧 + +Commas after the last branch in match expression make adding a new parameter easier and result in a cleaner versioning diff. + +This sniff enforces trailing commas in multi-line match expressions. + +This sniff provides the following setting: + +* `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. + +#### SlevomatCodingStandard.ControlStructures.DisallowTrailingCommaInMatchExpression 🔧 + +This sniff disallows trailing commas on the last branch of a match expression. + +This sniff provides the following setting: + +* `onlySingleLine`: to enable checks only for single-line match expressions. + #### SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing 🔧 Enforces configurable number of lines around jump statements (continue, return, ...). diff --git a/tests/Sniffs/ControlStructures/DisallowTrailingCommaInMatchExpressionSniffTest.php b/tests/Sniffs/ControlStructures/DisallowTrailingCommaInMatchExpressionSniffTest.php new file mode 100644 index 000000000..6d5b880c1 --- /dev/null +++ b/tests/Sniffs/ControlStructures/DisallowTrailingCommaInMatchExpressionSniffTest.php @@ -0,0 +1,49 @@ + false, + ]); + + self::assertNoSniffErrorInFile($report); + } + + public function testErrors(): void + { + $report = self::checkFile(__DIR__ . '/data/disallowTrailingCommaInMatchExpressionErrors.php', [ + 'onlySingleLine' => false, + ]); + + self::assertSame(7, $report->getErrorCount()); + + self::assertSniffError($report, 3, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertSniffError($report, 7, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertSniffError($report, 14, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertSniffError($report, 15, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertSniffError($report, 22, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertSniffError($report, 32, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertSniffError($report, 33, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + + self::assertAllFixedInFile($report); + } + + public function testWithOnlySingleLineEnabledErrors(): void + { + $report = self::checkFile(__DIR__ . '/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.php', [ + 'onlySingleLine' => true, + ]); + + self::assertSame(1, $report->getErrorCount()); + self::assertSniffError($report, 3, DisallowTrailingCommaInMatchExpressionSniff::CODE_DISALLOWED_TRAILING_COMMA); + self::assertAllFixedInFile($report); + } + +} diff --git a/tests/Sniffs/ControlStructures/RequireTrailingCommaInMatchExpressionSniffTest.php b/tests/Sniffs/ControlStructures/RequireTrailingCommaInMatchExpressionSniffTest.php new file mode 100644 index 000000000..bdef0965b --- /dev/null +++ b/tests/Sniffs/ControlStructures/RequireTrailingCommaInMatchExpressionSniffTest.php @@ -0,0 +1,46 @@ + true, + ]); + + self::assertNoSniffErrorInFile($report); + } + + public function testErrors(): void + { + $report = self::checkFile(__DIR__ . '/data/requireTrailingCommaInMatchExpressionErrors.php', [ + 'enable' => true, + ]); + + self::assertSame(6, $report->getErrorCount()); + + self::assertSniffError($report, 5, RequireTrailingCommaInMatchExpressionSniff::CODE_MISSING_TRAILING_COMMA); + self::assertSniffError($report, 12, RequireTrailingCommaInMatchExpressionSniff::CODE_MISSING_TRAILING_COMMA); + self::assertSniffError($report, 13, RequireTrailingCommaInMatchExpressionSniff::CODE_MISSING_TRAILING_COMMA); + self::assertSniffError($report, 20, RequireTrailingCommaInMatchExpressionSniff::CODE_MISSING_TRAILING_COMMA); + self::assertSniffError($report, 30, RequireTrailingCommaInMatchExpressionSniff::CODE_MISSING_TRAILING_COMMA); + self::assertSniffError($report, 31, RequireTrailingCommaInMatchExpressionSniff::CODE_MISSING_TRAILING_COMMA); + + self::assertAllFixedInFile($report); + } + + public function testShouldNotReportIfSniffIsDisabled(): void + { + $report = self::checkFile(__DIR__ . '/data/requireTrailingCommaInMatchExpressionErrors.php', [ + 'enable' => false, + ]); + + self::assertNoSniffErrorInFile($report); + } + +} diff --git a/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionErrors.fixed.php b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionErrors.fixed.php new file mode 100644 index 000000000..35509290f --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionErrors.fixed.php @@ -0,0 +1,35 @@ += 8.0 + +$foo = match(rand(0, 1)) {0 => false, 1 => true}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => true +}; + +$baz = match (rand(0, 1)) { + 0 => false, + 1 => match ($foo) { + false => 'foobar', + true => 'foobaz' + } +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz' + } + }; +} diff --git a/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionErrors.php b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionErrors.php new file mode 100644 index 000000000..a52a46608 --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionErrors.php @@ -0,0 +1,35 @@ += 8.0 + +$foo = match(rand(0, 1)) {0 => false, 1 => true,}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => true, +}; + +$baz = match (rand(0, 1)) { + 0 => false, + 1 => match ($foo) { + false => 'foobar', + true => 'foobaz', + }, +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true, + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz', + }, + }; +} diff --git a/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionNoErrors.php b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionNoErrors.php new file mode 100644 index 000000000..997124591 --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionNoErrors.php @@ -0,0 +1,33 @@ += 8.0 + +$foo = match (rand(0, 1)) { + 0 => false, + 1 => true +}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => match($foo) { + false => 'foobar', + true => 'foobaz' + } +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz' + } + }; +} diff --git a/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.fixed.php b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.fixed.php new file mode 100644 index 000000000..a0d16bb7e --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.fixed.php @@ -0,0 +1,35 @@ += 8.0 + +$foo = match(rand(0, 1)) {0 => false, 1 => true}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => true, +}; + +$baz = match (rand(0, 1)) { + 0 => false, + 1 => match ($foo) { + false => 'foobar', + true => 'foobaz', + }, +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true, + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz', + }, + }; +} diff --git a/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.php b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.php new file mode 100644 index 000000000..a52a46608 --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/disallowTrailingCommaInMatchExpressionWithOnlySingleLineEnabledErrors.php @@ -0,0 +1,35 @@ += 8.0 + +$foo = match(rand(0, 1)) {0 => false, 1 => true,}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => true, +}; + +$baz = match (rand(0, 1)) { + 0 => false, + 1 => match ($foo) { + false => 'foobar', + true => 'foobaz', + }, +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true, + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz', + }, + }; +} diff --git a/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionErrors.fixed.php b/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionErrors.fixed.php new file mode 100644 index 000000000..943e2e7ab --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionErrors.fixed.php @@ -0,0 +1,33 @@ += 8.0 + +$foo = match (rand(0, 1)) { + 0 => false, + 1 => true, +}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => match($foo) { + false => 'foobar', + true => 'foobaz', + }, +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true, + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz', + }, + }; +} diff --git a/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionErrors.php b/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionErrors.php new file mode 100644 index 000000000..997124591 --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionErrors.php @@ -0,0 +1,33 @@ += 8.0 + +$foo = match (rand(0, 1)) { + 0 => false, + 1 => true +}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => match($foo) { + false => 'foobar', + true => 'foobaz' + } +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz' + } + }; +} diff --git a/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionNoErrors.php b/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionNoErrors.php new file mode 100644 index 000000000..a0d16bb7e --- /dev/null +++ b/tests/Sniffs/ControlStructures/data/requireTrailingCommaInMatchExpressionNoErrors.php @@ -0,0 +1,35 @@ += 8.0 + +$foo = match(rand(0, 1)) {0 => false, 1 => true}; + +$bar = match (rand(0, 1)) { + 0 => false, + 1 => true, +}; + +$baz = match (rand(0, 1)) { + 0 => false, + 1 => match ($foo) { + false => 'foobar', + true => 'foobaz', + }, +}; + +function foo(): bool +{ + return match (rand(0, 1)) { + 0 => false, + 1 => true, + }; +} + +function bar(): string +{ + return match (rand(0, 1)) { + 0 => false, + 1 => match (foo()) { + false => 'foobar', + true => 'foobaz', + }, + }; +}