Skip to content

Commit

Permalink
Add new sniff for various integer types.
Browse files Browse the repository at this point in the history
Checks for:
* binary integers which were introduced in PHP 5.4
* invalid binary integers
* invalid octal integers which were truncated prior to PHP 7 and give a parse error in 7.
* hexadecimal numeric strings for which the type juggling behaviour and recognition as numeric was inconsistent prior to PHP 7 and are no longer treated as numeric in PHP 7+.

Includes unit tests.

Closes PHPCompatibility#55
  • Loading branch information
jrfnl committed Aug 8, 2016
1 parent 7378622 commit 8113211
Show file tree
Hide file tree
Showing 3 changed files with 384 additions and 0 deletions.
216 changes: 216 additions & 0 deletions Sniffs/PHP/ValidIntegersSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php
/**
* PHPCompatibility_Sniffs_PHP_ValidIntegersSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/

/**
* PHPCompatibility_Sniffs_PHP_ValidIntegersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class PHPCompatibility_Sniffs_PHP_ValidIntegersSniff extends PHPCompatibility_Sniff
{

/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_LNUMBER, // Binary, octal integers.
T_CONSTANT_ENCAPSED_STRING, // Hex numeric string.
);

}//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.
*
* @return void
*/
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
static $shown = false;
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];


if ($this->couldBeBinaryInteger($tokens, $stackPtr) === true) {
if ($this->supportsBelow('5.3')) {
$error = 'Binary integer literals were not present in PHP version 5.3 or earlier. Found: %s';
if (version_compare(PHP_VERSION, '5.4', '>=')) {
$data = array($token['content']);
}
else {
$data = array($this->getBinaryInteger($phpcsFile, $tokens, $stackPtr));
}
$phpcsFile->addError($error, $stackPtr, 'BinaryIntegerFound', $data);
//$this->addMessage($phpcsFile, $error, $stackPtr, true, 'BinaryIntegerFound', $data);
}

if ($this->isInvalidBinaryInteger($tokens, $stackPtr) === true) {
$error = 'Invalid binary integer detected. Found: %s';
$data = array($this->getBinaryInteger($phpcsFile, $tokens, $stackPtr));
$phpcsFile->addError($error, $stackPtr, 'InvalidBinaryIntegerFound', $data);
//$this->addMessage($phpcsFile, $error, $stackPtr, true, 'InvalidBinaryIntegerFound', $data);

}
// return;
}

$data = array( $token['content'] );
if ($this->isInvalidOctalInteger($tokens, $stackPtr) === true) {
$error = 'Invalid octal integer detected. Prior to PHP 7 this would lead to a truncated number. From PHP 7 onwards this causes a parse error. Found: %s';
$phpcsFile->addError($error, $stackPtr, 'InvalidOctalIntegerFound', $data);
//$this->addMessage($phpcsFile, $error, $stackPtr, true, 'InvalidOctalIntegerFound', $data);

// return;
}

if ($this->isHexidecimalNumericString($tokens, $stackPtr) === true) {
$error = 'The behaviour of hexadecimal numeric strings was inconsistent prior to PHP 7 and support has been removed in PHP 7. Found: %s';
$isError = $this->supportsAbove('7.0');
//$this->addMessage($phpcsFile, $error, $stackPtr, $isError, 'HexNumericStringFound', $data);

if ($isError === true) {
$phpcsFile->addError($error, $stackPtr, 'HexNumericStringFound', $data);
} else {
$phpcsFile->addWarning($error, $stackPtr, 'HexNumericStringFound', $data);
}
// return;
}
/*if( $shown === false ) {
var_dump( $tokens );
ini_set('xdebug.var_display_max_depth', 6);
var_dump( $phpcsFile->getErrors() );
var_dump( $phpcsFile->getWarnings() );
$shown = true;
}*/

}//end process()


/**
* Could the current token an potentially be a binary integer ?
*
* @param array $token Token stack.
* @param int $stackPtr The current position in the token stack.
*
* @return bool
*/
private function couldBeBinaryInteger($tokens, $stackPtr) {
$token = $tokens[$stackPtr];

if ($token['code'] !== T_LNUMBER) {
return false;
}

if (version_compare( PHP_VERSION, '5.4', '>=')) {
return (preg_match('`^0b[0-1]+$`D', $token['content']) === 1);
}
// Pre-5.4, binary strings are tokenized as T_LNUMBER (0) + T_STRING ("b01010101").
// At this point, we don't yet care whether it's a valid binary int, that's a separate check.
else {
return($token['content'] === '0' && $tokens[$stackPtr+1]['code'] === T_STRING && preg_match('`^b[0-9]+$`D', $tokens[$stackPtr+1]['content']) === 1);
}
}

/**
* Is the current token an invalid binary integer ?
*
* @param array $token Token stack.
* @param int $stackPtr The current position in the token stack.
*
* @return bool
*/
private function isInvalidBinaryInteger($tokens, $stackPtr) {
if ($this->couldBeBinaryInteger($tokens, $stackPtr) === false) {
return false;
}

if (version_compare( PHP_VERSION, '5.4', '>=')) {
// If it's an invalid binary int, the token will be split into two T_LNUMBER tokens.
return ($tokens[$stackPtr+1]['code'] === T_LNUMBER);
}
else {
return (preg_match('`^b[0-1]+$`D', $tokens[$stackPtr+1]['content']) === 0);
}
}

