Skip to content

Commit

Permalink
Merge pull request #114 from PHPCSStandards/controlstructures/new-get…
Browse files Browse the repository at this point in the history
…caughtexceptions-utility

Utils\ControlStructures: new `getCaughtExceptions()` method
  • Loading branch information
jrfnl committed Mar 21, 2020
2 parents 3fa56a6 + 744393d commit 8955a74
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 0 deletions.
82 changes: 82 additions & 0 deletions PHPCSUtils/Utils/ControlStructures.php
Expand Up @@ -10,6 +10,7 @@

namespace PHPCSUtils\Utils;

use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
Expand Down Expand Up @@ -335,4 +336,85 @@ public static function getDeclareScopeOpenClose(File $phpcsFile, $stackPtr)

return false;
}

/**
* Retrieve the exception(s) being caught in a CATCH condition.
*
* The returned array will contain the following information for each caught exception:
*
* <code>
* 0 => array(
* 'type' => string, // The type declaration for the exception being caught.
* 'type_token' => integer, // The stack pointer to the start of the type declaration.
* 'type_end_token' => integer, // The stack pointer to the end of the type declaration.
* )
* </code>
*
* @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
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of
* type T_CATCH or doesn't exist or in case
* of a parse error.
*/
public static function getCaughtExceptions(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

if (isset($tokens[$stackPtr]) === false
|| $tokens[$stackPtr]['code'] !== \T_CATCH
) {
throw new RuntimeException('$stackPtr must be of type T_CATCH');
}

if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
throw new RuntimeException('Parentheses opener/closer of the T_CATCH could not be determined');
}

$opener = $tokens[$stackPtr]['parenthesis_opener'];
$closer = $tokens[$stackPtr]['parenthesis_closer'];
$exceptions = [];

$foundName = '';
$firstToken = null;
$lastToken = null;

for ($i = ($opener + 1); $i < $closer; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
continue;
}

if (isset(Collections::$OONameTokens[$tokens[$i]['code']]) === false) {
// Add the current exception to the result array.
$exceptions[] = [
'type' => $foundName,
'type_token' => $firstToken,
'type_end_token' => $lastToken,
];

if ($tokens[$i]['code'] === \T_BITWISE_OR) {
// Multi-catch. Reset and continue.
$foundName = '';
$firstToken = null;
$lastToken = null;
continue;
}

break;
}

if (isset($firstToken) === false) {
$firstToken = $i;
}

$foundName .= $tokens[$i]['content'];
$lastToken = $i;
}

return $exceptions;
}
}
32 changes: 32 additions & 0 deletions Tests/Utils/ControlStructures/GetCaughtExceptionsTest.inc
@@ -0,0 +1,32 @@
<?php

