Skip to content

Commit

Permalink
Merge pull request #70 from PHPCSStandards/feature/new-controlstructu…
Browse files Browse the repository at this point in the history
…res-class

New Utils\ControlStructures class
  • Loading branch information
jrfnl authored Jan 30, 2020
2 parents fa99869 + 97f7bfe commit 8d9cf7b
Show file tree
Hide file tree
Showing 20 changed files with 1,615 additions and 0 deletions.
33 changes: 33 additions & 0 deletions PHPCSUtils/Tokens/Collections.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@
class Collections
{

/**
* Control structures which can use the alternative control structure syntax.
*
* @var array <int> => <int>
*/
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.
*
Expand Down Expand Up @@ -109,6 +125,23 @@ class Collections
\T_CLOSURE => \T_CLOSURE,
];

/**
* Control structure tokens.
*
* @var array <int> => <int>
*/
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.
*
Expand Down
338 changes: 338 additions & 0 deletions PHPCSUtils/Utils/ControlStructures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Utils;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;

/**
* Utility functions for use when examining control structures.
*
* @since 1.0.0
*/
class ControlStructures
{

/**
* Check whether a control structure has a body.
*
* Some control structures - `while`, `for` and `declare` - can be declared without a body, like
* `while (++$i < 10);`.
*
* All other control structures will always have a body, though the body may be empty, where "empty" means:
* no _code_ is found in the body. If a control structure body only contains a comment, it will be
* regarded as empty.
*
* @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.
* @param bool $allowEmpty Whether a control structure with an empty body should
* still be considered as having a body.
* Defaults to `true`.
*
* @return bool True when the control structure has a body, or when `$allowEmpty` is set to `false`
* when it has a non-empty body.
* False in all other cases, including when a non-control structure token has been passed.
*/
public static function hasBody(File $phpcsFile, $stackPtr, $allowEmpty = true)
{
$tokens = $phpcsFile->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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

/* testNoCloseParenthesis */
// Intentional parse error.
declare(ticks=1
Loading

0 comments on commit 8d9cf7b

Please sign in to comment.