/**
* Retrieve the content of the tokens which together look like a binary integer.
*
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param array $token Token stack.
* @param int $stackPtr The position of the current token in
* the stack.
*
* @return string
*/
private function getBinaryInteger(PHP_CodeSniffer_File $phpcsFile, $tokens, $stackPtr) {
$length = 2; // PHP < 5.4 T_LNUMBER + T_STRING

if (version_compare( PHP_VERSION, '5.4', '>=')) {
$i = $stackPtr;
while ($tokens[$i]['code'] === T_LNUMBER) {
$i++;
}
$length = ($i - $stackPtr);
}

return $phpcsFile->getTokensAsString($stackPtr, $length);
}

/**
* Is the current token an invalid octal integer ?
*
* @param array $token Token stack.
* @param int $stackPtr The current position in the token stack.
*
* @return bool
*/
private function isInvalidOctalInteger($tokens, $stackPtr) {
$token = $tokens[$stackPtr];

if ($token['code'] === T_LNUMBER && preg_match('`^0[0-7]*[8-9]+[0-9]*$`D', $token['content']) === 1) {
return true;
}

return false;
}

/**
* Is the current token a hexidecimal numeric string ?
*
* @param array $token Token stack.
* @param int $stackPtr The current position in the token stack.
*
* @return bool
*/
private function isHexidecimalNumericString($tokens, $stackPtr) {
$token = $tokens[$stackPtr];

if ($token['code'] === T_CONSTANT_ENCAPSED_STRING && preg_match('`^([\'"])0x[a-f0-9]+\1$`iD', $token['content']) === 1) {
return true;
}

return false;
}

}//end class
156 changes: 156 additions & 0 deletions Tests/Sniffs/PHP/ValidIntegersSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php
/**
* Valid Integers Sniff test file
*
* @package PHPCompatibility
*/


/**
* Valid Integers Sniff tests
*
* @uses BaseSniffTest
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ValidIntegersSniffTest extends BaseSniffTest
{

const TEST_FILE = 'sniff-examples/valid_integers.php';

/**
* setUp
*
* @return void
*/
public function setUp()
{
parent::setUp();

$this->_sniffFile = $this->sniffFile(self::TEST_FILE);
}


/**
* testBinaryInteger
*
* @dataProvider dataBinaryInteger
*
* @param int $line Line number where the error should occur.
* @param string $octal (Start of) Binary number as a string.
* @param bool $testNoViolation Whether or not to test for no violations in PHP 5.4.
*
* @return void
*/
public function testBinaryInteger53($line, $binary, $testNoViolation = false)
{
$file = $this->sniffFile(self::TEST_FILE, '5.3');
$this->assertError($this->_sniffFile, $line, 'Binary integer literals were not present in PHP version 5.3 or earlier. Found: ' . $binary);


}

/**
* testBinaryInteger
*
* @dataProvider dataBinaryInteger
*
* @param int $line Line number where the error should occur.
* @param string $octal (Start of) Binary number as a string.
*
* @return void
*/
public function testBinaryInteger54($line, $binary, $testNoViolation = false)
{
if ($testNoViolation === true) {
$file = $this->sniffFile(self::TEST_FILE, '5.4');
$this->assertNoViolation($file, $line);
}
}

/**
* dataBinaryInteger
*
* @see testBinaryInteger()
*
* @return array
*/
public function dataBinaryInteger() {
return array(
array(3, '0b001001101', true),
array(4, '0b01'),
);
}


/**
* testInvalidBinaryInteger
*
* @return void
*/
public function testInvalidBinaryInteger()
{
$this->assertError($this->_sniffFile, 4, 'Invalid binary integer detected. Found: 0b0123456');
}


/**
* testInvalidOctalInteger
*
* @dataProvider dataInvalidOctalInteger
*
* @param int $line Line number where the error should occur.
* @param string $octal Octal number as a string.
*
* @return void
*/
public function testInvalidOctalInteger($line, $octal)
{
$this->assertError($this->_sniffFile, $line, 'Invalid octal integer detected. Prior to PHP 7 this would lead to a truncated number. From PHP 7 onwards this causes a parse error. Found: ' . $octal);
}

/**
* dataInvalidOctalInteger
*
* @see testInvalidOctalInteger()
*
* @return array
*/
public function dataInvalidOctalInteger() {
return array(
array(7, '08'),
array(8, '038'),
array(9, '091'),
);
}


/**
* testValidOctalInteger
*
* @return void
*/
public function testValidOctalInteger() {
$this->assertNoViolation($this->_sniffFile, 5);
}


/**
* testHexNumericString
*
* @return void
*/
public function testHexNumericString()
{
$error = 'The behaviour of hexadecimal numeric strings was inconsistent prior to PHP 7 and support has been removed in PHP 7. Found: \'0xaa78b5\'';

$file = $this->sniffFile(self::TEST_FILE, '5.0');
$this->assertWarning($file, 11, $error);
$this->assertNoViolation($file, 12);

$file = $this->sniffFile(self::TEST_FILE, '7.0');
$this->assertError($file, 11, $error);
$this->assertNoViolation($file, 12);
}

}
12 changes: 12 additions & 0 deletions Tests/sniff-examples/valid_integers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

$binary = 0b001001101;
$invalidBinary = 0b0123456;

$validOctal = 061; // Ok.
$invalidOctal = 08;
$invalidOctal = 038;
$invalidOctal = 091;

$hex = '0xaa78b5';
$hex = 'aa78b5'; // Ok.

0 comments on commit 8113211

Please sign in to comment.