diff --git a/PHPCSUtils/Tokens/Collections.php b/PHPCSUtils/Tokens/Collections.php index e5eda198..0558499e 100644 --- a/PHPCSUtils/Tokens/Collections.php +++ b/PHPCSUtils/Tokens/Collections.php @@ -24,6 +24,22 @@ class Collections { + /** + * Control structures which can use the alternative control structure syntax. + * + * @var array => + */ + public static $alternativeControlStructureSyntaxTokens = [ + \T_IF => \T_IF, + \T_ELSEIF => \T_ELSEIF, + \T_ELSE => \T_ELSE, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_SWITCH => \T_SWITCH, + \T_WHILE => \T_WHILE, + \T_DECLARE => \T_DECLARE, + ]; + /** * Alternative control structure syntax closer keyword tokens. * @@ -109,6 +125,23 @@ class Collections \T_CLOSURE => \T_CLOSURE, ]; + /** + * Control structure tokens. + * + * @var array => + */ + public static $controlStructureTokens = [ + \T_IF => \T_IF, + \T_ELSEIF => \T_ELSEIF, + \T_ELSE => \T_ELSE, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_SWITCH => \T_SWITCH, + \T_DO => \T_DO, + \T_WHILE => \T_WHILE, + \T_DECLARE => \T_DECLARE, + ]; + /** * Tokens which are used to create lists. * diff --git a/PHPCSUtils/Utils/ControlStructures.php b/PHPCSUtils/Utils/ControlStructures.php new file mode 100644 index 00000000..af3c2dfa --- /dev/null +++ b/PHPCSUtils/Utils/ControlStructures.php @@ -0,0 +1,338 @@ +getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false + || isset(Collections::$controlStructureTokens[$tokens[$stackPtr]['code']]) === false + ) { + return false; + } + + // Handle `else if`. + if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === false) { + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next !== false && $tokens[$next]['code'] === \T_IF) { + $stackPtr = $next; + } + } + + // Deal with declare alternative syntax without scope opener. + if ($tokens[$stackPtr]['code'] === \T_DECLARE && isset($tokens[$stackPtr]['scope_opener']) === false) { + $declareOpenClose = self::getDeclareScopeOpenClose($phpcsFile, $stackPtr); + if ($declareOpenClose !== false) { + // Set the opener + closer in the tokens array. This will only affect our local copy. + $tokens[$stackPtr]['scope_opener'] = $declareOpenClose['opener']; + $tokens[$stackPtr]['scope_closer'] = $declareOpenClose['closer']; + } + } + + /* + * The scope markers are set. This is the simplest situation. + */ + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + if ($allowEmpty === true) { + return true; + } + + // Check whether the body is empty. + $start = ($tokens[$stackPtr]['scope_opener'] + 1); + $end = ($phpcsFile->numTokens + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $end = $tokens[$stackPtr]['scope_closer']; + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $end, true); + if ($nextNonEmpty !== false) { + return true; + } + + return false; + } + + /* + * Control structure without scope markers. + * Either single line statement or inline control structure. + * + * - Single line statement doesn't have a body and is therefore always empty. + * - Inline control structure has to have a body and can never be empty. + * + * This code also needs to take live coding into account where a scope opener is found, but + * no scope closer. + */ + $searchStart = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { + $searchStart = ($tokens[$stackPtr]['parenthesis_closer'] + 1); + } + + $nextNonEmpty = $phpcsFile->findNext( + Tokens::$emptyTokens, + $searchStart, + null, + true + ); + if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] === \T_SEMICOLON) { + // Parse error or single line statement. + return false; + } + + if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_CURLY_BRACKET) { + if ($allowEmpty === true) { + return true; + } + + // Unrecognized scope opener due to parse error. + $nextNext = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($nextNonEmpty + 1), + null, + true + ); + + if ($nextNext === false) { + return false; + } + + return true; + } + + return true; + } + + /** + * Check whether an IF or ELSE token is part of an `else if`. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * + * @return bool + */ + public static function isElseIf(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_ELSEIF) { + return true; + } + + if ($tokens[$stackPtr]['code'] !== \T_ELSE && $tokens[$stackPtr]['code'] !== \T_IF) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === true) { + return false; + } + + switch ($tokens[$stackPtr]['code']) { + case \T_ELSE: + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next !== false && $tokens[$next]['code'] === \T_IF) { + return true; + } + break; + + case \T_IF: + $previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($previous !== false && $tokens[$previous]['code'] === \T_ELSE) { + return true; + } + break; + } + + return false; + } + + /** + * Get the scope opener and closer for a `declare` statement. + * + * A `declare` statement can be: + * - applied to the rest of the file, like `declare(ticks=1);`; + * - applied to a limited scope using curly braces; + * - applied to a limited scope using the alternative control structure syntax. + * + * In the first case, the statement - correctly - won't have a scope opener/closer. + * In the second case, the statement will have the scope opener/closer indexes. + * In the last case, due to a bug in the PHPCS Tokenizer, it won't have the scope opener/closer indexes, + * while it really should. This bug is expected to be fixed in PHPCS 3.5.4. + * + * In other words, if a sniff needs to support PHPCS < 3.5.4 and needs to take the alternative + * control structure syntax into account, this method can be used to retrieve the + * scope opener/closer for the declare statement. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/pull/2843 + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * + * @return array|false Array with two keys `opener`, `closer` or false if + * not a `declare` token or if the opener/closer + * could not be determined. + */ + public static function getDeclareScopeOpenClose(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || $tokens[$stackPtr]['code'] !== \T_DECLARE + ) { + return false; + } + + if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === true) { + return [ + 'opener' => $tokens[$stackPtr]['scope_opener'], + 'closer' => $tokens[$stackPtr]['scope_closer'], + ]; + } + + $declareCount = 0; + $opener = null; + $closer = null; + + for ($i = $stackPtr; $i < $phpcsFile->numTokens; $i++) { + if ($tokens[$i]['code'] !== \T_DECLARE && $tokens[$i]['code'] !== \T_ENDDECLARE) { + continue; + } + + if ($tokens[$i]['code'] === \T_ENDDECLARE) { + --$declareCount; + + if ($declareCount !== 0) { + continue; + } + + // OK, we reached the target enddeclare. + $closer = $i; + break; + } + + if ($tokens[$i]['code'] === \T_DECLARE) { + ++$declareCount; + + // Find the scope opener + if (isset($tokens[$i]['parenthesis_closer']) === false) { + // Parse error or live coding, nothing to do. + return false; + } + + $scopeOpener = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($tokens[$i]['parenthesis_closer'] + 1), + null, + true + ); + + if ($scopeOpener === false) { + // Live coding, nothing to do. + return false; + } + + // Remember the scope opener for our target declare. + if ($declareCount === 1) { + $opener = $scopeOpener; + } + + $i = $scopeOpener; + + switch ($tokens[$scopeOpener]['code']) { + case \T_COLON: + // Nothing particular to do. Just continue the loop. + break; + + case \T_OPEN_CURLY_BRACKET: + /* + * Live coding or nested declare statement with curlies. + */ + + if (isset($tokens[$scopeOpener]['scope_closer']) === false) { + // Live coding, nothing to do. + return false; + } + + // Jump over the statement. + $i = $tokens[$scopeOpener]['scope_closer']; + --$declareCount; + + break; + + case \T_SEMICOLON: + // Nested single line declare statement. + --$declareCount; + break; + + default: + // This is an unexpected token. Most likely a parse error. Bow out. + return false; + } + } + + if ($declareCount === 0) { + break; + } + } + + if (isset($opener, $closer)) { + return [ + 'opener' => $opener, + 'closer' => $closer, + ]; + } + + return false; + } +} diff --git a/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError1Test.inc b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError1Test.inc new file mode 100644 index 00000000..2578c24a --- /dev/null +++ b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError1Test.inc @@ -0,0 +1,5 @@ +getTargetToken('/* testNoCloseParenthesis */', \T_DECLARE); + $result = ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + } +} diff --git a/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError2Test.inc b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError2Test.inc new file mode 100644 index 00000000..275a9989 --- /dev/null +++ b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError2Test.inc @@ -0,0 +1,5 @@ +getTargetToken('/* testNoScopeOpener */', \T_DECLARE); + $result = ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + } +} diff --git a/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError3Test.inc b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError3Test.inc new file mode 100644 index 00000000..29348ff8 --- /dev/null +++ b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError3Test.inc @@ -0,0 +1,5 @@ +getTargetToken('/* testNoScopeCloser */', \T_DECLARE); + $result = ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + } +} diff --git a/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError4Test.inc b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError4Test.inc new file mode 100644 index 00000000..b05cccfa --- /dev/null +++ b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseParseError4Test.inc @@ -0,0 +1,5 @@ +getTargetToken('/* testUnexpectedToken */', \T_DECLARE); + $result = ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + } +} diff --git a/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseTest.inc b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseTest.inc new file mode 100644 index 00000000..964e9b21 --- /dev/null +++ b/Tests/Utils/ControlStructures/GetDeclareScopeOpenCloseTest.inc @@ -0,0 +1,53 @@ +assertFalse(ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, 10000)); + } + + /** + * Test that false is returned when a token other than `T_DECLARE` is passed. + * + * @return void + */ + public function testNotDeclare() + { + $target = $this->getTargetToken('/* testNotDeclare */', \T_ECHO); + $this->assertFalse(ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, $target)); + } + + /** + * Test retrieving the scope open/close tokens for a `declare` statement. + * + * @dataProvider dataGetDeclareScopeOpenClose + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array|false $expected The expected return value. + * + * @return void + */ + public function testGetDeclareScopeOpenClose($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_DECLARE); + + // Translate offsets to absolute token positions. + if (isset($expected['opener'], $expected['closer']) === true) { + $expected['opener'] += $stackPtr; + $expected['closer'] += $stackPtr; + } + + $result = ControlStructures::getDeclareScopeOpenClose(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetDeclareScopeOpenClose() For the array format. + * + * @return array + */ + public function dataGetDeclareScopeOpenClose() + { + return [ + 'file-scope' => [ + '/* testFileScope */', + false, + ], + + 'curlies' => [ + '/* testCurlies */', + [ + 'opener' => 7, + 'closer' => 11, + ], + ], + 'nested-curlies-outside' => [ + '/* testNestedCurliesOutside */', + [ + 'opener' => 7, + 'closer' => 32, + ], + ], + 'nested-curlies-inside' => [ + '/* testNestedCurliesInside */', + [ + 'opener' => 12, + 'closer' => 17, + ], + ], + + 'alternative-syntax' => [ + '/* testAlternativeSyntax */', + [ + 'opener' => 7, + 'closer' => 11, + ], + ], + 'alternative-syntax-nested-level-1' => [ + '/* testAlternativeSyntaxNestedLevel1 */', + [ + 'opener' => 7, + 'closer' => 50, + ], + ], + 'alternative-syntax-nested-level-2' => [ + '/* testAlternativeSyntaxNestedLevel2 */', + [ + 'opener' => 12, + 'closer' => 34, + ], + ], + 'alternative-syntax-nested-level-3' => [ + '/* testAlternativeSyntaxNestedLevel3 */', + [ + 'opener' => 7, + 'closer' => 12, + ], + ], + + 'mixed-nested-level-1' => [ + '/* testMixedNestedLevel1 */', + [ + 'opener' => 7, + 'closer' => 61, + ], + ], + 'mixed-nested-level-2' => [ + '/* testMixedNestedLevel2 */', + [ + 'opener' => 12, + 'closer' => 46, + ], + ], + 'mixed-nested-level-3' => [ + '/* testMixedNestedLevel3 */', + [ + 'opener' => 7, + 'closer' => 24, + ], + ], + 'mixed-nested-level-4' => [ + '/* testMixedNestedLevel4 */', + false, + ], + + 'live-coding' => [ + '/* testLiveCoding */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/ControlStructures/HasBodyParseError1Test.inc b/Tests/Utils/ControlStructures/HasBodyParseError1Test.inc new file mode 100644 index 00000000..7eb8917c --- /dev/null +++ b/Tests/Utils/ControlStructures/HasBodyParseError1Test.inc @@ -0,0 +1,9 @@ +getTargetToken('/* testLiveCoding */', \T_ELSE); + + $result = ControlStructures::hasBody(self::$phpcsFile, $stackPtr); + $this->assertTrue($result, 'Failed hasBody check with $allowEmpty = true'); + + $result = ControlStructures::hasBody(self::$phpcsFile, $stackPtr, false); + $this->assertFalse($result, 'Failed hasBody check with $allowEmpty = false'); + } +} diff --git a/Tests/Utils/ControlStructures/HasBodyParseError2Test.inc b/Tests/Utils/ControlStructures/HasBodyParseError2Test.inc new file mode 100644 index 00000000..eba91210 --- /dev/null +++ b/Tests/Utils/ControlStructures/HasBodyParseError2Test.inc @@ -0,0 +1,9 @@ +getTargetToken('/* testLiveCoding */', \T_ELSE); + + $result = ControlStructures::hasBody(self::$phpcsFile, $stackPtr); + $this->assertTrue($result, 'Failed hasBody check with $allowEmpty = true'); + + $result = ControlStructures::hasBody(self::$phpcsFile, $stackPtr, false); + $this->assertTrue($result, 'Failed hasBody check with $allowEmpty = false'); + } +} diff --git a/Tests/Utils/ControlStructures/HasBodyTest.inc b/Tests/Utils/ControlStructures/HasBodyTest.inc new file mode 100644 index 00000000..e0363bb0 --- /dev/null +++ b/Tests/Utils/ControlStructures/HasBodyTest.inc @@ -0,0 +1,226 @@ + $c); // Surprisingly enough not a parse error. + +/* testForEachEmptyBody */ +foreach ($a as $k => $v) { + // phpcs:ignore Stnd.Cat.Sniff -- for reasons. +} + +/* testForEachWithCode */ +foreach ($a as $k => $v) { + echo "Key: $k; Current value of \$a: $v.\n"; +} + +/* testWhileWithoutBody */ +while (++$i <= 10) /*comment*/ ; + +/* testWhileEmptyBody */ +while (++$i <= 10) {} + +/* testWhileWithCode */ +while (++$i <= 10) { + echo $i; +} + +/* testDoWhileEmptyBody */ +do { +} while (++$i <= 10); + +/* testDoWhileWithCode */ +do { + echo $i; +} while (++$i <= 10); + +/* testSwitchWithoutBody */ +// Intentional parse error. +switch ($foo); + +/* testSwitchEmptyBody */ +switch ($foo) { + // Kind of useless, but not a parse error. +} + +/* testSwitchWithCode */ +switch ($foo) { + case 1: + echo '
something
'; + break; +} + +/* testDeclareWithoutBody */ +declare(ticks=1); + +/* testDeclareEmptyBody */ +declare(ticks=1) { + // Comment. +} + +/* testDeclareWithCode */ +declare(ticks=1) { + echo 'ticking'; +} + +/* + * Alternative control structure syntax. + */ + +/* testAlternativeIfEmptyBody */ +if (true): + // Code. + +/* testAlternativeElseIfWithCode */ +elseif (false): + echo 123; + +/* testAlternativeElseWithCode */ +else: + echo 123; +endif; + +/* testAlternativeForEmptyBody */ +for ($i = 1; $i <= 10; $i++) : + +endfor; + +/* testAlternativeForWithCode */ +for ($i = 1; $i <= 10; $i++) : + echo $i; +endfor; + +/* testAlternativeForeachEmptyBody */ +foreach ($a as $k => $v): + // Comment. +endforeach; + +/* testAlternativeForeachWithCode */ +foreach ($a as $k => $v): + echo "Key: $k; Current value of \$a: $v.\n"; +endforeach; + +/* testAlternativeWhileEmptyBody */ +while (++$i <= 10): + // phpcs:disable Stnd.Cat.Sniff -- for reasons. +endwhile; + +/* testAlternativeWhileWithCode */ +while (++$i <= 10): + echo $i; +endwhile; + +/* testAlternativeSwitchEmptyBody */ +switch ($foo) : +endswitch; + +/* testAlternativeSwitchWithCode */ +switch ($foo) : + case 1: + echo '
something
'; +endswitch; + +/* testAlternativeDeclareEmptyBody */ +declare (ticks = 1): + // comment +enddeclare; + +/* testAlternativeDeclareWithCode */ +declare (ticks = 1): + echo 'ticking'; +enddeclare; + +/* + * Control structures without braces. + * Without a body, these are a parse error straight away. + */ + +/* testInlineIfWithCode */ +if (true) + function_call($a); + +/* testInlineElseIfWithCode */ +elseif (false) + function_call($a); + +/* testInlineElseWithCode */ +else + function_call($a); + +/* testInlineForWithCode */ +for ($i = 1; $i <= 10; $i++) + echo $i; + +/* testInlineForEachWithCode */ +foreach ($a as $k => $v) + echo "Key: $k; Current value of \$a: $v.\n"; + +/* testInlineWhileWithCode */ +while (++$i <= 10) + echo $i; + +/* testInlineDoWhileWithCode */ +do + echo $i; +while (++$i <= 10); + +// Live coding. +// Intentional parse error. This test has to be the last in the file. + if ($a) { + // Code. + /* testElseLiveCoding */ + } else { + // Code. diff --git a/Tests/Utils/ControlStructures/HasBodyTest.php b/Tests/Utils/ControlStructures/HasBodyTest.php new file mode 100644 index 00000000..59c473ef --- /dev/null +++ b/Tests/Utils/ControlStructures/HasBodyTest.php @@ -0,0 +1,321 @@ +assertFalse(ControlStructures::hasBody(self::$phpcsFile, 10000)); + } + + /** + * Test that false is returned when a non-control structure token is passed. + * + * @return void + */ + public function testNotControlStructure() + { + $target = $this->getTargetToken('/* testNotControlStructure */', \T_ECHO); + $this->assertFalse(ControlStructures::hasBody(self::$phpcsFile, $target)); + } + + /** + * Test whether the function correctly identifies whether a control structure has a body. + * + * @dataProvider dataHasBody + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $hasBody The expected boolean return value when the function is called + * with `$allowEmpty = true`. + * @param bool $hasNonEmptyBody The expected boolean return value when the function is called + * with `$allowEmpty = false`. + * + * @return void + */ + public function testHasBody($testMarker, $hasBody, $hasNonEmptyBody) + { + $stackPtr = $this->getTargetToken($testMarker, Collections::$controlStructureTokens); + + $result = ControlStructures::hasBody(self::$phpcsFile, $stackPtr); + $this->assertSame($hasBody, $result, 'Failed hasBody check with $allowEmpty = true'); + + $result = ControlStructures::hasBody(self::$phpcsFile, $stackPtr, false); + $this->assertSame($hasNonEmptyBody, $result, 'Failed hasBody check with $allowEmpty = false'); + } + + /** + * Data provider. + * + * @see testHasBody() For the array format. + * + * @return array + */ + public function dataHasBody() + { + return [ + 'if-without-body' => [ + '/* testIfWithoutBody */', + false, + false, + ], + 'if-empty-body' => [ + '/* testIfEmptyBody */', + true, + false, + ], + 'elseif-empty-body' => [ + '/* testElseIfEmptyBody */', + true, + false, + ], + 'else-if-empty-body' => [ + '/* testElseSpaceIfEmptyBody */', + true, + false, + ], + 'else-empty-body' => [ + '/* testElseEmptyBody */', + true, + false, + ], + 'if-with-code' => [ + '/* testIfWithCode */', + true, + true, + ], + 'elseif-with-code' => [ + '/* testElseIfWithCode */', + true, + true, + ], + 'else-if-with-code' => [ + '/* testElseSpaceIfWithCode */', + true, + true, + ], + 'else-with-code' => [ + '/* testElseWithCode */', + true, + true, + ], + 'for-without-body' => [ + '/* testForWithoutBody */', + false, + false, + ], + 'for-empty-body' => [ + '/* testForEmptyBody */', + true, + false, + ], + 'for-with-code' => [ + '/* testForWithCode */', + true, + true, + ], + 'foreach-without-body' => [ + '/* testForEachWithoutBody */', + false, + false, + ], + 'foreach-empty-body' => [ + '/* testForEachEmptyBody */', + true, + false, + ], + 'foreach-with-code' => [ + '/* testForEachWithCode */', + true, + true, + ], + 'while-without-body' => [ + '/* testWhileWithoutBody */', + false, + false, + ], + 'while-empty-body' => [ + '/* testWhileEmptyBody */', + true, + false, + ], + 'while-with-code' => [ + '/* testWhileWithCode */', + true, + true, + ], + 'do-while-empty-body' => [ + '/* testDoWhileEmptyBody */', + true, + false, + ], + 'do-while-with-code' => [ + '/* testDoWhileWithCode */', + true, + true, + ], + 'switch-without-body' => [ + '/* testSwitchWithoutBody */', + false, + false, + ], + 'switch-empty-body' => [ + '/* testSwitchEmptyBody */', + true, + false, + ], + 'switch-with-code' => [ + '/* testSwitchWithCode */', + true, + true, + ], + 'declare-without-body' => [ + '/* testDeclareWithoutBody */', + false, + false, + ], + 'declare-empty-body' => [ + '/* testDeclareEmptyBody */', + true, + false, + ], + 'declare-with-code' => [ + '/* testDeclareWithCode */', + true, + true, + ], + 'alternative-syntax-if-empty-body' => [ + '/* testAlternativeIfEmptyBody */', + true, + false, + ], + 'alternative-syntax-elseif-with-code' => [ + '/* testAlternativeElseIfWithCode */', + true, + true, + ], + 'alternative-syntax-else-with-code' => [ + '/* testAlternativeElseWithCode */', + true, + true, + ], + 'alternative-syntax-for-empty-body' => [ + '/* testAlternativeForEmptyBody */', + true, + false, + ], + 'alternative-syntax-for-with-code' => [ + '/* testAlternativeForWithCode */', + true, + true, + ], + 'alternative-syntax-foreach-empty-body' => [ + '/* testAlternativeForeachEmptyBody */', + true, + false, + ], + 'alternative-syntax-foreach-with-code' => [ + '/* testAlternativeForeachWithCode */', + true, + true, + ], + 'alternative-syntax-while-empty-body' => [ + '/* testAlternativeWhileEmptyBody */', + true, + false, + ], + 'alternative-syntax-while-with-code' => [ + '/* testAlternativeWhileWithCode */', + true, + true, + ], + 'alternative-syntax-switch-empty-body' => [ + '/* testAlternativeSwitchEmptyBody */', + true, + false, + ], + 'alternative-syntax-switch-with-code' => [ + '/* testAlternativeSwitchWithCode */', + true, + true, + ], + 'alternative-syntax-declare-empty-body' => [ + '/* testAlternativeDeclareEmptyBody */', + true, + false, + ], + 'alternative-syntax-declare-with-code' => [ + '/* testAlternativeDeclareWithCode */', + true, + true, + ], + 'inline-if-with-code' => [ + '/* testInlineIfWithCode */', + true, + true, + ], + 'inline-elseif-with-code' => [ + '/* testInlineElseIfWithCode */', + true, + true, + ], + 'inline-else-with-code' => [ + '/* testInlineElseWithCode */', + true, + true, + ], + 'inline-for-with-code' => [ + '/* testInlineForWithCode */', + true, + true, + ], + 'inline-foreach-with-code' => [ + '/* testInlineForEachWithCode */', + true, + true, + ], + 'inline-while-with-code' => [ + '/* testInlineWhileWithCode */', + true, + true, + ], + 'inline-do-while-with-code' => [ + '/* testInlineDoWhileWithCode */', + true, + true, + ], + 'else-live-coding' => [ + '/* testElseLiveCoding */', + true, + false, + ], + ]; + } +} diff --git a/Tests/Utils/ControlStructures/IsElseIfTest.inc b/Tests/Utils/ControlStructures/IsElseIfTest.inc new file mode 100644 index 00000000..b7dd30ba --- /dev/null +++ b/Tests/Utils/ControlStructures/IsElseIfTest.inc @@ -0,0 +1,58 @@ +assertFalse(ControlStructures::isElseIf(self::$phpcsFile, 10000)); + } + + /** + * Test that false is returned when a token other than `T_IF`, `T_ELSE`, `T_ELSEIF` is passed. + * + * @return void + */ + public function testNotIfElseifOrElse() + { + $target = $this->getTargetToken('/* testNotIfElseifOrElse */', \T_ECHO); + $this->assertFalse(ControlStructures::isElseIf(self::$phpcsFile, $target)); + } + + /** + * Test whether a T_IF or T_ELSE token is correctly identified as either elseif or not. + * + * @dataProvider dataIsElseIf + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected boolean return value. + * + * @return void + */ + public function testIsElseIf($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, [\T_IF, \T_ELSEIF, \T_ELSE]); + $result = ControlStructures::isElseIf(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsElseIf() For the array format. + * + * @return array + */ + public function dataIsElseIf() + { + return [ + 'if' => [ + '/* testIf */', + false, + ], + 'elseif' => [ + '/* testElseIf */', + true, + ], + 'else-if' => [ + '/* testElseSpaceIf */', + true, + ], + 'else-if-with-comment-else' => [ + '/* testElseCommentIfElse */', + true, + ], + 'else-if-with-comment-if' => [ + '/* testElseCommentIfIf */', + true, + ], + 'else' => [ + '/* testElse */', + false, + ], + + 'alternative-syntax-if' => [ + '/* testAlternativeIf */', + false, + ], + 'alternative-syntax-elseif' => [ + '/* testAlternativeElseIf */', + true, + ], + 'alternative-syntax-else' => [ + '/* testAlternativeElse */', + false, + ], + + 'inline-if' => [ + '/* testAlternativeIf */', + false, + ], + 'inline-elseif' => [ + '/* testAlternativeElseIf */', + true, + ], + 'inline-else' => [ + '/* testAlternativeElse */', + false, + ], + + 'live-coding' => [ + '/* testLiveCoding */', + false, + ], + ]; + } +}