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 (warning) and give a parse error in 7 (error).
* hexadecimal numeric strings for which the type juggling behaviour and recognition as numeric was inconsistent prior to PHP 7 (warning) and are no longer treated as numeric in PHP 7+(error).

Includes unit tests.

Closes PHPCompatibility#55
  • Loading branch information
jrfnl committed Aug 13, 2016
1 parent 9cb63b0 commit 1f6fc73
Show file tree
Hide file tree
Showing 3 changed files with 388 additions and 0 deletions.
208 changes: 208 additions & 0 deletions Sniffs/PHP/ValidIntegersSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php
/**
* PHPCompatibility_Sniffs_PHP_ValidIntegersSniff.
*
* @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
{
protected $isLowPHPVersion = false;

/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$this->isLowPHPVersion = version_compare(phpversion(), '5.4', '<');

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)
{
$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 ($this->isLowPHPVersion === false) {
$data = array($token['content']);
}
else {
$data = array($this->getBinaryInteger($phpcsFile, $tokens, $stackPtr));
}
$phpcsFile->addError($error, $stackPtr, '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);
}
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';
$isError = $this->supportsAbove('7.0');

if ($isError === true) {
$phpcsFile->addError($error, $stackPtr, 'InvalidOctalIntegerFound', $data);
} else {
$phpcsFile->addWarning($error, $stackPtr, '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');

if ($isError === true) {
$phpcsFile->addError($error, $stackPtr, 'HexNumericStringFound', $data);
} else {
$phpcsFile->addWarning($error, $stackPtr, 'HexNumericStringFound', $data);
}
return;
}

}//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 ($this->isLowPHPVersion === false) {
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 ($this->isLowPHPVersion === false) {
// 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 ($this->isLowPHPVersion === false) {
$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
168 changes: 168 additions & 0 deletions Tests/Sniffs/PHP/ValidIntegersSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?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';

/**
* Sniffed file
*
* @var PHP_CodeSniffer_File
*/
protected $_sniffFile;

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

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


/**
* testBinaryInteger
*
* @group ValidIntegers
*
* @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 noViolation.
* Defaults to true. Set to false if another error is
* expected on the same line (invalid binary)
*
* @return void
*/
public function testBinaryInteger($line, $binary, $testNoViolation = true)
{

$file = $this->sniffFile(self::TEST_FILE, '5.3');
$this->assertError($file, $line, 'Binary integer literals were not present in PHP version 5.3 or earlier. Found: ' . $binary);


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', false),
);
}


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


/**
* testInvalidOctalInteger
*
* @group ValidIntegers
*
* @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)
{
$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: ' . $octal;

$file = $this->sniffFile(self::TEST_FILE, '5.4');
$this->assertWarning($file, $line, $error);

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

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


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


/**
* testHexNumericString
*
* @group ValidIntegers
*
* @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 1f6fc73

Please sign in to comment.