/* testNotCatch */
try {

/* testSingleCatchNameOnly */
} catch (RuntimeException $e) {

/* testSingleCatchNameLeadingBackslash */
} catch (\RuntimeException $e) {

/* testSingleCatchPartiallyQualified */
} catch ( MyNS\RuntimeException $e) {

/* testSingleCatchFullyQualified */
} catch ( \MyNS\RuntimeException $e) {

/* testSingleCatchPartiallyQualifiedWithCommentAndWhitespace */
} catch ( My\NS \ Sub\ /* comment */ RuntimeException $e) {

/* testSingleCatchNamespaceOperator */
} catch ( namespace\RuntimeException $e) {

/* testMultiCatchSingleNames */
} catch (RuntimeException | ParseErrorException | AnotherException $e) {

/* testMultiCatchCompoundNames */
} catch (\NS\RuntimeException | My\ParseErrorException | namespace \ AnotherException $e) {

/* testLiveCoding */
// Intentional parse error.
} catch (
202 changes: 202 additions & 0 deletions Tests/Utils/ControlStructures/GetCaughtExceptionsTest.php
@@ -0,0 +1,202 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2020 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Tests\Utils\ControlStructures;

use PHPCSUtils\TestUtils\UtilityMethodTestCase;
use PHPCSUtils\Utils\ControlStructures;

/**
* Tests for the \PHPCSUtils\Utils\ControlStructures::getCaughtExceptions() method.
*
* @covers \PHPCSUtils\Utils\ControlStructures::getCaughtExceptions
*
* @group controlstructures
*
* @since 1.0.0
*/
class GetCaughtExceptionsTest extends UtilityMethodTestCase
{

/**
* Test receiving an expected exception when a non-existent token is passed.
*
* @return void
*/
public function testNonExistentToken()
{
$this->expectPhpcsException('$stackPtr must be of type T_CATCH');
ControlStructures::getCaughtExceptions(self::$phpcsFile, 10000);
}

/**
* Test receiving an expected exception when a non-CATCH token is passed.
*
* @return void
*/
public function testNotCatch()
{
$this->expectPhpcsException('$stackPtr must be of type T_CATCH');

$target = $this->getTargetToken('/* testNotCatch */', \T_TRY);
ControlStructures::getCaughtExceptions(self::$phpcsFile, $target);
}

/**
* Test receiving an expected exception when a parse error is encountered.
*
* @return void
*/
public function testParseError()
{
$this->expectPhpcsException('Parentheses opener/closer of the T_CATCH could not be determined');

$target = $this->getTargetToken('/* testLiveCoding */', \T_CATCH);
ControlStructures::getCaughtExceptions(self::$phpcsFile, $target);
}

/**
* Test retrieving the exceptions caught in a `catch` condition.
*
* @dataProvider dataGetCaughtExceptions
*
* @param string $testMarker The comment which prefaces the target token in the test file.
* @param array $expected The expected return value.
*
* @return void
*/
public function testGetCaughtExceptions($testMarker, $expected)
{
$stackPtr = $this->getTargetToken($testMarker, \T_CATCH);

// Translate offsets to absolute token positions.
foreach ($expected as $key => $value) {
$expected[$key]['type_token'] += $stackPtr;
$expected[$key]['type_end_token'] += $stackPtr;
}

$result = ControlStructures::getCaughtExceptions(self::$phpcsFile, $stackPtr);
$this->assertSame($expected, $result);
}

/**
* Data provider.
*
* @see testGetCaughtExceptions() For the array format.
*
* @return array
*/
public function dataGetCaughtExceptions()
{
return [
'single-name-only' => [
'target' => '/* testSingleCatchNameOnly */',
'expected' => [
[
'type' => 'RuntimeException',
'type_token' => 3,
'type_end_token' => 3,
],
],
],
'single-name-leading-backslash' => [
'target' => '/* testSingleCatchNameLeadingBackslash */',
'expected' => [
[
'type' => '\RuntimeException',
'type_token' => 3,
'type_end_token' => 4,
],
],
],
'single-partially-qualified' => [
'target' => '/* testSingleCatchPartiallyQualified */',
'expected' => [
[
'type' => 'MyNS\RuntimeException',
'type_token' => 4,
'type_end_token' => 6,
],
],
],
'single-fully-qualified' => [
'target' => '/* testSingleCatchFullyQualified */',
'expected' => [
[
'type' => '\MyNS\RuntimeException',
'type_token' => 4,
'type_end_token' => 7,
],
],
],
'single-name-with-comments-whitespace' => [
'target' => '/* testSingleCatchPartiallyQualifiedWithCommentAndWhitespace */',
'expected' => [
[
'type' => 'My\NS\Sub\RuntimeException',
'type_token' => 4,
'type_end_token' => 15,
],
],
],
'single-namespace-operator' => [
'target' => '/* testSingleCatchNamespaceOperator */',
'expected' => [
[
'type' => 'namespace\RuntimeException',
'type_token' => 4,
'type_end_token' => 6,
],
],
],
'multi-unqualified-names' => [
'target' => '/* testMultiCatchSingleNames */',
'expected' => [
[
'type' => 'RuntimeException',
'type_token' => 3,
'type_end_token' => 3,
],
[
'type' => 'ParseErrorException',
'type_token' => 7,
'type_end_token' => 7,
],
[
'type' => 'AnotherException',
'type_token' => 11,
'type_end_token' => 11,
],
],
],

'multi-qualified-names' => [
'target' => '/* testMultiCatchCompoundNames */',
'expected' => [
[
'type' => '\NS\RuntimeException',
'type_token' => 3,
'type_end_token' => 6,
],
[
'type' => 'My\ParseErrorException',
'type_token' => 10,
'type_end_token' => 12,
],
[
'type' => 'namespace\AnotherException',
'type_token' => 16,
'type_end_token' => 20,
],
],
],
];
}
}

0 comments on commit 8955a74

Please sign in to comment.