From 3b4ee2340fa1587d487c16b6fa5cd6cada0c38ab Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 17 Oct 2016 03:31:11 +0200 Subject: [PATCH] Add new `ForbiddenBreakContinueOutsideLoop` sniff. Usage of a break/continue statement outside control structure loops or switch statements was never valid, however since PHP 7.0 it will throw a fatal error. --- ...ForbiddenBreakContinueOutsideLoopSniff.php | 111 +++++++++++++++ ...iddenBreakContinueOutsideLoopSniffTest.php | 114 +++++++++++++++ .../forbidden_break_continue_outside_loop.php | 131 ++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniff.php create mode 100644 Tests/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniffTest.php create mode 100644 Tests/sniff-examples/forbidden_break_continue_outside_loop.php diff --git a/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniff.php b/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniff.php new file mode 100644 index 000000000..7372aa590 --- /dev/null +++ b/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniff.php @@ -0,0 +1,111 @@ + + */ + +/** + * PHPCompatibility_Sniffs_PHP_ForbiddenBreakContinueOutsideLoop. + * + * Forbids use of break or continue statements outside of looping structures. + * + * PHP version 7 + * + * @category PHP + * @package PHPCompatibility + * @author Juliette Reinders Folmer + */ +class PHPCompatibility_Sniffs_PHP_ForbiddenBreakContinueOutsideLoopSniff extends PHPCompatibility_Sniff +{ + + /** + * Token codes of control structure in which usage of break/continue is valid. + * + * @var array + */ + protected $validLoopStructures = array( + T_FOR => true, + T_FOREACH => true, + T_WHILE => true, + T_DO => true, + T_SWITCH => true, + ); + + /** + * Token codes which did not correctly get a condition assigned in older PHPCS versions. + * + * @var array + */ + protected $backCompat = array( + T_CASE => true, + T_DEFAULT => true, + ); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return array( + T_BREAK, + T_CONTINUE, + ); + + }//end register() + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. + * + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $token = $tokens[$stackPtr]; + + // Check if the break/continue is within a valid loop structure. + if (empty($token['conditions']) === false) { + foreach ($token['conditions'] as $tokenCode) { + if (isset($this->validLoopStructures[$tokenCode]) === true) { + return; + } + } + } + else { + // Deal with older PHPCS versions. + if (isset($token['scope_condition']) === true && isset($this->backCompat[$tokens[$token['scope_condition']]['code']]) === true) { + return; + } + } + + // If we're still here, no valid loop structure container has been found, so throw an error. + $error = "Using '%s' outside of a loop or switch structure is invalid"; + $isError = false; + $data = array( + $token['content'], + ); + if ($this->supportsAbove('7.0')) { + $isError = true; + $error .= ' and will throw a fatal error since PHP 7.0'; + } + + if ($isError === true) { + $phpcsFile->addError($error, $stackPtr, 'Found', $data); + } else { + $phpcsFile->addWarning($error, $stackPtr, 'Found', $data); + } + + }//end process() + +}//end class diff --git a/Tests/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniffTest.php b/Tests/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniffTest.php new file mode 100644 index 000000000..2914238cd --- /dev/null +++ b/Tests/Sniffs/PHP/ForbiddenBreakContinueOutsideLoopSniffTest.php @@ -0,0 +1,114 @@ + + */ +class ForbiddenBreakContinueOutsideLoopSniffTest extends BaseSniffTest +{ + const TEST_FILE = 'sniff-examples/forbidden_break_continue_outside_loop.php'; + + /** + * testForbiddenBreakContinueOutsideLoop + * + * @group forbiddenBreakContinueOutsideLoop + * + * @dataProvider dataBreakContinueOutsideLoop + * + * @param int $line The line number. + * @param string $found Either 'break' or 'continue'. + * + * @return void + */ + public function testBreakContinueOutsideLoop($line, $found) + { + $file = $this->sniffFile(self::TEST_FILE, '5.4'); // Arbitrary pre-PHP7 version. + $this->assertWarning($file, $line, "Using '{$found}' outside of a loop or switch structure is invalid"); + + $file = $this->sniffFile(self::TEST_FILE, '7.0'); + $this->assertError($file, $line, "Using '{$found}' outside of a loop or switch structure is invalid and will throw a fatal error since PHP 7.0"); + } + + /** + * Data provider. + * + * @see testBreakContinueOutsideLoop() + * + * @return array + */ + public function dataBreakContinueOutsideLoop() + { + return array( + array(116, 'continue'), + array(118, 'continue'), + array(120, 'break'), + array(124, 'continue'), + array(128, 'break'), + array(131, 'continue'), + ); + } + + + /** + * testNoViolation + * + * @group forbiddenBreakContinueOutsideLoop + * + * @dataProvider dataNoViolation + * + * @param int $line The line number. + * + * @return void + */ + public function testNoViolation($line) + { + $file = $this->sniffFile(self::TEST_FILE, '7.0'); + $this->assertNoViolation($file, $line); + } + + /** + * Data provider. + * + * @see testNoViolation() + * + * @return array + */ + public function dataNoViolation() + { + return array( + array(8), + array(11), + array(17), + array(20), + array(26), + array(29), + array(36), + array(39), + array(47), + array(51), + array(54), + array(60), + array(63), + array(69), + array(72), + array(78), + array(81), + array(89), + array(93), + array(96), + array(103), + array(106), + ); + } +} diff --git a/Tests/sniff-examples/forbidden_break_continue_outside_loop.php b/Tests/sniff-examples/forbidden_break_continue_outside_loop.php new file mode 100644 index 000000000..1a888e011 --- /dev/null +++ b/Tests/sniff-examples/forbidden_break_continue_outside_loop.php @@ -0,0 +1,131 @@ + $value) { + if ($key === 5) { + continue; + } + if ($key === 8) { + break; + } +} + +while ($whileExample < 10) { + if ($whileExample === 5) { + continue; + } + if ($whileExample === 8) { + break; + } + $whileExample++; +} + +do { + if ($doWhileExample === 5) { + continue; + } + if ($doWhileExample === 8) { + break; + } + $doWhileExample++; +} while ($doWhileExample < 10); + +switch ($switchKey) { + case 5: + echo 'hello'; + continue; + + case 8: + echo 'world'; + break; + + default: + break; +} + +// Alternative syntax for control structures. +for ($i = 0; $i < 10; $i++): + if ($i === 5): + continue; + endif; + if ($i === 8): + break; + endif; +endfor; + +foreach ($forExample as $key => $value): + if ($key === 5): + continue; + endif; + if ($key === 8): + break; + endif; +endforeach; + +while ($whileExample < 10): + if ($whileExample === 5): + continue; + endif; + if ($whileExample === 8): + break; + endif; + $whileExample++; +endwhile; + +switch ($switchKey): + case 5: + echo 'hello'; + continue; + + case 8: + echo 'world'; + break; + + default: + break; +endswitch; + +// Control structure within a function. +function testingScope() { + for ($i = 0; $i < 10; $i++) { + if ($i === 5) { + continue; + } + if ($i === 8) { + break; + } + } +} + + +/** + * Invalid examples - these should all trigger an error. + */ +if ( $a === $b ) { + continue; +} elseif ( $a === $c ) { + continue; +} else { + break; +} + +function testFunctionA() { + continue; +} + +function testFunctionB() { + break; +} + +continue;