Introducing coding standards validation.

This commit is contained in:
Marcos Schratzenstaller
2019-04-22 17:28:46 -03:00
committed by Nathan Rice
parent 6e40c921ab
commit fd504d41ac
1624 changed files with 190339 additions and 262 deletions
@@ -0,0 +1,87 @@
<?php
/**
* \PHPCompatibility\Sniffs\Classes\NewAnonymousClasses.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\Classes;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Classes\NewAnonymousClasses.
*
* Anonymous classes are supported in PHP 7.0
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class NewAnonymousClassesSniff extends Sniff
{
/**
* Tokens which in various PHP versions indicate the `class` keyword.
*
* The dedicated anonymous class token is added from the `register()`
* method if the token is available.
*
* @var array
*/
private $indicators = array(
T_CLASS => T_CLASS,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
if (defined('T_ANON_CLASS')) {
$this->indicators[T_ANON_CLASS] = T_ANON_CLASS;
}
return array(T_NEW);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.6') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
if ($nextNonEmpty === false || isset($this->indicators[$tokens[$nextNonEmpty]['code']]) === false) {
return;
}
// Still here ? In that case, it is an anonymous class.
$phpcsFile->addError(
'Anonymous classes are not supported in PHP 5.6 or earlier',
$stackPtr,
'Found'
);
}
}
@@ -0,0 +1,842 @@
<?php
/**
* \PHPCompatibility\Sniffs\Classes\NewClassesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Classes;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Classes\NewClassesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
class NewClassesSniff extends AbstractNewFeatureSniff
{
/**
* A list of new classes, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the class appears.
*
* @var array(string => array(string => bool))
*/
protected $newClasses = array(
'ArrayObject' => array(
'4.4' => false,
'5.0' => true,
),
'ArrayIterator' => array(
'4.4' => false,
'5.0' => true,
),
'CachingIterator' => array(
'4.4' => false,
'5.0' => true,
),
'DirectoryIterator' => array(
'4.4' => false,
'5.0' => true,
),
'RecursiveDirectoryIterator' => array(
'4.4' => false,
'5.0' => true,
),
'RecursiveIteratorIterator' => array(
'4.4' => false,
'5.0' => true,
),
'php_user_filter' => array(
'4.4' => false,
'5.0' => true,
),
'tidy' => array(
'4.4' => false,
'5.0' => true,
),
'SimpleXMLElement' => array(
'5.0.0' => false,
'5.0.1' => true,
),
'tidyNode' => array(
'5.0.0' => false,
'5.0.1' => true,
),
'libXMLError' => array(
'5.0' => false,
'5.1' => true,
),
'PDO' => array(
'5.0' => false,
'5.1' => true,
),
'PDOStatement' => array(
'5.0' => false,
'5.1' => true,
),
'AppendIterator' => array(
'5.0' => false,
'5.1' => true,
),
'EmptyIterator' => array(
'5.0' => false,
'5.1' => true,
),
'FilterIterator' => array(
'5.0' => false,
'5.1' => true,
),
'InfiniteIterator' => array(
'5.0' => false,
'5.1' => true,
),
'IteratorIterator' => array(
'5.0' => false,
'5.1' => true,
),
'LimitIterator' => array(
'5.0' => false,
'5.1' => true,
),
'NoRewindIterator' => array(
'5.0' => false,
'5.1' => true,
),
'ParentIterator' => array(
'5.0' => false,
'5.1' => true,
),
'RecursiveArrayIterator' => array(
'5.0' => false,
'5.1' => true,
),
'RecursiveCachingIterator' => array(
'5.0' => false,
'5.1' => true,
),
'RecursiveFilterIterator' => array(
'5.0' => false,
'5.1' => true,
),
'SimpleXMLIterator' => array(
'5.0' => false,
'5.1' => true,
),
'SplFileObject' => array(
'5.0' => false,
'5.1' => true,
),
'XMLReader' => array(
'5.0' => false,
'5.1' => true,
),
'SplFileInfo' => array(
'5.1.1' => false,
'5.1.2' => true,
),
'SplTempFileObject' => array(
'5.1.1' => false,
'5.1.2' => true,
),
'XMLWriter' => array(
'5.1.1' => false,
'5.1.2' => true,
),
'DateTime' => array(
'5.1' => false,
'5.2' => true,
),
'DateTimeZone' => array(
'5.1' => false,
'5.2' => true,
),
'RegexIterator' => array(
'5.1' => false,
'5.2' => true,
),
'RecursiveRegexIterator' => array(
'5.1' => false,
'5.2' => true,
),
'ReflectionFunctionAbstract' => array(
'5.1' => false,
'5.2' => true,
),
'ZipArchive' => array(
'5.1' => false,
'5.2' => true,
),
'Closure' => array(
'5.2' => false,
'5.3' => true,
),
'DateInterval' => array(
'5.2' => false,
'5.3' => true,
),
'DatePeriod' => array(
'5.2' => false,
'5.3' => true,
),
'finfo' => array(
'5.2' => false,
'5.3' => true,
),
'Collator' => array(
'5.2' => false,
'5.3' => true,
),
'NumberFormatter' => array(
'5.2' => false,
'5.3' => true,
),
'Locale' => array(
'5.2' => false,
'5.3' => true,
),
'Normalizer' => array(
'5.2' => false,
'5.3' => true,
),
'MessageFormatter' => array(
'5.2' => false,
'5.3' => true,
),
'IntlDateFormatter' => array(
'5.2' => false,
'5.3' => true,
),
'Phar' => array(
'5.2' => false,
'5.3' => true,
),
'PharData' => array(
'5.2' => false,
'5.3' => true,
),
'PharFileInfo' => array(
'5.2' => false,
'5.3' => true,
),
'FilesystemIterator' => array(
'5.2' => false,
'5.3' => true,
),
'GlobIterator' => array(
'5.2' => false,
'5.3' => true,
),
'MultipleIterator' => array(
'5.2' => false,
'5.3' => true,
),
'RecursiveTreeIterator' => array(
'5.2' => false,
'5.3' => true,
),
'SplDoublyLinkedList' => array(
'5.2' => false,
'5.3' => true,
),
'SplFixedArray' => array(
'5.2' => false,
'5.3' => true,
),
'SplHeap' => array(
'5.2' => false,
'5.3' => true,
),
'SplMaxHeap' => array(
'5.2' => false,
'5.3' => true,
),
'SplMinHeap' => array(
'5.2' => false,
'5.3' => true,
),
'SplObjectStorage' => array(
'5.2' => false,
'5.3' => true,
),
'SplPriorityQueue' => array(
'5.2' => false,
'5.3' => true,
),
'SplQueue' => array(
'5.2' => false,
'5.3' => true,
),
'SplStack' => array(
'5.2' => false,
'5.3' => true,
),
'ResourceBundle' => array(
'5.3.1' => false,
'5.3.2' => true,
),
'CallbackFilterIterator' => array(
'5.3' => false,
'5.4' => true,
),
'RecursiveCallbackFilterIterator' => array(
'5.3' => false,
'5.4' => true,
),
'ReflectionZendExtension' => array(
'5.3' => false,
'5.4' => true,
),
'SessionHandler' => array(
'5.3' => false,
'5.4' => true,
),
'SNMP' => array(
'5.3' => false,
'5.4' => true,
),
'Transliterator' => array(
'5.3' => false,
'5.4' => true,
),
'Spoofchecker' => array(
'5.3' => false,
'5.4' => true,
),
'Generator' => array(
'5.4' => false,
'5.5' => true,
),
'CURLFile' => array(
'5.4' => false,
'5.5' => true,
),
'DateTimeImmutable' => array(
'5.4' => false,
'5.5' => true,
),
'IntlCalendar' => array(
'5.4' => false,
'5.5' => true,
),
'IntlGregorianCalendar' => array(
'5.4' => false,
'5.5' => true,
),
'IntlTimeZone' => array(
'5.4' => false,
'5.5' => true,
),
'IntlBreakIterator' => array(
'5.4' => false,
'5.5' => true,
),
'IntlRuleBasedBreakIterator' => array(
'5.4' => false,
'5.5' => true,
),
'IntlCodePointBreakIterator' => array(
'5.4' => false,
'5.5' => true,
),
'UConverter' => array(
'5.4' => false,
'5.5' => true,
),
'GMP' => array(
'5.5' => false,
'5.6' => true,
),
'IntlChar' => array(
'5.6' => false,
'7.0' => true,
),
'ReflectionType' => array(
'5.6' => false,
'7.0' => true,
),
'ReflectionGenerator' => array(
'5.6' => false,
'7.0' => true,
),
'ReflectionClassConstant' => array(
'7.0' => false,
'7.1' => true,
),
);
/**
* A list of new Exception classes, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the class appears.
*
* {@internal Classes listed here do not need to be added to the $newClasses
* property as well.
* This list is automatically added to the $newClasses property
* in the `register()` method.}}
*
* {@internal Helper to update this list: https://3v4l.org/MhlUp}}
*
* @var array(string => array(string => bool))
*/
protected $newExceptions = array(
'com_exception' => array(
'4.4' => false,
'5.0' => true,
),
'DOMException' => array(
'4.4' => false,
'5.0' => true,
),
'Exception' => array(
// According to the docs introduced in PHP 5.1, but this appears to be.
// an error. Class was introduced with try/catch keywords in PHP 5.0.
'4.4' => false,
'5.0' => true,
),
'ReflectionException' => array(
'4.4' => false,
'5.0' => true,
),
'SoapFault' => array(
'4.4' => false,
'5.0' => true,
),
'SQLiteException' => array(
'4.4' => false,
'5.0' => true,
),
'ErrorException' => array(
'5.0' => false,
'5.1' => true,
),
'BadFunctionCallException' => array(
'5.0' => false,
'5.1' => true,
),
'BadMethodCallException' => array(
'5.0' => false,
'5.1' => true,
),
'DomainException' => array(
'5.0' => false,
'5.1' => true,
),
'InvalidArgumentException' => array(
'5.0' => false,
'5.1' => true,
),
'LengthException' => array(
'5.0' => false,
'5.1' => true,
),
'LogicException' => array(
'5.0' => false,
'5.1' => true,
),
'mysqli_sql_exception' => array(
'5.0' => false,
'5.1' => true,
),
'OutOfBoundsException' => array(
'5.0' => false,
'5.1' => true,
),
'OutOfRangeException' => array(
'5.0' => false,
'5.1' => true,
),
'OverflowException' => array(
'5.0' => false,
'5.1' => true,
),
'PDOException' => array(
'5.0' => false,
'5.1' => true,
),
'RangeException' => array(
'5.0' => false,
'5.1' => true,
),
'RuntimeException' => array(
'5.0' => false,
'5.1' => true,
),
'UnderflowException' => array(
'5.0' => false,
'5.1' => true,
),
'UnexpectedValueException' => array(
'5.0' => false,
'5.1' => true,
),
'PharException' => array(
'5.2' => false,
'5.3' => true,
),
'SNMPException' => array(
'5.3' => false,
'5.4' => true,
),
'IntlException' => array(
'5.4' => false,
'5.5' => true,
),
'Error' => array(
'5.6' => false,
'7.0' => true,
),
'ArithmeticError' => array(
'5.6' => false,
'7.0' => true,
),
'AssertionError' => array(
'5.6' => false,
'7.0' => true,
),
'DivisionByZeroError' => array(
'5.6' => false,
'7.0' => true,
),
'ParseError' => array(
'5.6' => false,
'7.0' => true,
),
'TypeError' => array(
'5.6' => false,
'7.0' => true,
),
'ClosedGeneratorException' => array(
'5.6' => false,
'7.0' => true,
),
'UI\Exception\InvalidArgumentException' => array(
'5.6' => false,
'7.0' => true,
),
'UI\Exception\RuntimeException' => array(
'5.6' => false,
'7.0' => true,
),
'ArgumentCountError' => array(
'7.0' => false,
'7.1' => true,
),
'SodiumException' => array(
'7.1' => false,
'7.2' => true,
),
'CompileError' => array(
'7.2' => false,
'7.3' => true,
),
'JsonException' => array(
'7.2' => false,
'7.3' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of class names.
$this->newClasses = $this->arrayKeysToLowercase($this->newClasses);
$this->newExceptions = $this->arrayKeysToLowercase($this->newExceptions);
// Add the Exception classes to the Classes list.
$this->newClasses = array_merge($this->newClasses, $this->newExceptions);
$targets = array(
T_NEW,
T_CLASS,
T_DOUBLE_COLON,
T_FUNCTION,
T_CLOSURE,
T_CATCH,
);
if (defined('T_ANON_CLASS')) {
$targets[] = T_ANON_CLASS;
}
if (defined('T_RETURN_TYPE')) {
$targets[] = T_RETURN_TYPE;
}
return $targets;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
switch ($tokens[$stackPtr]['type']) {
case 'T_FUNCTION':
case 'T_CLOSURE':
$this->processFunctionToken($phpcsFile, $stackPtr);
// Deal with older PHPCS version which don't recognize return type hints
// as well as newer PHPCS versions (3.3.0+) where the tokenization has changed.
$returnTypeHint = $this->getReturnTypeHintToken($phpcsFile, $stackPtr);
if ($returnTypeHint !== false) {
$this->processReturnTypeToken($phpcsFile, $returnTypeHint);
}
break;
case 'T_CATCH':
$this->processCatchToken($phpcsFile, $stackPtr);
break;
case 'T_RETURN_TYPE':
$this->processReturnTypeToken($phpcsFile, $stackPtr);
break;
default:
$this->processSingularToken($phpcsFile, $stackPtr);
break;
}
}
/**
* Processes this test for when a token resulting in a singular class name 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
*/
private function processSingularToken(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$FQClassName = '';
if ($tokens[$stackPtr]['type'] === 'T_NEW') {
$FQClassName = $this->getFQClassNameFromNewToken($phpcsFile, $stackPtr);
} elseif ($tokens[$stackPtr]['type'] === 'T_CLASS' || $tokens[$stackPtr]['type'] === 'T_ANON_CLASS') {
$FQClassName = $this->getFQExtendedClassName($phpcsFile, $stackPtr);
} elseif ($tokens[$stackPtr]['type'] === 'T_DOUBLE_COLON') {
$FQClassName = $this->getFQClassNameFromDoubleColonToken($phpcsFile, $stackPtr);
}
if ($FQClassName === '') {
return;
}
$className = substr($FQClassName, 1); // Remove global namespace indicator.
$classNameLc = strtolower($className);
if (isset($this->newClasses[$classNameLc]) === false) {
return;
}
$itemInfo = array(
'name' => $className,
'nameLc' => $classNameLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Processes this test for when a function token is encountered.
*
* - Detect new classes when used as a type hint.
*
* @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
*/
private function processFunctionToken(File $phpcsFile, $stackPtr)
{
// Retrieve typehints stripped of global NS indicator and/or nullable indicator.
$typeHints = $this->getTypeHintsFromFunctionDeclaration($phpcsFile, $stackPtr);
if (empty($typeHints) || is_array($typeHints) === false) {
return;
}
foreach ($typeHints as $hint) {
$typeHintLc = strtolower($hint);
if (isset($this->newClasses[$typeHintLc]) === true) {
$itemInfo = array(
'name' => $hint,
'nameLc' => $typeHintLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
}
}
/**
* Processes this test for when a catch token is encountered.
*
* - Detect exceptions when used in a catch statement.
*
* @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
*/
private function processCatchToken(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Bow out during live coding.
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
return;
}
$opener = $tokens[$stackPtr]['parenthesis_opener'];
$closer = ($tokens[$stackPtr]['parenthesis_closer'] + 1);
$name = '';
$listen = array(
// Parts of a (namespaced) class name.
T_STRING => true,
T_NS_SEPARATOR => true,
// End/split tokens.
T_VARIABLE => false,
T_BITWISE_OR => false,
T_CLOSE_CURLY_BRACKET => false, // Shouldn't be needed as we expect a var before this.
);
for ($i = ($opener + 1); $i < $closer; $i++) {
if (isset($listen[$tokens[$i]['code']]) === false) {
continue;
}
if ($listen[$tokens[$i]['code']] === true) {
$name .= $tokens[$i]['content'];
continue;
} else {
if (empty($name) === true) {
// Weird, we should have a name by the time we encounter a variable or |.
// So this may be the closer.
continue;
}
$name = ltrim($name, '\\');
$nameLC = strtolower($name);
if (isset($this->newExceptions[$nameLC]) === true) {
$itemInfo = array(
'name' => $name,
'nameLc' => $nameLC,
);
$this->handleFeature($phpcsFile, $i, $itemInfo);
}
// Reset for a potential multi-catch.
$name = '';
}
}
}
/**
* Processes this test for when a return type token is encountered.
*
* - Detect new classes when used as a return type declaration.
*
* @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
*/
private function processReturnTypeToken(File $phpcsFile, $stackPtr)
{
$returnTypeHint = $this->getReturnTypeHintName($phpcsFile, $stackPtr);
$returnTypeHint = ltrim($returnTypeHint, '\\');
$returnTypeHintLc = strtolower($returnTypeHint);
if (isset($this->newClasses[$returnTypeHintLc]) === false) {
return;
}
// Still here ? Then this is a return type declaration using a new class.
$itemInfo = array(
'name' => $returnTypeHint,
'nameLc' => $returnTypeHintLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newClasses[$itemInfo['nameLc']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The built-in class ' . parent::getErrorMsgTemplate();
}
}
@@ -0,0 +1,78 @@
<?php
/**
* \PHPCompatibility\Sniffs\Classes\NewConstVisibility.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Classes;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Classes\NewConstVisibility.
*
* Visibility for class constants is available since PHP 7.1.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewConstVisibilitySniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CONST);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true, null, true);
// Is the previous token a visibility indicator ?
if ($prevToken === false || isset(Tokens::$scopeModifiers[$tokens[$prevToken]['code']]) === false) {
return;
}
// Is this a class constant ?
if ($this->isClassConstant($phpcsFile, $stackPtr) === false) {
// This may be a constant declaration in the global namespace with visibility,
// but that would throw a parse error, i.e. not our concern.
return;
}
$phpcsFile->addError(
'Visibility indicators for class constants are not supported in PHP 7.0 or earlier. Found "%s const"',
$stackPtr,
'Found',
array($tokens[$prevToken]['content'])
);
}
}
@@ -0,0 +1,79 @@
<?php
/**
* \PHPCompatibility\Sniffs\Classes\NewLateStaticBindingSniff.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Classes;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Classes\NewLateStaticBindingSniff.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewLateStaticBindingSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STATIC);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
if ($nextNonEmpty === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if ($tokens[$nextNonEmpty]['code'] !== T_DOUBLE_COLON) {
return;
}
$inClass = $this->inClassScope($phpcsFile, $stackPtr, false);
if ($inClass === true && $this->supportsBelow('5.2') === true) {
$phpcsFile->addError(
'Late static binding is not supported in PHP 5.2 or earlier.',
$stackPtr,
'Found'
);
}
if ($inClass === false) {
$phpcsFile->addError(
'Late static binding is not supported outside of class scope.',
$stackPtr,
'OutsideClassScope'
);
}
}
}
@@ -0,0 +1,75 @@
<?php
/**
* \PHPCompatibility\Sniffs\Constants\NewMagicClassConstantSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Constants;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Constants\NewMagicClassConstantSniff.
*
* The special ClassName::class constant is available as of PHP 5.5.0, and allows for
* fully qualified class name resolution at compile.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewMagicClassConstantSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (strtolower($tokens[$stackPtr]['content']) !== 'class') {
return;
}
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true, null, true);
if ($prevToken === false || $tokens[$prevToken]['code'] !== T_DOUBLE_COLON) {
return;
}
$phpcsFile->addError(
'The magic class constant ClassName::class was not available in PHP 5.4 or earlier',
$stackPtr,
'Found'
);
}
}
@@ -0,0 +1,360 @@
<?php
/**
* \PHPCompatibility\Sniffs\Constants\RemovedConstantsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Constants;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Constants\RemovedConstantsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedConstantsSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of removed PHP Constants.
*
* The array lists : version number with false (deprecated) or true (removed).
* If's sufficient to list the first version where the constant was deprecated/removed.
*
* Note: PHP Constants are case-sensitive!
*
* @var array(string => array(string => bool|string|null))
*/
protected $removedConstants = array(
// Disabled since PHP 5.3.0 due to thread safety issues.
'FILEINFO_COMPRESS' => array(
'5.3' => true,
),
'CURLOPT_CLOSEPOLICY' => array(
'5.6' => true,
),
'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' => array(
'5.6' => true,
),
'CURLCLOSEPOLICY_LEAST_TRAFFIC' => array(
'5.6' => true,
),
'CURLCLOSEPOLICY_SLOWEST' => array(
'5.6' => true,
),
'CURLCLOSEPOLICY_CALLBACK' => array(
'5.6' => true,
),
'CURLCLOSEPOLICY_OLDEST' => array(
'5.6' => true,
),
'PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT' => array(
'7.0' => true,
),
'INTL_IDNA_VARIANT_2003' => array(
'7.2' => false,
),
'MCRYPT_MODE_ECB' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_MODE_CBC' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_MODE_CFB' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_MODE_OFB' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_MODE_NOFB' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_MODE_STREAM' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_ENCRYPT' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_DECRYPT' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_DEV_RANDOM' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_DEV_URANDOM' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RAND' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_3DES' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_ARCFOUR_IV' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_ARCFOUR' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_BLOWFISH' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_CAST_128' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_CAST_256' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_CRYPT' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_DES' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_DES_COMPAT' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_ENIGMA' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_GOST' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_IDEA' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_LOKI97' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_MARS' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_PANAMA' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RIJNDAEL_128' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RIJNDAEL_192' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RIJNDAEL_256' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RC2' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RC4' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RC6' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RC6_128' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RC6_192' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_RC6_256' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SAFER64' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SAFER128' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SAFERPLUS' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SERPENT' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SERPENT_128' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SERPENT_192' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SERPENT_256' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_SKIPJACK' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_TEAN' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_THREEWAY' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_TRIPLEDES' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_TWOFISH' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_TWOFISH128' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_TWOFISH192' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_TWOFISH256' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_WAKE' => array(
'7.1' => false,
'7.2' => true,
),
'MCRYPT_XTEA' => array(
'7.1' => false,
'7.2' => true,
),
'PHPDBG_FILE' => array(
'7.3' => true,
),
'PHPDBG_METHOD' => array(
'7.3' => true,
),
'PHPDBG_LINENO' => array(
'7.3' => true,
),
'PHPDBG_FUNC' => array(
'7.3' => true,
),
'FILTER_FLAG_SCHEME_REQUIRED' => array(
'7.3' => false,
),
'FILTER_FLAG_HOST_REQUIRED' => array(
'7.3' => false,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$constantName = $tokens[$stackPtr]['content'];
if (isset($this->removedConstants[$constantName]) === false) {
return;
}
if ($this->isUseOfGlobalConstant($phpcsFile, $stackPtr) === false) {
return;
}
$itemInfo = array(
'name' => $constantName,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->removedConstants[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The constant "%s" is ';
}
}
@@ -0,0 +1,223 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\DiscouragedSwitchContinue.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\ControlStructures\DiscouragedSwitchContinue.
*
* PHP 7.3 will throw a warning when continue is used to target a switch control structure.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class DiscouragedSwitchContinueSniff extends Sniff
{
/**
* Token codes of control structures which can be targeted using continue.
*
* @var array
*/
protected $loopStructures = array(
\T_FOR => \T_FOR,
\T_FOREACH => \T_FOREACH,
\T_WHILE => \T_WHILE,
\T_DO => \T_DO,
\T_SWITCH => \T_SWITCH,
);
/**
* Tokens which start a new case within a switch.
*
* @var array
*/
protected $caseTokens = array(
\T_CASE => \T_CASE,
\T_DEFAULT => \T_DEFAULT,
);
/**
* Token codes which are accepted to determine the level for the continue.
*
* This array is enriched with the arithmetic operators in the register() method.
*
* @var array
*/
protected $acceptedLevelTokens = array(
\T_LNUMBER => \T_LNUMBER,
\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
\T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$this->acceptedLevelTokens += Tokens::$arithmeticTokens;
$this->acceptedLevelTokens += Tokens::$emptyTokens;
return array(\T_SWITCH);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
return;
}
$switchOpener = $tokens[$stackPtr]['scope_opener'];
$switchCloser = $tokens[$stackPtr]['scope_closer'];
// Quick check whether we need to bother with the more complex logic.
$hasContinue = $phpcsFile->findNext(\T_CONTINUE, ($switchOpener + 1), $switchCloser);
if ($hasContinue === false) {
return;
}
$caseDefault = $switchOpener;
do {
$caseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
if ($caseDefault === false) {
break;
}
if (isset($tokens[$caseDefault]['scope_opener']) === false) {
// Unknown start of the case, skip.
continue;
}
$caseOpener = $tokens[$caseDefault]['scope_opener'];
$nextCaseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
if ($nextCaseDefault === false) {
$caseCloser = $switchCloser;
} else {
$caseCloser = $nextCaseDefault;
}
// Check for unscoped control structures within the case.
$controlStructure = $caseOpener;
$doCount = 0;
while (($controlStructure = $phpcsFile->findNext($this->loopStructures, ($controlStructure + 1), $caseCloser)) !== false) {
if ($tokens[$controlStructure]['code'] === \T_DO) {
$doCount++;
}
if (isset($tokens[$controlStructure]['scope_opener'], $tokens[$controlStructure]['scope_closer']) === false) {
if ($tokens[$controlStructure]['code'] === \T_WHILE && $doCount > 0) {
// While in a do-while construct.
$doCount--;
continue;
}
// Control structure without braces found within the case, ignore this case.
continue 2;
}
}
// Examine the contents of the case.
$continue = $caseOpener;
do {
$continue = $phpcsFile->findNext(\T_CONTINUE, ($continue + 1), $caseCloser);
if ($continue === false) {
break;
}
$nextSemicolon = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($continue + 1), $caseCloser);
$codeString = '';
for ($i = ($continue + 1); $i < $nextSemicolon; $i++) {
if (isset($this->acceptedLevelTokens[$tokens[$i]['code']]) === false) {
// Function call/variable or other token which make numeric level impossible to determine.
continue 2;
}
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
continue;
}
$codeString .= $tokens[$i]['content'];
}
$level = null;
if ($codeString !== '') {
if (is_numeric($codeString)) {
$level = (int) $codeString;
} else {
// With the above logic, the string can only contain digits and operators, eval!
$level = eval("return ( $codeString );");
}
}
if (isset($level) === false || $level === 0) {
$level = 1;
}
// Examine which control structure is being targeted by the continue statement.
if (isset($tokens[$continue]['conditions']) === false) {
continue;
}
$conditions = array_reverse($tokens[$continue]['conditions'], true);
// PHPCS adds more structures to the conditions array than we want to take into
// consideration, so clean up the array.
foreach ($conditions as $tokenPtr => $tokenCode) {
if (isset($this->loopStructures[$tokenCode]) === false) {
unset($conditions[$tokenPtr]);
}
}
$targetCondition = \array_slice($conditions, ($level - 1), 1, true);
if (empty($targetCondition)) {
continue;
}
$conditionToken = key($targetCondition);
if ($conditionToken === $stackPtr) {
$phpcsFile->addWarning(
"Targeting a 'switch' control structure with a 'continue' statement is strongly discouraged and will throw a warning as of PHP 7.3.",
$continue,
'Found'
);
}
} while ($continue < $caseCloser);
} while ($caseDefault < $switchCloser);
}
}
@@ -0,0 +1,109 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\ForbiddenBreakContinueOutsideLoop.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ControlStructures\ForbiddenBreakContinueOutsideLoop.
*
* Forbids use of break or continue statements outside of looping structures.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ForbiddenBreakContinueOutsideLoopSniff extends 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,
);
}
/**
* 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(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;
$errorCode = 'Found';
$data = array($token['content']);
if ($this->supportsAbove('7.0')) {
$error .= ' and will throw a fatal error since PHP 7.0';
$isError = true;
$errorCode = 'FatalError';
}
$this->addMessage($phpcsFile, $error, $stackPtr, $isError, $errorCode, $data);
}
}
@@ -0,0 +1,101 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\ForbiddenBreakContinueVariableArguments.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\ControlStructures\ForbiddenBreakContinueVariableArguments.
*
* Forbids variable arguments on break or continue statements.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class ForbiddenBreakContinueVariableArgumentsSniff extends Sniff
{
/**
* Error types this sniff handles for forbidden break/continue arguments.
*
* Array key is the error code. Array value will be used as part of the error message.
*
* @var array
*/
private $errorTypes = array(
'variableArgument' => 'a variable argument',
'zeroArgument' => '0 as an argument',
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_BREAK, T_CONTINUE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$nextSemicolonToken = $phpcsFile->findNext(array(T_SEMICOLON, T_CLOSE_TAG), ($stackPtr), null, false);
$errorType = '';
for ($curToken = $stackPtr + 1; $curToken < $nextSemicolonToken; $curToken++) {
if ($tokens[$curToken]['type'] === 'T_STRING') {
// If the next non-whitespace token after the string
// is an opening parenthesis then it's a function call.
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, $curToken + 1, null, true);
if ($tokens[$openBracket]['code'] === T_OPEN_PARENTHESIS) {
$errorType = 'variableArgument';
break;
}
} elseif (in_array($tokens[$curToken]['type'], array('T_VARIABLE', 'T_FUNCTION', 'T_CLOSURE'), true)) {
$errorType = 'variableArgument';
break;
} elseif ($tokens[$curToken]['type'] === 'T_LNUMBER' && $tokens[$curToken]['content'] === '0') {
$errorType = 'zeroArgument';
break;
}
}
if ($errorType !== '') {
$error = 'Using %s on break or continue is forbidden since PHP 5.4';
$errorCode = $errorType . 'Found';
$data = array($this->errorTypes[$errorType]);
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
}
}
@@ -0,0 +1,79 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\ForbiddenSwitchWithMultipleDefaultBlocksSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ControlStructures\ForbiddenSwitchWithMultipleDefaultBlocksSniff.
*
* Switch statements can not have multiple default blocks since PHP 7.0
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class ForbiddenSwitchWithMultipleDefaultBlocksSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_SWITCH);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
return;
}
$defaultToken = $stackPtr;
$defaultCount = 0;
$targetLevel = $tokens[$stackPtr]['level'] + 1;
while ($defaultCount < 2 && ($defaultToken = $phpcsFile->findNext(array(T_DEFAULT), $defaultToken + 1, $tokens[$stackPtr]['scope_closer'])) !== false) {
// Same level or one below (= two default cases after each other).
if ($tokens[$defaultToken]['level'] === $targetLevel || $tokens[$defaultToken]['level'] === ($targetLevel + 1)) {
$defaultCount++;
}
}
if ($defaultCount > 1) {
$phpcsFile->addError(
'Switch statements can not have multiple default blocks since PHP 7.0',
$stackPtr,
'Found'
);
}
}
}
@@ -0,0 +1,334 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\NewExecutionDirectivesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\ControlStructures\NewExecutionDirectivesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewExecutionDirectivesSniff extends AbstractNewFeatureSniff
{
/**
* A list of new execution directives
*
* The array lists : version number with false (not present) or true (present).
* If the execution order is conditional, add the condition as a string to the version nr.
* If's sufficient to list the first version where the execution directive appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newDirectives = array(
'ticks' => array(
'3.1' => false,
'4.0' => true,
'valid_value_callback' => 'isNumeric',
),
'encoding' => array(
'5.2' => false,
'5.3' => '--enable-zend-multibyte', // Directive ignored unless.
'5.4' => true,
'valid_value_callback' => 'validEncoding',
),
'strict_types' => array(
'5.6' => false,
'7.0' => true,
'valid_values' => array(1),
),
);
/**
* Tokens to ignore when trying to find the value for the directive.
*
* @var array
*/
protected $ignoreTokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$this->ignoreTokens = Tokens::$emptyTokens;
$this->ignoreTokens[T_EQUAL] = T_EQUAL;
return array(T_DECLARE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === true) {
$openParenthesis = $tokens[$stackPtr]['parenthesis_opener'];
$closeParenthesis = $tokens[$stackPtr]['parenthesis_closer'];
} else {
if (version_compare(PHPCSHelper::getVersion(), '2.3.4', '>=')) {
return;
}
// Deal with PHPCS 2.3.0-2.3.3 which do not yet set the parenthesis properly for declare statements.
$openParenthesis = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1), null, false, null, true);
if ($openParenthesis === false || isset($tokens[$openParenthesis]['parenthesis_closer']) === false) {
return;
}
$closeParenthesis = $tokens[$openParenthesis]['parenthesis_closer'];
}
$directivePtr = $phpcsFile->findNext(T_STRING, ($openParenthesis + 1), $closeParenthesis, false);
if ($directivePtr === false) {
return;
}
$directiveContent = $tokens[$directivePtr]['content'];
if (isset($this->newDirectives[$directiveContent]) === false) {
$error = 'Declare can only be used with the directives %s. Found: %s';
$data = array(
implode(', ', array_keys($this->newDirectives)),
$directiveContent,
);
$phpcsFile->addError($error, $stackPtr, 'InvalidDirectiveFound', $data);
} else {
// Check for valid directive for version.
$itemInfo = array(
'name' => $directiveContent,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
// Check for valid directive value.
$valuePtr = $phpcsFile->findNext($this->ignoreTokens, $directivePtr + 1, $closeParenthesis, true);
if ($valuePtr === false) {
return;
}
$this->addWarningOnInvalidValue($phpcsFile, $valuePtr, $directiveContent);
}
}
/**
* Determine whether an error/warning should be thrown for an item based on collected information.
*
* @param array $errorInfo Detail information about an item.
*
* @return bool
*/
protected function shouldThrowError(array $errorInfo)
{
return ($errorInfo['not_in_version'] !== '' || $errorInfo['conditional_version'] !== '');
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newDirectives[$itemInfo['name']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array(
'valid_value_callback',
'valid_values',
);
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['conditional_version'] = '';
$errorInfo['condition'] = '';
$versionArray = $this->getVersionArray($itemArray);
if (empty($versionArray) === false) {
foreach ($versionArray as $version => $present) {
if (is_string($present) === true && $this->supportsBelow($version) === true) {
// We cannot test for compilation option (ok, except by scraping the output of phpinfo...).
$errorInfo['conditional_version'] = $version;
$errorInfo['condition'] = $present;
}
}
}
return $errorInfo;
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'Directive ' . parent::getErrorMsgTemplate();
}
/**
* Generates the error or warning for this item.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the relevant token in
* the stack.
* @param array $itemInfo Base information about the item.
* @param array $errorInfo Array with detail (version) information
* relevant to the item.
*
* @return void
*/
public function addError(File $phpcsFile, $stackPtr, array $itemInfo, array $errorInfo)
{
if ($errorInfo['not_in_version'] !== '') {
parent::addError($phpcsFile, $stackPtr, $itemInfo, $errorInfo);
} elseif ($errorInfo['conditional_version'] !== '') {
$error = 'Directive %s is present in PHP version %s but will be disregarded unless PHP is compiled with %s';
$errorCode = $this->stringToErrorCode($itemInfo['name']) . 'WithConditionFound';
$data = array(
$itemInfo['name'],
$errorInfo['conditional_version'],
$errorInfo['condition'],
);
$phpcsFile->addWarning($error, $stackPtr, $errorCode, $data);
}
}
/**
* Generates a error or warning for this sniff.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the execution directive value
* in the token array.
* @param string $directive The directive.
*
* @return void
*/
protected function addWarningOnInvalidValue(File $phpcsFile, $stackPtr, $directive)
{
$tokens = $phpcsFile->getTokens();
$value = $tokens[$stackPtr]['content'];
if (isset(Tokens::$stringTokens[$tokens[$stackPtr]['code']]) === true) {
$value = $this->stripQuotes($value);
}
$isError = false;
if (isset($this->newDirectives[$directive]['valid_values'])) {
if (in_array($value, $this->newDirectives[$directive]['valid_values']) === false) {
$isError = true;
}
} elseif (isset($this->newDirectives[$directive]['valid_value_callback'])) {
$valid = call_user_func(array($this, $this->newDirectives[$directive]['valid_value_callback']), $value);
if ($valid === false) {
$isError = true;
}
}
if ($isError === true) {
$error = 'The execution directive %s does not seem to have a valid value. Please review. Found: %s';
$errorCode = $this->stringToErrorCode($directive) . 'InvalidValueFound';
$data = array(
$directive,
$value,
);
$phpcsFile->addWarning($error, $stackPtr, $errorCode, $data);
}
}
/**
* Check whether a value is numeric.
*
* Callback function to test whether the value for an execution directive is valid.
*
* @param mixed $value The value to test.
*
* @return bool
*/
protected function isNumeric($value)
{
return is_numeric($value);
}
/**
* Check whether a value is a valid encoding.
*
* Callback function to test whether the value for an execution directive is valid.
*
* @param mixed $value The value to test.
*
* @return bool
*/
protected function validEncoding($value)
{
static $encodings;
if (isset($encodings) === false && function_exists('mb_list_encodings')) {
$encodings = mb_list_encodings();
}
if (empty($encodings) || is_array($encodings) === false) {
// If we can't test the encoding, let it pass through.
return true;
}
return in_array($value, $encodings, true);
}
}
@@ -0,0 +1,96 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\NewForeachExpressionReferencingSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* New `foreach` Expression Referencing.
*
* Before PHP 5.5.0, referencing $value is only possible if the iterated array
* can be referenced (i.e. if it is a variable).
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewForeachExpressionReferencingSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_FOREACH);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
return;
}
$opener = $tokens[$stackPtr]['parenthesis_opener'];
$closer = $tokens[$stackPtr]['parenthesis_closer'];
$asToken = $phpcsFile->findNext(T_AS, ($opener + 1), $closer);
if ($asToken === false) {
return;
}
/*
* Note: referencing $key is not allowed in any version, so this should only find referenced $values.
* If it does find a referenced key, it would be a parse error anyway.
*/
$hasReference = $phpcsFile->findNext(T_BITWISE_AND, ($asToken + 1), $closer);
if ($hasReference === false) {
return;
}
$nestingLevel = 0;
if ($asToken !== ($opener + 1) && isset($tokens[$opener + 1]['nested_parenthesis'])) {
$nestingLevel = count($tokens[$opener + 1]['nested_parenthesis']);
}
if ($this->isVariable($phpcsFile, ($opener + 1), $asToken, $nestingLevel) === true) {
return;
}
// Non-variable detected before the `as` keyword.
$phpcsFile->addError(
'Referencing $value is only possible if the iterated array is a variable in PHP 5.4 or earlier.',
$hasReference,
'Found'
);
}
}
@@ -0,0 +1,79 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\NewListInForeachSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* Detect unpacking nested arrays with list() in a foreach().
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewListInForeachSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_FOREACH);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
return;
}
$opener = $tokens[$stackPtr]['parenthesis_opener'];
$closer = $tokens[$stackPtr]['parenthesis_closer'];
$asToken = $phpcsFile->findNext(T_AS, ($opener + 1), $closer);
if ($asToken === false) {
return;
}
$hasList = $phpcsFile->findNext(array(T_LIST, T_OPEN_SHORT_ARRAY), ($asToken + 1), $closer);
if ($hasList === false) {
return;
}
$phpcsFile->addError(
'Unpacking nested arrays with list() in a foreach is not supported in PHP 5.4 or earlier.',
$hasList,
'Found'
);
}
}
@@ -0,0 +1,75 @@
<?php
/**
* \PHPCompatibility\Sniffs\ControlStructures\NewMultiCatch.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ControlStructures;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ControlStructures\NewMultiCatch.
*
* Catching multiple exception types in one statement is available since PHP 7.1.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewMultiCatchSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CATCH);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Bow out during live coding.
if (isset($token['parenthesis_opener'], $token['parenthesis_closer']) === false) {
return;
}
$hasBitwiseOr = $phpcsFile->findNext(T_BITWISE_OR, $token['parenthesis_opener'], $token['parenthesis_closer']);
if ($hasBitwiseOr === false) {
return;
}
$phpcsFile->addError(
'Catching multiple exceptions within one statement is not supported in PHP 7.0 or earlier.',
$hasBitwiseOr,
'Found'
);
}
}
@@ -0,0 +1,309 @@
<?php
/**
* \PHPCompatibility\Sniffs\Extensions\RemovedExtensionsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Extensions;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Extensions\RemovedExtensionsSniff.
*
* Discourages the use of removed extensions. Suggests alternative extensions if available
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class RemovedExtensionsSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of functions to whitelist, if any.
*
* This is intended for projects using functions which start with the same
* prefix as one of the removed extensions.
*
* This property can be set from the ruleset, like so:
* <rule ref="PHPCompatibility.Extensions.RemovedExtensions">
* <properties>
* <property name="functionWhitelist" type="array" value="mysql_to_rfc3339,mysql_another_function" />
* </properties>
* </rule>
*
* @var array
*/
public $functionWhitelist;
/**
* A list of removed extensions with their alternative, if any
*
* The array lists : version number with false (deprecated) and true (removed).
* If's sufficient to list the first version where the extension was deprecated/removed.
*
* @var array(string|null)
*/
protected $removedExtensions = array(
'activescript' => array(
'5.1' => true,
'alternative' => 'pecl/activescript',
),
'cpdf' => array(
'5.1' => true,
'alternative' => 'pecl/pdflib',
),
'dbase' => array(
'5.3' => true,
'alternative' => null,
),
'dbx' => array(
'5.1' => true,
'alternative' => 'pecl/dbx',
),
'dio' => array(
'5.1' => true,
'alternative' => 'pecl/dio',
),
'ereg' => array(
'5.3' => false,
'7.0' => true,
'alternative' => 'pcre',
),
'fam' => array(
'5.1' => true,
'alternative' => null,
),
'fbsql' => array(
'5.3' => true,
'alternative' => null,
),
'fdf' => array(
'5.3' => true,
'alternative' => 'pecl/fdf',
),
'filepro' => array(
'5.2' => true,
'alternative' => null,
),
'hw_api' => array(
'5.2' => true,
'alternative' => null,
),
'ingres' => array(
'5.1' => true,
'alternative' => 'pecl/ingres',
),
'ircg' => array(
'5.1' => true,
'alternative' => null,
),
'mcrypt' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'openssl (preferred) or pecl/mcrypt once available',
),
'mcve' => array(
'5.1' => true,
'alternative' => 'pecl/mvce',
),
'ming' => array(
'5.3' => true,
'alternative' => 'pecl/ming',
),
'mnogosearch' => array(
'5.1' => true,
'alternative' => null,
),
'msql' => array(
'5.3' => true,
'alternative' => null,
),
'mssql' => array(
'7.0' => true,
'alternative' => null,
),
'mysql_' => array(
'5.5' => false,
'7.0' => true,
'alternative' => 'mysqli',
),
'ncurses' => array(
'5.3' => true,
'alternative' => 'pecl/ncurses',
),
'oracle' => array(
'5.1' => true,
'alternative' => 'oci8 or pdo_oci',
),
'ovrimos' => array(
'5.1' => true,
'alternative' => null,
),
'pfpro' => array(
'5.3' => true,
'alternative' => null,
),
'sqlite' => array(
'5.4' => true,
'alternative' => null,
),
// Has to be before `sybase` as otherwise it will never match.
'sybase_ct' => array(
'7.0' => true,
'alternative' => null,
),
'sybase' => array(
'5.3' => true,
'alternative' => 'sybase_ct',
),
'w32api' => array(
'5.1' => true,
'alternative' => 'pecl/ffi',
),
'yp' => array(
'5.1' => true,
'alternative' => null,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of function names.
$this->removedExtensions = $this->arrayKeysToLowercase($this->removedExtensions);
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Find the next non-empty token.
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) {
// Not a function call.
return;
}
if (isset($tokens[$openBracket]['parenthesis_closer']) === false) {
// Not a function call.
return;
}
// Find the previous non-empty token.
$search = Tokens::$emptyTokens;
$search[] = T_BITWISE_AND;
$previous = $phpcsFile->findPrevious($search, ($stackPtr - 1), null, true);
if ($tokens[$previous]['code'] === T_FUNCTION) {
// It's a function definition, not a function call.
return;
}
if ($tokens[$previous]['code'] === T_NEW) {
// We are creating an object, not calling a function.
return;
}
if ($tokens[$previous]['code'] === T_OBJECT_OPERATOR) {
// We are calling a method of an object.
return;
}
$function = $tokens[$stackPtr]['content'];
$functionLc = strtolower($function);
if ($this->isWhiteListed($functionLc) === true) {
// Function is whitelisted.
return;
}
foreach ($this->removedExtensions as $extension => $versionList) {
if (strpos($functionLc, $extension) === 0) {
$itemInfo = array(
'name' => $extension,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
break;
}
}
}
/**
* Is the current function being checked whitelisted ?
*
* Parsing the list late as it may be provided as a property, but also inline.
*
* @param string $content Content of the current token.
*
* @return bool
*/
protected function isWhiteListed($content)
{
if (isset($this->functionWhitelist) === false) {
return false;
}
if (is_string($this->functionWhitelist) === true) {
if (strpos($this->functionWhitelist, ',') !== false) {
$this->functionWhitelist = explode(',', $this->functionWhitelist);
} else {
$this->functionWhitelist = (array) $this->functionWhitelist;
}
}
if (is_array($this->functionWhitelist) === true) {
$this->functionWhitelist = array_map('strtolower', $this->functionWhitelist);
return in_array($content, $this->functionWhitelist, true);
}
return false;
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->removedExtensions[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return "Extension '%s' is ";
}
}
@@ -0,0 +1,79 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\ForbiddenParameterShadowSuperGlobalsSniff
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Declan Kelly <declankelly90@gmail.com>
* @copyright 2015 Declan Kelly
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\ForbiddenParameterShadowSuperGlobalsSniff
*
* Discourages use of superglobals as parameters for functions.
*
* {@internal List of superglobals is maintained in the parent class.}}
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Declan Kelly <declankelly90@gmail.com>
* @copyright 2015 Declan Kelly
*/
class ForbiddenParameterShadowSuperGlobalsSniff extends Sniff
{
/**
* Register the tokens to listen for.
*
* @return array
*/
public function register()
{
return array(
T_FUNCTION,
T_CLOSURE,
);
}
/**
* Processes the test.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('5.4') === false) {
return;
}
// Get all parameters from function signature.
$parameters = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($parameters) || is_array($parameters) === false) {
return;
}
foreach ($parameters as $param) {
if (isset($this->superglobals[$param['name']]) === true) {
$error = 'Parameter shadowing super global (%s) causes fatal error since PHP 5.4';
$errorCode = $this->stringToErrorCode(substr($param['name'], 1)) . 'Found';
$data = array($param['name']);
$phpcsFile->addError($error, $param['token'], $errorCode, $data);
}
}
}
}
@@ -0,0 +1,86 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\ForbiddenParametersWithSameName.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\ForbiddenParametersWithSameName.
*
* Functions can not have multiple parameters with the same name since PHP 7.0
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class ForbiddenParametersWithSameNameSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_FUNCTION,
T_CLOSURE,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Skip function without body.
if (isset($token['scope_opener']) === false) {
return;
}
// Get all parameters from method signature.
$parameters = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($parameters) || is_array($parameters) === false) {
return;
}
$paramNames = array();
foreach ($parameters as $param) {
$paramNames[] = strtolower($param['name']);
}
if (count($paramNames) !== count(array_unique($paramNames))) {
$phpcsFile->addError(
'Functions can not have multiple parameters with the same name since PHP 7.0',
$stackPtr,
'Found'
);
}
}
}
@@ -0,0 +1,118 @@
<?php
/**
* PHP 7.1 Forbidden variable names in closure use statements.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* PHP 7.1 Forbidden variable names in closure use statements.
*
* Variables bound to a closure via the use construct cannot use the same name
* as any superglobals, $this, or any parameter since PHP 7.1.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ForbiddenVariableNamesInClosureUseSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_USE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.1') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
// Verify this use statement is used with a closure - if so, it has to have parenthesis before it.
$previousNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true, null, true);
if ($previousNonEmpty === false || $tokens[$previousNonEmpty]['code'] !== T_CLOSE_PARENTHESIS
|| isset($tokens[$previousNonEmpty]['parenthesis_opener']) === false
) {
return;
}
// ... and (a variable within) parenthesis after it.
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) {
// Live coding.
return;
}
$closurePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($tokens[$previousNonEmpty]['parenthesis_opener'] - 1), null, true);
if ($closurePtr === false || $tokens[$closurePtr]['code'] !== T_CLOSURE) {
return;
}
// Get the parameters declared by the closure.
$closureParams = PHPCSHelper::getMethodParameters($phpcsFile, $closurePtr);
$errorMsg = 'Variables bound to a closure via the use construct cannot use the same name as superglobals, $this, or a declared parameter since PHP 7.1. Found: %s';
for ($i = ($nextNonEmpty + 1); $i < $tokens[$nextNonEmpty]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE) {
continue;
}
$variableName = $tokens[$i]['content'];
if ($variableName === '$this') {
$phpcsFile->addError($errorMsg, $i, 'FoundThis', array($variableName));
continue;
}
if (isset($this->superglobals[$variableName]) === true) {
$phpcsFile->addError($errorMsg, $i, 'FoundSuperglobal', array($variableName));
continue;
}
// Check whether it is one of the parameters declared by the closure.
if (empty($closureParams) === false) {
foreach ($closureParams as $param) {
if ($param['name'] === $variableName) {
$phpcsFile->addError($errorMsg, $i, 'FoundShadowParam', array($variableName));
continue 2;
}
}
}
}
}
}
@@ -0,0 +1,237 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewClosure.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewClosure.
*
* Closures are available since PHP 5.3
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class NewClosureSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CLOSURE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.2')) {
$phpcsFile->addError(
'Closures / anonymous functions are not available in PHP 5.2 or earlier',
$stackPtr,
'Found'
);
}
/*
* Closures can only be declared as static since PHP 5.4.
*/
$isStatic = $this->isClosureStatic($phpcsFile, $stackPtr);
if ($this->supportsBelow('5.3') && $isStatic === true) {
$phpcsFile->addError(
'Closures / anonymous functions could not be declared as static in PHP 5.3 or earlier',
$stackPtr,
'StaticFound'
);
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
// Live coding or parse error.
return;
}
$scopeStart = ($tokens[$stackPtr]['scope_opener'] + 1);
$scopeEnd = $tokens[$stackPtr]['scope_closer'];
$usesThis = $this->findThisUsageInClosure($phpcsFile, $scopeStart, $scopeEnd);
if ($this->supportsBelow('5.3')) {
/*
* Closures declared within classes only have access to $this since PHP 5.4.
*/
if ($usesThis !== false) {
$thisFound = $usesThis;
do {
$phpcsFile->addError(
'Closures / anonymous functions did not have access to $this in PHP 5.3 or earlier',
$thisFound,
'ThisFound'
);
$thisFound = $this->findThisUsageInClosure($phpcsFile, ($thisFound + 1), $scopeEnd);
} while ($thisFound !== false);
}
/*
* Closures declared within classes only have access to self/parent/static since PHP 5.4.
*/
$usesClassRef = $this->findClassRefUsageInClosure($phpcsFile, $scopeStart, $scopeEnd);
if ($usesClassRef !== false) {
do {
$phpcsFile->addError(
'Closures / anonymous functions could not use "%s::" in PHP 5.3 or earlier',
$usesClassRef,
'ClassRefFound',
array(strtolower($tokens[$usesClassRef]['content']))
);
$usesClassRef = $this->findClassRefUsageInClosure($phpcsFile, ($usesClassRef + 1), $scopeEnd);
} while ($usesClassRef !== false);
}
}
/*
* Check for correct usage.
*/
if ($this->supportsAbove('5.4') && $usesThis !== false) {
$thisFound = $usesThis;
do {
/*
* Closures only have access to $this if not declared as static.
*/
if ($isStatic === true) {
$phpcsFile->addError(
'Closures / anonymous functions declared as static do not have access to $this',
$thisFound,
'ThisFoundInStatic'
);
}
/*
* Closures only have access to $this if used within a class context.
*/
elseif ($this->inClassScope($phpcsFile, $stackPtr, false) === false) {
$phpcsFile->addWarning(
'Closures / anonymous functions only have access to $this if used within a class or when bound to an object using bindTo(). Please verify.',
$thisFound,
'ThisFoundOutsideClass'
);
}
$thisFound = $this->findThisUsageInClosure($phpcsFile, ($thisFound + 1), $scopeEnd);
} while ($thisFound !== false);
}
// Prevent double reporting for nested closures.
return $scopeEnd;
}
/**
* Check whether the closure is declared as static.
*
* @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 bool
*/
protected function isClosureStatic(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true, null, true);
return ($prevToken !== false && $tokens[$prevToken]['code'] === T_STATIC);
}
/**
* Check if the code within a closure uses the $this variable.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $startToken The position within the closure to continue searching from.
* @param int $endToken The closure scope closer to stop searching at.
*
* @return int|false The stackPtr to the first $this usage if found or false if
* $this is not used.
*/
protected function findThisUsageInClosure(File $phpcsFile, $startToken, $endToken)
{
// Make sure the $startToken is valid.
if ($startToken >= $endToken) {
return false;
}
return $phpcsFile->findNext(
T_VARIABLE,
$startToken,
$endToken,
false,
'$this'
);
}
/**
* Check if the code within a closure uses "self/parent/static".
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $startToken The position within the closure to continue searching from.
* @param int $endToken The closure scope closer to stop searching at.
*
* @return int|false The stackPtr to the first classRef usage if found or false if
* they are not used.
*/
protected function findClassRefUsageInClosure(File $phpcsFile, $startToken, $endToken)
{
// Make sure the $startToken is valid.
if ($startToken >= $endToken) {
return false;
}
$tokens = $phpcsFile->getTokens();
$classRef = $phpcsFile->findNext(array(T_SELF, T_PARENT, T_STATIC), $startToken, $endToken);
if ($classRef === false || $tokens[$classRef]['code'] !== T_STATIC) {
return $classRef;
}
// T_STATIC, make sure it is used as a class reference.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($classRef + 1), $endToken, true);
if ($next === false || $tokens[$next]['code'] !== T_DOUBLE_COLON) {
return false;
}
return $classRef;
}
}
@@ -0,0 +1,162 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewNullableTypes.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewNullableTypes.
*
* Nullable type hints and return types are available since PHP 7.1.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewNullableTypesSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* {@internal Not sniffing for T_NULLABLE which was introduced in PHPCS 2.7.2
* as in that case we can't distinguish between parameter type hints and
* return type hints for the error message.}}
*
* @return array
*/
public function register()
{
$tokens = array(
T_FUNCTION,
T_CLOSURE,
);
if (defined('T_RETURN_TYPE')) {
$tokens[] = T_RETURN_TYPE;
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$tokenCode = $tokens[$stackPtr]['code'];
if ($tokenCode === T_FUNCTION || $tokenCode === T_CLOSURE) {
$this->processFunctionDeclaration($phpcsFile, $stackPtr);
// Deal with older PHPCS version which don't recognize return type hints
// as well as newer PHPCS versions (3.3.0+) where the tokenization has changed.
$returnTypeHint = $this->getReturnTypeHintToken($phpcsFile, $stackPtr);
if ($returnTypeHint !== false) {
$this->processReturnType($phpcsFile, $returnTypeHint);
}
} else {
$this->processReturnType($phpcsFile, $stackPtr);
}
}
/**
* Process this test for function tokens.
*
* @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
*/
protected function processFunctionDeclaration(File $phpcsFile, $stackPtr)
{
$params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($params) === false && is_array($params)) {
foreach ($params as $param) {
if ($param['nullable_type'] === true) {
$phpcsFile->addError(
'Nullable type declarations are not supported in PHP 7.0 or earlier. Found: %s',
$param['token'],
'typeDeclarationFound',
array($param['type_hint'])
);
}
}
}
}
/**
* Process this test for return type tokens.
*
* @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
*/
protected function processReturnType(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[($stackPtr - 1)]['code']) === false) {
return;
}
$previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
// Deal with namespaced class names.
if ($tokens[$previous]['code'] === T_NS_SEPARATOR) {
$validTokens = Tokens::$emptyTokens;
$validTokens[T_STRING] = true;
$validTokens[T_NS_SEPARATOR] = true;
$stackPtr--;
while (isset($validTokens[$tokens[($stackPtr - 1)]['code']]) === true) {
$stackPtr--;
}
$previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
}
// T_NULLABLE token was introduced in PHPCS 2.7.2. Before that it identified as T_INLINE_THEN.
if ((defined('T_NULLABLE') === true && $tokens[$previous]['type'] === 'T_NULLABLE')
|| (defined('T_NULLABLE') === false && $tokens[$previous]['code'] === T_INLINE_THEN)
) {
$phpcsFile->addError(
'Nullable return types are not supported in PHP 7.0 or earlier.',
$stackPtr,
'returnTypeFound'
);
}
}
}
@@ -0,0 +1,195 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewParamTypeDeclarationsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewParamTypeDeclarationsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class NewParamTypeDeclarationsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new types.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the keyword appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newTypes = array(
'array' => array(
'5.0' => false,
'5.1' => true,
),
'self' => array(
'5.1' => false,
'5.2' => true,
),
'parent' => array(
'5.1' => false,
'5.2' => true,
),
'callable' => array(
'5.3' => false,
'5.4' => true,
),
'int' => array(
'5.6' => false,
'7.0' => true,
),
'float' => array(
'5.6' => false,
'7.0' => true,
),
'bool' => array(
'5.6' => false,
'7.0' => true,
),
'string' => array(
'5.6' => false,
'7.0' => true,
),
'iterable' => array(
'7.0' => false,
'7.1' => true,
),
'object' => array(
'7.1' => false,
'7.2' => true,
),
);
/**
* Invalid types
*
* The array lists : the invalid type hint => what was probably intended/alternative.
*
* @var array(string => string)
*/
protected $invalidTypes = array(
'static' => 'self',
'boolean' => 'bool',
'integer' => 'int',
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_FUNCTION,
T_CLOSURE,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
// Get all parameters from method signature.
$paramNames = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($paramNames)) {
return;
}
$supportsPHP4 = $this->supportsBelow('4.4');
foreach ($paramNames as $param) {
if ($param['type_hint'] === '') {
continue;
}
// Strip off potential nullable indication.
$typeHint = ltrim($param['type_hint'], '?');
if ($supportsPHP4 === true) {
$phpcsFile->addError(
'Type declarations were not present in PHP 4.4 or earlier.',
$param['token'],
'TypeHintFound'
);
} elseif (isset($this->newTypes[$typeHint])) {
$itemInfo = array(
'name' => $typeHint,
);
$this->handleFeature($phpcsFile, $param['token'], $itemInfo);
// As of PHP 7.0, using `self` or `parent` outside class scope throws a fatal error.
// Only throw this error for PHP 5.2+ as before that the "type hint not supported" error
// will be thrown.
if (($typeHint === 'self' || $typeHint === 'parent')
&& $this->inClassScope($phpcsFile, $stackPtr, false) === false
&& $this->supportsAbove('5.2') !== false
) {
$phpcsFile->addError(
"'%s' type cannot be used outside of class scope",
$param['token'],
ucfirst($typeHint) . 'OutsideClassScopeFound',
array($typeHint)
);
}
} elseif (isset($this->invalidTypes[$typeHint])) {
$error = "'%s' is not a valid type declaration. Did you mean %s ?";
$data = array(
$typeHint,
$this->invalidTypes[$typeHint],
);
$phpcsFile->addError($error, $param['token'], 'InvalidTypeHintFound', $data);
}
}
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newTypes[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return "'%s' type declaration is not present in PHP version %s or earlier";
}
}
@@ -0,0 +1,173 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewReturnTypeDeclarationsSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NewReturnTypeDeclarationsSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class NewReturnTypeDeclarationsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new types
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the keyword appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newTypes = array(
'int' => array(
'5.6' => false,
'7.0' => true,
),
'float' => array(
'5.6' => false,
'7.0' => true,
),
'bool' => array(
'5.6' => false,
'7.0' => true,
),
'string' => array(
'5.6' => false,
'7.0' => true,
),
'array' => array(
'5.6' => false,
'7.0' => true,
),
'callable' => array(
'5.6' => false,
'7.0' => true,
),
'parent' => array(
'5.6' => false,
'7.0' => true,
),
'self' => array(
'5.6' => false,
'7.0' => true,
),
'Class name' => array(
'5.6' => false,
'7.0' => true,
),
'iterable' => array(
'7.0' => false,
'7.1' => true,
),
'void' => array(
'7.0' => false,
'7.1' => true,
),
'object' => array(
'7.1' => false,
'7.2' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array(
T_FUNCTION,
T_CLOSURE,
);
if (defined('T_RETURN_TYPE')) {
$tokens[] = T_RETURN_TYPE;
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Deal with older PHPCS version which don't recognize return type hints
// as well as newer PHPCS versions (3.3.0+) where the tokenization has changed.
if ($tokens[$stackPtr]['code'] === T_FUNCTION || $tokens[$stackPtr]['code'] === T_CLOSURE) {
$returnTypeHint = $this->getReturnTypeHintToken($phpcsFile, $stackPtr);
if ($returnTypeHint !== false) {
$stackPtr = $returnTypeHint;
}
}
if (isset($this->newTypes[$tokens[$stackPtr]['content']]) === true) {
$itemInfo = array(
'name' => $tokens[$stackPtr]['content'],
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
// Handle class name based return types.
elseif ($tokens[$stackPtr]['code'] === T_STRING
|| (defined('T_RETURN_TYPE') && $tokens[$stackPtr]['code'] === T_RETURN_TYPE)
) {
$itemInfo = array(
'name' => 'Class name',
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newTypes[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return '%s return type is not present in PHP version %s or earlier';
}
}
@@ -0,0 +1,178 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NonStaticMagicMethodsSniff.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\FunctionDeclarations;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionDeclarations\NonStaticMagicMethodsSniff.
*
* Verifies the use of the correct visibility and static properties of magic methods.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class NonStaticMagicMethodsSniff extends Sniff
{
/**
* A list of PHP magic methods and their visibility and static requirements.
*
* Method names in the array should be all *lowercase*.
* Visibility can be either 'public', 'protected' or 'private'.
* Static can be either 'true' - *must* be static, or 'false' - *must* be non-static.
* When a method does not have a specific requirement for either visibility or static,
* do *not* add the key.
*
* @var array(string)
*/
protected $magicMethods = array(
'__get' => array(
'visibility' => 'public',
'static' => false,
),
'__set' => array(
'visibility' => 'public',
'static' => false,
),
'__isset' => array(
'visibility' => 'public',
'static' => false,
),
'__unset' => array(
'visibility' => 'public',
'static' => false,
),
'__call' => array(
'visibility' => 'public',
'static' => false,
),
'__callstatic' => array(
'visibility' => 'public',
'static' => true,
),
'__sleep' => array(
'visibility' => 'public',
),
'__tostring' => array(
'visibility' => 'public',
),
'__set_state' => array(
'static' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$targets = array(
T_CLASS,
T_INTERFACE,
T_TRAIT,
);
if (defined('T_ANON_CLASS')) {
$targets[] = T_ANON_CLASS;
}
return $targets;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
// Should be removed, the requirement was previously also there, 5.3 just started throwing a warning about it.
if ($this->supportsAbove('5.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
return;
}
$classScopeCloser = $tokens[$stackPtr]['scope_closer'];
$functionPtr = $stackPtr;
// Find all the functions in this class or interface.
while (($functionToken = $phpcsFile->findNext(T_FUNCTION, $functionPtr, $classScopeCloser)) !== false) {
/*
* Get the scope closer for this function in order to know how
* to advance to the next function.
* If no body of function (e.g. for interfaces), there is
* no closing curly brace; advance the pointer differently.
*/
if (isset($tokens[$functionToken]['scope_closer'])) {
$scopeCloser = $tokens[$functionToken]['scope_closer'];
} else {
$scopeCloser = ($functionToken + 1);
}
$methodName = $phpcsFile->getDeclarationName($functionToken);
$methodNameLc = strtolower($methodName);
if (isset($this->magicMethods[$methodNameLc]) === false) {
$functionPtr = $scopeCloser;
continue;
}
$methodProperties = $phpcsFile->getMethodProperties($functionToken);
$errorCodeBase = $this->stringToErrorCode($methodNameLc);
if (isset($this->magicMethods[$methodNameLc]['visibility']) && $this->magicMethods[$methodNameLc]['visibility'] !== $methodProperties['scope']) {
$error = 'Visibility for magic method %s must be %s. Found: %s';
$errorCode = $errorCodeBase . 'MethodVisibility';
$data = array(
$methodName,
$this->magicMethods[$methodNameLc]['visibility'],
$methodProperties['scope'],
);
$phpcsFile->addError($error, $functionToken, $errorCode, $data);
}
if (isset($this->magicMethods[$methodNameLc]['static']) && $this->magicMethods[$methodNameLc]['static'] !== $methodProperties['is_static']) {
$error = 'Magic method %s cannot be defined as static.';
$errorCode = $errorCodeBase . 'MethodStatic';
$data = array($methodName);
if ($this->magicMethods[$methodNameLc]['static'] === true) {
$error = 'Magic method %s must be defined as static.';
$errorCode = $errorCodeBase . 'MethodNonStatic';
}
$phpcsFile->addError($error, $functionToken, $errorCode, $data);
}
// Advance to next function.
$functionPtr = $scopeCloser;
}
}
}
@@ -0,0 +1,194 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\NewMagicMethodsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionNameRestrictions;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\NewMagicMethodsSniff.
*
* Warns for non-magic behaviour of magic methods prior to becoming magic.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewMagicMethodsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new magic methods, not considered magic in older versions.
*
* Method names in the array should be all *lowercase*.
* The array lists : version number with false (not magic) or true (magic).
* If's sufficient to list the first version where the method became magic.
*
* @var array(string => array(string => int|string|null))
*/
protected $newMagicMethods = array(
'__get' => array(
'4.4' => false,
'5.0' => true,
),
'__isset' => array(
'5.0' => false,
'5.1' => true,
),
'__unset' => array(
'5.0' => false,
'5.1' => true,
),
'__set_state' => array(
'5.0' => false,
'5.1' => true,
),
'__callstatic' => array(
'5.2' => false,
'5.3' => true,
),
'__invoke' => array(
'5.2' => false,
'5.3' => true,
),
'__debuginfo' => array(
'5.5' => false,
'5.6' => true,
),
// Special case - only became properly magical in 5.2.0,
// before that it was only called for echo and print.
'__tostring' => array(
'5.1' => false,
'5.2' => true,
'message' => 'The method %s() was not truly magical in PHP version %s and earlier. The associated magic functionality will only be called when directly combined with echo or print.',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_FUNCTION);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$functionName = $phpcsFile->getDeclarationName($stackPtr);
$functionNameLc = strtolower($functionName);
if (isset($this->newMagicMethods[$functionNameLc]) === false) {
return;
}
if ($this->inClassScope($phpcsFile, $stackPtr, false) === false) {
return;
}
$itemInfo = array(
'name' => $functionName,
'nameLc' => $functionNameLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newMagicMethods[$itemInfo['nameLc']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('message');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['error'] = false; // Warning, not error.
$errorInfo['message'] = '';
if (empty($itemArray['message']) === false) {
$errorInfo['message'] = $itemArray['message'];
}
return $errorInfo;
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The method %s() was not magical in PHP version %s and earlier. The associated magic functionality will not be invoked.';
}
/**
* Allow for concrete child classes to filter the error message before it's passed to PHPCS.
*
* @param string $error The error message which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return string
*/
protected function filterErrorMsg($error, array $itemInfo, array $errorInfo)
{
if ($errorInfo['message'] !== '') {
$error = $errorInfo['message'];
}
return $error;
}
}
@@ -0,0 +1,80 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\RemovedMagicAutoloadSniff.
*
* PHP version 7.2
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionNameRestrictions;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\RemovedMagicAutoloadSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class RemovedMagicAutoloadSniff extends Sniff
{
/**
* Scopes to look for when testing using validDirectScope
*
* @var array
*/
private $checkForScopes = array(
'T_CLASS' => true,
'T_ANON_CLASS' => true,
'T_INTERFACE' => true,
'T_TRAIT' => true,
'T_NAMESPACE' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_FUNCTION);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.2') === false) {
return;
}
$funcName = $phpcsFile->getDeclarationName($stackPtr);
if (strtolower($funcName) !== '__autoload') {
return;
}
if ($this->validDirectScope($phpcsFile, $stackPtr, $this->checkForScopes) !== false) {
return;
}
if ($this->determineNamespace($phpcsFile, $stackPtr) !== '') {
return;
}
$phpcsFile->addWarning('Use of __autoload() function is deprecated since PHP 7.2', $stackPtr, 'Found');
}
}
@@ -0,0 +1,92 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\RemovedNamespacedAssertSniff.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionNameRestrictions;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* Removed Namespaced Assert.
*
* As of PHP 7.3, a compile-time deprecation warning will be thrown when a function
* called `assert()` is declared. In PHP 8 this will become a compile-error.
*
* Methods are unaffected.
* Global, non-namespaced, assert() function declarations were always a fatal
* "function already declared" error, so not the concern of this sniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedNamespacedAssertSniff extends Sniff
{
/**
* Scopes in which an `assert` function can be declared without issue.
*
* @var array
*/
private $scopes = array(
T_CLASS,
T_INTERFACE,
T_TRAIT,
T_CLOSURE,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Enrich the scopes list.
if (defined('T_ANON_CLASS')) {
$this->scopes[] = T_ANON_CLASS;
}
return array(T_FUNCTION);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.3') === false) {
return;
}
$funcName = $phpcsFile->getDeclarationName($stackPtr);
if (strtolower($funcName) !== 'assert') {
return;
}
if ($phpcsFile->hasCondition($stackPtr, $this->scopes) === true) {
return;
}
if ($this->determineNamespace($phpcsFile, $stackPtr) === '') {
// Not a namespaced function declaration. Parse error, but not our concern.
return;
}
$phpcsFile->addWarning('Declaring a free-standing function called assert() is deprecated since PHP 7.3.', $stackPtr, 'Found');
}
}
@@ -0,0 +1,135 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\RemovedPHP4StyleConstructorsSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Koen Eelen <koen.eelen@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionNameRestrictions;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\RemovedPHP4StyleConstructorsSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Koen Eelen <koen.eelen@cu.be>
*/
class RemovedPHP4StyleConstructorsSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_CLASS,
T_INTERFACE,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
if ($this->determineNamespace($phpcsFile, $stackPtr) !== '') {
/*
* Namespaced methods with the same name as the class are treated as
* regular methods, so we can bow out if we're in a namespace.
*
* Note: the exception to this is PHP 5.3.0-5.3.2. This is currently
* not dealt with.
*/
return;
}
$tokens = $phpcsFile->getTokens();
$class = $tokens[$stackPtr];
if (isset($class['scope_closer']) === false) {
return;
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_STRING) {
// Anonymous class in combination with PHPCS 2.3.x.
return;
}
$scopeCloser = $class['scope_closer'];
$className = $tokens[$nextNonEmpty]['content'];
if (empty($className) || is_string($className) === false) {
return;
}
$nextFunc = $stackPtr;
$classNameLc = strtolower($className);
$newConstructorFound = false;
$oldConstructorFound = false;
$oldConstructorPos = -1;
while (($nextFunc = $phpcsFile->findNext(T_FUNCTION, ($nextFunc + 1), $scopeCloser)) !== false) {
$functionScopeCloser = $nextFunc;
if (isset($tokens[$nextFunc]['scope_closer'])) {
// Normal (non-interface, non-abstract) method.
$functionScopeCloser = $tokens[$nextFunc]['scope_closer'];
}
$funcName = $phpcsFile->getDeclarationName($nextFunc);
if (empty($funcName) || is_string($funcName) === false) {
$nextFunc = $functionScopeCloser;
continue;
}
$funcNameLc = strtolower($funcName);
if ($funcNameLc === '__construct') {
$newConstructorFound = true;
}
if ($funcNameLc === $classNameLc) {
$oldConstructorFound = true;
$oldConstructorPos = $nextFunc;
}
// If both have been found, no need to continue looping through the functions.
if ($newConstructorFound === true && $oldConstructorFound === true) {
break;
}
$nextFunc = $functionScopeCloser;
}
if ($newConstructorFound === false && $oldConstructorFound === true) {
$phpcsFile->addWarning(
'Use of deprecated PHP4 style class constructor is not supported since PHP 7.',
$oldConstructorPos,
'Found'
);
}
}
}
@@ -0,0 +1,144 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\ReservedFunctionNamesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionNameRestrictions;
use Generic_Sniffs_NamingConventions_CamelCapsFunctionNameSniff as PHPCS_CamelCapsFunctionNameSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Standards_AbstractScopeSniff as PHPCS_AbstractScopeSniff;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionNameRestrictions\ReservedFunctionNamesSniff.
*
* All function and method names starting with double underscore are reserved by PHP.
*
* {@internal Extends an upstream sniff to benefit from the properties contained therein.
* The properties are lists of valid PHP magic function and method names, which
* should be ignored for the purposes of this sniff.
* As this sniff is not PHP version specific, we don't need access to the utility
* methods in the PHPCompatibility\Sniff, so extending the upstream sniff is fine.
* As the upstream sniff checks the same (and more, but we don't need the rest),
* the logic in this sniff is largely the same as used upstream.
* Extending the upstream sniff instead of including it via the ruleset, however,
* prevents hard to debug issues of errors not being reported from the upstream sniff
* if this library is used in combination with other rulesets.}}
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ReservedFunctionNamesSniff extends PHPCS_CamelCapsFunctionNameSniff
{
/**
* Overload the constructor to work round various PHPCS cross-version compatibility issues.
*/
public function __construct()
{
$scopeTokens = array(T_CLASS, T_INTERFACE, T_TRAIT);
if (defined('T_ANON_CLASS')) {
$scopeTokens[] = T_ANON_CLASS;
}
// Call the grand-parent constructor directly.
PHPCS_AbstractScopeSniff::__construct($scopeTokens, array(T_FUNCTION), true);
// Make sure debuginfo is included in the array. Upstream only includes it since 2.5.1.
$this->magicMethods['debuginfo'] = true;
}
/**
* Processes the tokens within the scope.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being processed.
* @param int $stackPtr The position where this token was
* found.
* @param int $currScope The position of the current scope.
*
* @return void
*/
protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
{
$tokens = $phpcsFile->getTokens();
/*
* Determine if this is a function which needs to be examined.
* The `processTokenWithinScope()` is called for each valid scope a method is in,
* so for nested classes, we need to make sure we only examine the token for
* the lowest level valid scope.
*/
$conditions = $tokens[$stackPtr]['conditions'];
end($conditions);
$deepestScope = key($conditions);
if ($deepestScope !== $currScope) {
return;
}
$methodName = $phpcsFile->getDeclarationName($stackPtr);
if ($methodName === null) {
// Ignore closures.
return;
}
// Is this a magic method. i.e., is prefixed with "__" ?
if (preg_match('|^__[^_]|', $methodName) > 0) {
$magicPart = strtolower(substr($methodName, 2));
if (isset($this->magicMethods[$magicPart]) === false
&& isset($this->methodsDoubleUnderscore[$magicPart]) === false
) {
$className = '[anonymous class]';
$scopeNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($currScope + 1), null, true);
if ($scopeNextNonEmpty !== false && $tokens[$scopeNextNonEmpty]['code'] === T_STRING) {
$className = $tokens[$scopeNextNonEmpty]['content'];
}
$phpcsFile->addWarning(
'Method name "%s" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.',
$stackPtr,
'MethodDoubleUnderscore',
array($className . '::' . $methodName)
);
}
}
}
/**
* Processes the tokens outside the scope.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being processed.
* @param int $stackPtr The position where this token was
* found.
*
* @return void
*/
protected function processTokenOutsideScope(File $phpcsFile, $stackPtr)
{
$functionName = $phpcsFile->getDeclarationName($stackPtr);
if ($functionName === null) {
// Ignore closures.
return;
}
// Is this a magic function. i.e., it is prefixed with "__".
if (preg_match('|^__[^_]|', $functionName) > 0) {
$magicPart = strtolower(substr($functionName, 2));
if (isset($this->magicFunctions[$magicPart]) === false) {
$phpcsFile->addWarning(
'Function name "%s" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.',
$stackPtr,
'FunctionDoubleUnderscore',
array($functionName)
);
}
}
}
}
@@ -0,0 +1,439 @@
<?php
/**
* PHPCompatibility, an external standard for PHP_CodeSniffer.
*
* @package PHPCompatibility
* @copyright 2012-2018 PHPCompatibility Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCompatibility/PHPCompatibility
*/
namespace PHPCompatibility\Sniffs\FunctionUse;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* Functions inspecting function arguments report the current parameter value
* instead of the original since PHP 7.0.
*
* `func_get_arg()`, `func_get_args()`, `debug_backtrace()` and exception backtraces
* will no longer report the original value that was passed to a parameter, but will
* instead provide the current value (which might have been modified).
*
* PHP version 7.0
*
* @since 9.1.0
*/
class ArgumentFunctionsReportCurrentValueSniff extends Sniff
{
/**
* A list of functions that, when called, can behave differently in PHP 7
* when dealing with parameters of the function they're called in.
*
* @var array
*/
protected $changedFunctions = array(
'func_get_arg' => true,
'func_get_args' => true,
'debug_backtrace' => true,
'debug_print_backtrace' => true,
);
/**
* Tokens to look out for to allow us to skip past nested scoped structures.
*
* @var array
*/
private $skipPastNested = array(
'T_CLASS' => true,
'T_ANON_CLASS' => true,
'T_INTERFACE' => true,
'T_TRAIT' => true,
'T_FUNCTION' => true,
'T_CLOSURE' => true,
);
/**
* List of tokens which when they preceed a T_STRING *within a function* indicate
* this is not a call to a PHP native function.
*
* This list already takes into account that nested scoped structures are being
* skipped over, so doesn't check for those again.
* Similarly, as constants won't have parentheses, those don't need to be checked
* for either.
*
* @var array
*/
private $noneFunctionCallIndicators = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
);
/**
* The tokens for variable incrementing/decrementing.
*
* @var array
*/
private $plusPlusMinusMinus = array(
T_DEC => true,
T_INC => true,
);
/**
* Tokens to ignore when determining the start of a statement.
*
* @var array
*/
private $ignoreForStartOfStatement = array(
T_COMMA,
T_DOUBLE_ARROW,
T_OPEN_SQUARE_BRACKET,
T_OPEN_PARENTHESIS,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_FUNCTION,
T_CLOSURE,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
// Abstract function, interface function, live coding or parse error.
return;
}
$scopeOpener = $tokens[$stackPtr]['scope_opener'];
$scopeCloser = $tokens[$stackPtr]['scope_closer'];
// Does the function declaration have parameters ?
$params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($params)) {
// No named arguments found, so no risk of them being changed.
return;
}
$paramNames = array();
foreach ($params as $param) {
$paramNames[] = $param['name'];
}
for ($i = ($scopeOpener + 1); $i < $scopeCloser; $i++) {
if (isset($this->skipPastNested[$tokens[$i]['type']]) && isset($tokens[$i]['scope_closer'])) {
// Skip past nested structures.
$i = $tokens[$i]['scope_closer'];
continue;
}
if ($tokens[$i]['code'] !== T_STRING) {
continue;
}
$foundFunctionName = strtolower($tokens[$i]['content']);
if (isset($this->changedFunctions[$foundFunctionName]) === false) {
// Not one of the target functions.
continue;
}
/*
* Ok, so is this really a function call to one of the PHP native functions ?
*/
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
if ($next === false || $tokens[$next]['code'] !== T_OPEN_PARENTHESIS) {
// Live coding, parse error or not a function call.
continue;
}
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true);
if ($prev !== false) {
if (isset($this->noneFunctionCallIndicators[$tokens[$prev]['code']])) {
continue;
}
// Check for namespaced functions, ie: \foo\bar() not \bar().
if ($tokens[ $prev ]['code'] === T_NS_SEPARATOR) {
$pprev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
if ($pprev !== false && $tokens[ $pprev ]['code'] === T_STRING) {
continue;
}
}
}
/*
* Address some special cases.
*/
if ($foundFunctionName !== 'func_get_args') {
$paramOne = $this->getFunctionCallParameter($phpcsFile, $i, 1);
if ($paramOne !== false) {
switch ($foundFunctionName) {
/*
* Check if `debug_(print_)backtrace()` is called with the
* `DEBUG_BACKTRACE_IGNORE_ARGS` option.
*/
case 'debug_backtrace':
case 'debug_print_backtrace':
$hasIgnoreArgs = $phpcsFile->findNext(
T_STRING,
$paramOne['start'],
($paramOne['end'] + 1),
false,
'DEBUG_BACKTRACE_IGNORE_ARGS'
);
if ($hasIgnoreArgs !== false) {
// Debug_backtrace() called with ignore args option.
continue 2;
}
break;
/*
* Collect the necessary information to only throw a notice if the argument
* touched/changed is in line with the passed $arg_num.
*
* Also, we can ignore `func_get_arg()` if the argument offset passed is
* higher than the number of named parameters.
*
* {@internal Note: This does not take calculations into account!
* Should be exceptionally rare and can - if needs be - be addressed at a later stage.}}
*/
case 'func_get_arg':
$number = $phpcsFile->findNext(T_LNUMBER, $paramOne['start'], ($paramOne['end'] + 1));
if ($number !== false) {
$argNumber = $tokens[$number]['content'];
if (isset($paramNames[$argNumber]) === false) {
// Requesting a non-named additional parameter. Ignore.
continue 2;
}
}
break;
}
}
} else {
/*
* Check if the call to func_get_args() happens to be in an array_slice() or
* array_splice() with an $offset higher than the number of named parameters.
* In that case, we can ignore it.
*
* {@internal Note: This does not take offset calculations into account!
* Should be exceptionally rare and can - if needs be - be addressed at a later stage.}}
*/
if ($prev !== false && $tokens[$prev]['code'] === T_OPEN_PARENTHESIS) {
$maybeFunctionCall = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
if ($maybeFunctionCall !== false
&& $tokens[$maybeFunctionCall]['code'] === T_STRING
&& ($tokens[$maybeFunctionCall]['content'] === 'array_slice'
|| $tokens[$maybeFunctionCall]['content'] === 'array_splice')
) {
$parentFuncParamTwo = $this->getFunctionCallParameter($phpcsFile, $maybeFunctionCall, 2);
$number = $phpcsFile->findNext(
T_LNUMBER,
$parentFuncParamTwo['start'],
($parentFuncParamTwo['end'] + 1)
);
if ($number !== false && isset($paramNames[$tokens[$number]['content']]) === false) {
// Requesting non-named additional parameters. Ignore.
continue ;
}
// Slice starts at a named argument, but we know which params are being accessed.
$paramNamesSubset = array_slice($paramNames, $tokens[$number]['content']);
}
}
}
/*
* For debug_backtrace(), check if the result is being dereferenced and if so,
* whether the `args` index is used.
* I.e. whether `$index` in `debug_backtrace()[$stackFrame][$index]` is a string
* with the content `args`.
*
* Note: We already know that $next is the open parenthesis of the function call.
*/
if ($foundFunctionName === 'debug_backtrace' && isset($tokens[$next]['parenthesis_closer'])) {
$afterParenthesis = $phpcsFile->findNext(
Tokens::$emptyTokens,
($tokens[$next]['parenthesis_closer'] + 1),
null,
true
);
if ($tokens[$afterParenthesis]['code'] === T_OPEN_SQUARE_BRACKET
&& isset($tokens[$afterParenthesis]['bracket_closer'])
) {
$afterStackFrame = $phpcsFile->findNext(
Tokens::$emptyTokens,
($tokens[$afterParenthesis]['bracket_closer'] + 1),
null,
true
);
if ($tokens[$afterStackFrame]['code'] === T_OPEN_SQUARE_BRACKET
&& isset($tokens[$afterStackFrame]['bracket_closer'])
) {
$arrayIndex = $phpcsFile->findNext(
T_CONSTANT_ENCAPSED_STRING,
($afterStackFrame + 1),
$tokens[$afterStackFrame]['bracket_closer']
);
if ($arrayIndex !== false && $this->stripQuotes($tokens[$arrayIndex]['content']) !== 'args') {
continue;
}
}
}
}
/*
* Only check for variables before the start of the statement to
* prevent false positives on the return value of the function call
* being assigned to one of the parameters, i.e.:
* `$param = func_get_args();`.
*/
$startOfStatement = PHPCSHelper::findStartOfStatement($phpcsFile, $i, $this->ignoreForStartOfStatement);
/*
* Ok, so we've found one of the target functions in the right scope.
* Now, let's check if any of the passed parameters were touched.
*/
$scanResult = 'clean';
for ($j = ($scopeOpener + 1); $j < $startOfStatement; $j++) {
if (isset($this->skipPastNested[$tokens[$j]['type']])
&& isset($tokens[$j]['scope_closer'])
) {
// Skip past nested structures.
$j = $tokens[$j]['scope_closer'];
continue;
}
if ($tokens[$j]['code'] !== T_VARIABLE) {
continue;
}
if ($foundFunctionName === 'func_get_arg' && isset($argNumber)) {
if (isset($paramNames[$argNumber])
&& $tokens[$j]['content'] !== $paramNames[$argNumber]
) {
// Different param than the one requested by func_get_arg().
continue;
}
} elseif ($foundFunctionName === 'func_get_args' && isset($paramNamesSubset)) {
if (in_array($tokens[$j]['content'], $paramNamesSubset, true) === false) {
// Different param than the ones requested by func_get_args().
continue;
}
} elseif (in_array($tokens[$j]['content'], $paramNames, true) === false) {
// Variable is not one of the function parameters.
continue;
}
/*
* Ok, so we've found a variable which was passed as one of the parameters.
* Now, is this variable being changed, i.e. incremented, decremented or
* assigned something ?
*/
$scanResult = 'warning';
if (isset($variableToken) === false) {
$variableToken = $j;
}
$beforeVar = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($j - 1), null, true);
if ($beforeVar !== false && isset($this->plusPlusMinusMinus[$tokens[$beforeVar]['code']])) {
// Variable is being (pre-)incremented/decremented.
$scanResult = 'error';
$variableToken = $j;
break;
}
$afterVar = $phpcsFile->findNext(Tokens::$emptyTokens, ($j + 1), null, true);
if ($afterVar === false) {
// Shouldn't be possible, but just in case.
continue;
}
if (isset($this->plusPlusMinusMinus[$tokens[$afterVar]['code']])) {
// Variable is being (post-)incremented/decremented.
$scanResult = 'error';
$variableToken = $j;
break;
}
if ($tokens[$afterVar]['code'] === T_OPEN_SQUARE_BRACKET
&& isset($tokens[$afterVar]['bracket_closer'])
) {
// Skip past array access on the variable.
while (($afterVar = $phpcsFile->findNext(Tokens::$emptyTokens, ($tokens[$afterVar]['bracket_closer'] + 1), null, true)) !== false) {
if ($tokens[$afterVar]['code'] !== T_OPEN_SQUARE_BRACKET
|| isset($tokens[$afterVar]['bracket_closer']) === false
) {
break;
}
}
}
if ($afterVar !== false
&& isset(Tokens::$assignmentTokens[$tokens[$afterVar]['code']])
) {
// Variable is being assigned something.
$scanResult = 'error';
$variableToken = $j;
break;
}
}
unset($argNumber, $paramNamesSubset);
if ($scanResult === 'clean') {
continue;
}
$error = 'Since PHP 7.0, functions inspecting arguments, like %1$s(), no longer report the original value as passed to a parameter, but will instead provide the current value. The parameter "%2$s" was %4$s on line %3$s.';
$data = array(
$foundFunctionName,
$tokens[$variableToken]['content'],
$tokens[$variableToken]['line'],
);
if ($scanResult === 'error') {
$data[] = 'changed';
$phpcsFile->addError($error, $i, 'Changed', $data);
} elseif ($scanResult === 'warning') {
$data[] = 'used, and possibly changed (by reference),';
$phpcsFile->addWarning($error, $i, 'NeedsInspection', $data);
}
unset($variableToken);
}
}
}
@@ -0,0 +1,164 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionUse\ArgumentFunctionsUsageSniff.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionUse;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionUse\ArgumentFunctionsUsageSniff.
*
* - Prior to PHP 5.3, these functions could not be used as a function call parameter.
*
* - Calling these functions from the outermost scope of a file which has been included by
* calling `include` or `require` from within a function in the calling file, worked
* prior to PHP 5.3. As of PHP 5.3, this will generate a warning and will always return false/-1.
* If the file was called directly or included in the global scope, calls to these
* functions would already generate a warning prior to PHP 5.3.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ArgumentFunctionsUsageSniff extends Sniff
{
/**
* The target functions for this sniff.
*
* @var array
*/
protected $targetFunctions = array(
'func_get_args' => true,
'func_get_arg' => true,
'func_num_args' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$functionLc = strtolower($tokens[$stackPtr]['content']);
if (isset($this->targetFunctions[$functionLc]) === false) {
return;
}
// Next non-empty token should be the open parenthesis.
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_NEW => true,
);
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevNonEmpty]['code']]) === true) {
// Not a call to a PHP function.
return;
} elseif ($tokens[$prevNonEmpty]['code'] === T_NS_SEPARATOR && $tokens[$prevNonEmpty - 1]['code'] === T_STRING) {
// Namespaced function.
return;
}
$data = $tokens[$stackPtr]['content'];
/*
* Check for usage of the functions in the global scope.
*
* As PHPCS can not determine whether a file is included from within a function in
* another file, so always throw a warning/error.
*/
if ($phpcsFile->hasCondition($stackPtr, array(T_FUNCTION, T_CLOSURE)) === false) {
$isError = false;
$message = 'Use of %s() outside of a user-defined function is only supported if the file is included from within a user-defined function in another file prior to PHP 5.3.';
if ($this->supportsAbove('5.3') === true) {
$isError = true;
$message .= ' As of PHP 5.3, it is no longer supported at all.';
}
$this->addMessage($phpcsFile, $message, $stackPtr, $isError, 'OutsideFunctionScope', $data);
}
/*
* Check for usage of the functions as a parameter in a function call.
*/
if ($this->supportsBelow('5.2') === false) {
return;
}
if (isset($tokens[$stackPtr]['nested_parenthesis']) === false) {
return;
}
$throwError = false;
$closer = end($tokens[$stackPtr]['nested_parenthesis']);
if (isset($tokens[$closer]['parenthesis_owner'])
&& $tokens[$tokens[$closer]['parenthesis_owner']]['type'] === 'T_CLOSURE'
) {
$throwError = true;
} else {
$opener = key($tokens[$stackPtr]['nested_parenthesis']);
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true);
if ($tokens[$prevNonEmpty]['code'] !== T_STRING) {
return;
}
$prevPrevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true);
if ($tokens[$prevPrevNonEmpty]['code'] === T_FUNCTION) {
return;
}
$throwError = true;
}
if ($throwError === false) {
return;
}
$phpcsFile->addError(
'%s() could not be used in parameter lists prior to PHP 5.3.',
$stackPtr,
'InParameterList',
$data
);
}
}
@@ -0,0 +1,159 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionUse\OptionalToRequiredFunctionParametersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionUse;
use PHPCompatibility\Sniffs\FunctionUse\RequiredToOptionalFunctionParametersSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionUse\OptionalToRequiredFunctionParametersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class OptionalToRequiredFunctionParametersSniff extends RequiredToOptionalFunctionParametersSniff
{
/**
* A list of function parameters, which were optional in older versions and became required later on.
*
* The array lists : version number with true (required) and false (optional use deprecated).
*
* The index is the location of the parameter in the parameter list, starting at 0 !
* If's sufficient to list the last version in which the parameter was not yet required.
*
* @var array
*/
protected $functionParameters = array(
// Special case, the optional nature is not deprecated, but usage is recommended
// and leaving the parameter out will throw an E_NOTICE.
'crypt' => array(
1 => array(
'name' => 'salt',
'5.6' => 'recommended',
),
),
'parse_str' => array(
1 => array(
'name' => 'result',
'7.2' => false,
),
),
);
/**
* Determine whether an error/warning should be thrown for an item based on collected information.
*
* @param array $errorInfo Detail information about an item.
*
* @return bool
*/
protected function shouldThrowError(array $errorInfo)
{
return ($errorInfo['optionalDeprecated'] !== ''
|| $errorInfo['optionalRemoved'] !== ''
|| $errorInfo['optionalRecommended'] !== '');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = array(
'paramName' => '',
'optionalRecommended' => '',
'optionalDeprecated' => '',
'optionalRemoved' => '',
'error' => false,
);
$versionArray = $this->getVersionArray($itemArray);
if (empty($versionArray) === false) {
foreach ($versionArray as $version => $required) {
if ($this->supportsAbove($version) === true) {
if ($required === true && $errorInfo['optionalRemoved'] === '') {
$errorInfo['optionalRemoved'] = $version;
$errorInfo['error'] = true;
} elseif ($required === 'recommended' && $errorInfo['optionalRecommended'] === '') {
$errorInfo['optionalRecommended'] = $version;
} elseif ($errorInfo['optionalDeprecated'] === '') {
$errorInfo['optionalDeprecated'] = $version;
}
}
}
}
$errorInfo['paramName'] = $itemArray['name'];
return $errorInfo;
}
/**
* Generates the error or warning for this item.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the relevant token in
* the stack.
* @param array $itemInfo Base information about the item.
* @param array $errorInfo Array with detail (version) information
* relevant to the item.
*
* @return void
*/
public function addError(File $phpcsFile, $stackPtr, array $itemInfo, array $errorInfo)
{
$error = 'The "%s" parameter for function %s() is missing. Passing this parameter is ';
if ($errorInfo['optionalRecommended'] === '') {
$error .= 'no longer optional. The optional nature of the parameter is ';
} else {
$error .= 'strongly recommended ';
}
$errorCode = $this->stringToErrorCode($itemInfo['name'] . '_' . $errorInfo['paramName']);
$data = array(
$errorInfo['paramName'],
$itemInfo['name'],
);
if ($errorInfo['optionalRecommended'] !== '') {
$error .= 'since PHP %s ';
$errorCode .= 'SoftRecommended';
$data[] = $errorInfo['optionalRecommended'];
} else {
if ($errorInfo['optionalDeprecated'] !== '') {
$error .= 'deprecated since PHP %s and ';
$errorCode .= 'SoftRequired';
$data[] = $errorInfo['optionalDeprecated'];
}
if ($errorInfo['optionalRemoved'] !== '') {
$error .= 'removed since PHP %s and ';
$errorCode .= 'HardRequired';
$data[] = $errorInfo['optionalRemoved'];
}
// Remove the last 'and' from the message.
$error = substr($error, 0, (strlen($error) - 5));
}
$this->addMessage($phpcsFile, $error, $stackPtr, $errorInfo['error'], $errorCode, $data);
}
}
@@ -0,0 +1,219 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionUse\RemovedFunctionParametersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionUse;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionUse\RemovedFunctionParametersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class RemovedFunctionParametersSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of removed function parameters, which were present in older versions.
*
* The array lists : version number with false (deprecated) and true (removed).
* The index is the location of the parameter in the parameter list, starting at 0 !
* If's sufficient to list the first version where the function parameter was deprecated/removed.
*
* @var array
*/
protected $removedFunctionParameters = array(
'define' => array(
2 => array(
'name' => 'case_insensitive',
'7.3' => false, // Slated for removal in PHP 8.0.0.
),
),
'gmmktime' => array(
6 => array(
'name' => 'is_dst',
'5.1' => false,
'7.0' => true,
),
),
'ldap_first_attribute' => array(
2 => array(
'name' => 'ber_identifier',
'5.2.4' => true,
),
),
'ldap_next_attribute' => array(
2 => array(
'name' => 'ber_identifier',
'5.2.4' => true,
),
),
'mktime' => array(
6 => array(
'name' => 'is_dst',
'5.1' => false,
'7.0' => true,
),
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of function names.
$this->removedFunctionParameters = $this->arrayKeysToLowercase($this->removedFunctionParameters);
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CONST => true,
);
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}
$function = $tokens[$stackPtr]['content'];
$functionLc = strtolower($function);
if (isset($this->removedFunctionParameters[$functionLc]) === false) {
return;
}
$parameterCount = $this->getFunctionCallParameterCount($phpcsFile, $stackPtr);
if ($parameterCount === 0) {
return;
}
// If the parameter count returned > 0, we know there will be valid open parenthesis.
$openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
$parameterOffsetFound = $parameterCount - 1;
foreach ($this->removedFunctionParameters[$functionLc] as $offset => $parameterDetails) {
if ($offset <= $parameterOffsetFound) {
$itemInfo = array(
'name' => $function,
'nameLc' => $functionLc,
'offset' => $offset,
);
$this->handleFeature($phpcsFile, $openParenthesis, $itemInfo);
}
}
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->removedFunctionParameters[$itemInfo['nameLc']][$itemInfo['offset']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('name');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['paramName'] = $itemArray['name'];
return $errorInfo;
}
/**
* Get the item name to be used for the creation of the error code.
*
* @param array $itemInfo Base information about the item.
* @param array $errorInfo Detail information about an item.
*
* @return string
*/
protected function getItemName(array $itemInfo, array $errorInfo)
{
return $itemInfo['name'] . '_' . $errorInfo['paramName'];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The "%s" parameter for function %s() is ';
}
/**
* Filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
array_shift($data);
array_unshift($data, $errorInfo['paramName'], $itemInfo['name']);
return $data;
}
}
@@ -0,0 +1,970 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionUse\RemovedFunctionsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\FunctionUse;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\FunctionUse\RemovedFunctionsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class RemovedFunctionsSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of deprecated and removed functions with their alternatives.
*
* The array lists : version number with false (deprecated) or true (removed) and an alternative function.
* If no alternative exists, it is NULL, i.e, the function should just not be used.
*
* @var array(string => array(string => bool|string|null))
*/
protected $removedFunctions = array(
'php_check_syntax' => array(
'5.0.5' => true,
'alternative' => null,
),
'call_user_method' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'call_user_func()',
),
'call_user_method_array' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'call_user_func_array()',
),
'define_syslog_variables' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => null,
),
'dl' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => null,
),
'ereg' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'preg_match()',
),
'ereg_replace' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'preg_replace()',
),
'eregi' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'preg_match()',
),
'eregi_replace' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'preg_replace()',
),
'imagepsbbox' => array(
'7.0' => true,
'alternative' => null,
),
'imagepsencodefont' => array(
'7.0' => true,
'alternative' => null,
),
'imagepsextendfont' => array(
'7.0' => true,
'alternative' => null,
),
'imagepsfreefont' => array(
'7.0' => true,
'alternative' => null,
),
'imagepsloadfont' => array(
'7.0' => true,
'alternative' => null,
),
'imagepsslantfont' => array(
'7.0' => true,
'alternative' => null,
),
'imagepstext' => array(
'7.0' => true,
'alternative' => null,
),
'import_request_variables' => array(
'5.3' => false,
'5.4' => true,
'alternative' => null,
),
'ldap_sort' => array(
'7.0' => false,
'alternative' => null,
),
'mcrypt_generic_end' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'mcrypt_generic_deinit()',
),
'mysql_db_query' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'mysqli::select_db() and mysqli::query()',
),
'mysql_escape_string' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'mysqli::real_escape_string()',
),
'mysql_list_dbs' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'mysqli_bind_param' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli_stmt::bind_param()',
),
'mysqli_bind_result' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli_stmt::bind_result()',
),
'mysqli_client_encoding' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli::character_set_name()',
),
'mysqli_fetch' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli_stmt::fetch()',
),
'mysqli_param_count' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli_stmt_param_count()',
),
'mysqli_get_metadata' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli_stmt::result_metadata()',
),
'mysqli_send_long_data' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => 'mysqli_stmt::send_long_data()',
),
'magic_quotes_runtime' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'session_register' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => '$_SESSION',
),
'session_unregister' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => '$_SESSION',
),
'session_is_registered' => array(
'5.3' => false,
'5.4' => true,
'5.5' => true,
'5.6' => true,
'alternative' => '$_SESSION',
),
'set_magic_quotes_runtime' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'set_socket_blocking' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'stream_set_blocking()',
),
'split' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'preg_split()',
),
'spliti' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'preg_split()',
),
'sql_regcase' => array(
'5.3' => false,
'5.4' => false,
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'php_logo_guid' => array(
'5.5' => true,
'5.6' => true,
'alternative' => null,
),
'php_egg_logo_guid' => array(
'5.5' => true,
'5.6' => true,
'alternative' => null,
),
'php_real_logo_guid' => array(
'5.5' => true,
'5.6' => true,
'alternative' => null,
),
'zend_logo_guid' => array(
'5.5' => true,
'5.6' => true,
'alternative' => null,
),
'datefmt_set_timezone_id' => array(
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => 'IntlDateFormatter::setTimeZone()',
),
'mcrypt_ecb' => array(
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'mcrypt_cbc' => array(
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'mcrypt_cfb' => array(
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'mcrypt_ofb' => array(
'5.5' => false,
'5.6' => false,
'7.0' => true,
'alternative' => null,
),
'ocibindbyname' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_bind_by_name()',
),
'ocicancel' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_cancel()',
),
'ocicloselob' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::close()',
),
'ocicollappend' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::append()',
),
'ocicollassign' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::assign()',
),
'ocicollassignelem' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::assignElem()',
),
'ocicollgetelem' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::getElem()',
),
'ocicollmax' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::max()',
),
'ocicollsize' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::size()',
),
'ocicolltrim' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::trim()',
),
'ocicolumnisnull' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_is_null()',
),
'ocicolumnname' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_name()',
),
'ocicolumnprecision' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_precision()',
),
'ocicolumnscale' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_scale()',
),
'ocicolumnsize' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_size()',
),
'ocicolumntype' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_type()',
),
'ocicolumntyperaw' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_field_type_raw()',
),
'ocicommit' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_commit()',
),
'ocidefinebyname' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_define_by_name()',
),
'ocierror' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_error()',
),
'ociexecute' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_execute()',
),
'ocifetch' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_fetch()',
),
'ocifetchinto' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => null,
),
'ocifetchstatement' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_fetch_all()',
),
'ocifreecollection' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Collection::free()',
),
'ocifreecursor' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_free_statement()',
),
'ocifreedesc' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::free()',
),
'ocifreestatement' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_free_statement()',
),
'ociinternaldebug' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_internal_debug()',
),
'ociloadlob' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::load()',
),
'ocilogoff' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_close()',
),
'ocilogon' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_connect()',
),
'ocinewcollection' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_new_collection()',
),
'ocinewcursor' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_new_cursor()',
),
'ocinewdescriptor' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_new_descriptor()',
),
'ocinlogon' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_new_connect()',
),
'ocinumcols' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_num_fields()',
),
'ociparse' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_parse()',
),
'ociplogon' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_pconnect()',
),
'ociresult' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_result()',
),
'ocirollback' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_rollback()',
),
'ocirowcount' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_num_rows()',
),
'ocisavelob' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::save()',
),
'ocisavelobfile' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::import()',
),
'ociserverversion' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_server_version()',
),
'ocisetprefetch' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_set_prefetch()',
),
'ocistatementtype' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'oci_statement_type()',
),
'ociwritelobtofile' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::export()',
),
'ociwritetemporarylob' => array(
'5.4' => false,
'5.5' => false,
'5.6' => false,
'alternative' => 'OCI-Lob::writeTemporary()',
),
'mysqli_get_cache_stats' => array(
'5.4' => true,
'alternative' => null,
),
'mcrypt_create_iv' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'random_bytes() or OpenSSL',
),
'mcrypt_decrypt' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_get_algorithms_name' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_get_block_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_get_iv_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_get_key_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_get_modes_name' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_get_supported_key_sizes' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_is_block_algorithm_mode' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_is_block_algorithm' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_is_block_mode' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_enc_self_test' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_encrypt' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_generic_deinit' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_generic_init' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_generic' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_get_block_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_get_cipher_name' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_get_iv_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_get_key_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_list_algorithms' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_list_modes' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_close' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_get_algo_block_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_get_algo_key_size' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_get_supported_key_sizes' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_is_block_algorithm_mode' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_is_block_algorithm' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_is_block_mode' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_open' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mcrypt_module_self_test' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'mdecrypt_generic' => array(
'7.1' => false,
'7.2' => true,
'alternative' => 'OpenSSL',
),
'jpeg2wbmp' => array(
'7.2' => false,
'alternative' => 'imagecreatefromjpeg() and imagewbmp()',
),
'png2wbmp' => array(
'7.2' => false,
'alternative' => 'imagecreatefrompng() or imagewbmp()',
),
'create_function' => array(
'7.2' => false,
'alternative' => 'an anonymous function',
),
'each' => array(
'7.2' => false,
'alternative' => 'a foreach loop',
),
'gmp_random' => array(
'7.2' => false,
'alternative' => 'gmp_random_bits() or gmp_random_range()',
),
'read_exif_data' => array(
'7.2' => false,
'alternative' => 'exif_read_data()',
),
'image2wbmp' => array(
'7.3' => false,
'alternative' => 'imagewbmp()',
),
'mbregex_encoding' => array(
'7.3' => false,
'alternative' => 'mb_regex_encoding()',
),
'mbereg' => array(
'7.3' => false,
'alternative' => 'mb_ereg()',
),
'mberegi' => array(
'7.3' => false,
'alternative' => 'mb_eregi()',
),
'mbereg_replace' => array(
'7.3' => false,
'alternative' => 'mb_ereg_replace()',
),
'mberegi_replace' => array(
'7.3' => false,
'alternative' => 'mb_eregi_replace()',
),
'mbsplit' => array(
'7.3' => false,
'alternative' => 'mb_split()',
),
'mbereg_match' => array(
'7.3' => false,
'alternative' => 'mb_ereg_match()',
),
'mbereg_search' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search()',
),
'mbereg_search_pos' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search_pos()',
),
'mbereg_search_regs' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search_regs()',
),
'mbereg_search_init' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search_init()',
),
'mbereg_search_getregs' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search_getregs()',
),
'mbereg_search_getpos' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search_getpos()',
),
'mbereg_search_setpos' => array(
'7.3' => false,
'alternative' => 'mb_ereg_search_setpos()',
),
'fgetss' => array(
'7.3' => false,
'alternative' => null,
),
'gzgetss' => array(
'7.3' => false,
'alternative' => null,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of function names.
$this->removedFunctions = $this->arrayKeysToLowercase($this->removedFunctions);
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CLASS => true,
T_CONST => true,
T_USE => true,
T_NS_SEPARATOR => true,
);
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}
$function = $tokens[$stackPtr]['content'];
$functionLc = strtolower($function);
if (isset($this->removedFunctions[$functionLc]) === false) {
return;
}
$itemInfo = array(
'name' => $function,
'nameLc' => $functionLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->removedFunctions[$itemInfo['nameLc']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'Function %s() is ';
}
}
@@ -0,0 +1,310 @@
<?php
/**
* \PHPCompatibility\Sniffs\FunctionUse\RequiredToOptionalFunctionParametersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\FunctionUse;
use PHPCompatibility\AbstractComplexVersionSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\FunctionUse\RequiredToOptionalFunctionParametersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RequiredToOptionalFunctionParametersSniff extends AbstractComplexVersionSniff
{
/**
* A list of function parameters, which were required in older versions and became optional later on.
*
* The array lists : version number with true (required) and false (optional).
*
* The index is the location of the parameter in the parameter list, starting at 0 !
* If's sufficient to list the last version in which the parameter was still required.
*
* @var array
*/
protected $functionParameters = array(
'array_push' => array(
1 => array(
'name' => 'element to push',
'7.2' => true,
'7.3' => false,
),
),
'array_unshift' => array(
1 => array(
'name' => 'element to prepend',
'7.2' => true,
'7.3' => false,
),
),
'bcscale' => array(
0 => array(
'name' => 'scale',
'7.2' => true,
'7.3' => false,
),
),
'ftp_fget' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_fput' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_get' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_nb_fget' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_nb_fput' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_nb_get' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_nb_put' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'ftp_put' => array(
3 => array(
'name' => 'mode',
'7.2' => true,
'7.3' => false,
),
),
'getenv' => array(
0 => array(
'name' => 'varname',
'7.0' => true,
'7.1' => false,
),
),
'preg_match_all' => array(
2 => array(
'name' => 'matches',
'5.3' => true,
'5.4' => false,
),
),
'stream_socket_enable_crypto' => array(
2 => array(
'name' => 'crypto_type',
'5.5' => true,
'5.6' => false,
),
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of function names.
$this->functionParameters = $this->arrayKeysToLowercase($this->functionParameters);
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CONST => true,
);
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}
$function = $tokens[$stackPtr]['content'];
$functionLc = strtolower($function);
if (isset($this->functionParameters[$functionLc]) === false) {
return;
}
$parameterCount = $this->getFunctionCallParameterCount($phpcsFile, $stackPtr);
$openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
// If the parameter count returned > 0, we know there will be valid open parenthesis.
if ($parameterCount === 0 && $tokens[$openParenthesis]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
$parameterOffsetFound = $parameterCount - 1;
foreach ($this->functionParameters[$functionLc] as $offset => $parameterDetails) {
if ($offset > $parameterOffsetFound) {
$itemInfo = array(
'name' => $function,
'nameLc' => $functionLc,
'offset' => $offset,
);
$this->handleFeature($phpcsFile, $openParenthesis, $itemInfo);
}
}
}
/**
* Determine whether an error/warning should be thrown for an item based on collected information.
*
* @param array $errorInfo Detail information about an item.
*
* @return bool
*/
protected function shouldThrowError(array $errorInfo)
{
return ($errorInfo['requiredVersion'] !== '');
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->functionParameters[$itemInfo['nameLc']][$itemInfo['offset']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('name');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = array(
'paramName' => '',
'requiredVersion' => '',
);
$versionArray = $this->getVersionArray($itemArray);
if (empty($versionArray) === false) {
foreach ($versionArray as $version => $required) {
if ($required === true && $this->supportsBelow($version) === true) {
$errorInfo['requiredVersion'] = $version;
}
}
}
$errorInfo['paramName'] = $itemArray['name'];
return $errorInfo;
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The "%s" parameter for function %s() is missing, but was required for PHP version %s and lower';
}
/**
* Generates the error or warning for this item.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the relevant token in
* the stack.
* @param array $itemInfo Base information about the item.
* @param array $errorInfo Array with detail (version) information
* relevant to the item.
*
* @return void
*/
public function addError(File $phpcsFile, $stackPtr, array $itemInfo, array $errorInfo)
{
$error = $this->getErrorMsgTemplate();
$errorCode = $this->stringToErrorCode($itemInfo['name'] . '_' . $errorInfo['paramName']) . 'Missing';
$data = array(
$errorInfo['paramName'],
$itemInfo['name'],
$errorInfo['requiredVersion'],
);
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
}
@@ -0,0 +1,153 @@
<?php
/**
* \PHPCompatibility\Sniffs\Generators\NewGeneratorReturnSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Generators;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Generators\NewGeneratorReturnSniff.
*
* As of PHP 7.0, a return statement can be used within a generator for a final expression to be returned.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewGeneratorReturnSniff extends Sniff
{
/**
* Scope conditions within which a yield can exist.
*
* @var array
*/
private $validConditions = array(
T_FUNCTION => T_FUNCTION,
T_CLOSURE => T_CLOSURE,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$targets = array(
T_YIELD,
);
/*
* The `yield` keyword was introduced in PHP 5.5 with the token T_YIELD.
* The `yield from` keyword was introduced in PHP 7.0 and tokenizes as
* "T_YIELD T_WHITESPACE T_STRING".
*
* Pre-PHPCS 3.1.0, the T_YIELD token was not correctly back-filled for PHP < 5.5.
* Also, as of PHPCS 3.1.0, the PHPCS tokenizer adds a new T_YIELD_FROM
* token.
*
* So for PHP 5.3-5.4 icw PHPCS < 3.1.0, we need to look for T_STRING with content "yield".
* For PHP 5.5+ we need to look for T_YIELD.
* For PHPCS 3.1.0+, we also need to look for T_YIELD_FROM.
*/
if (version_compare(PHP_VERSION_ID, '50500', '<') === true
&& version_compare(PHPCSHelper::getVersion(), '3.1.0', '<') === true
) {
$targets[] = T_STRING;
}
if (defined('T_YIELD_FROM')) {
$targets[] = T_YIELD_FROM;
}
return $targets;
}
/**
* 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|int Void or a stack pointer to skip forward.
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.6') !== true) {
return;
}
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_STRING
&& $tokens[$stackPtr]['content'] !== 'yield'
) {
return;
}
if (empty($tokens[$stackPtr]['conditions']) === true) {
return;
}
// Walk the condition from inner to outer to see if we can find a valid function/closure scope.
$conditions = array_reverse($tokens[$stackPtr]['conditions'], true);
foreach ($conditions as $ptr => $type) {
if (isset($this->validConditions[$type]) === true) {
$function = $ptr;
break;
}
}
if (isset($function) === false) {
// Yield outside function scope, fatal error, but not our concern.
return;
}
if (isset($tokens[$function]['scope_opener'], $tokens[$function]['scope_closer']) === false) {
// Can't reliably determine start/end of function scope.
return;
}
$targets = array(T_RETURN, T_CLOSURE, T_FUNCTION, T_CLASS);
if (defined('T_ANON_CLASS')) {
$targets[] = T_ANON_CLASS;
}
$current = $tokens[$function]['scope_opener'];
while (($current = $phpcsFile->findNext($targets, ($current + 1), $tokens[$function]['scope_closer'])) !== false) {
if ($tokens[$current]['code'] === T_RETURN) {
$phpcsFile->addError(
'Returning a final expression from a generator was not supported in PHP 5.6 or earlier',
$current,
'ReturnFound'
);
return $tokens[$function]['scope_closer'];
}
// Found a nested scope in which return can exist without problems.
if (isset($tokens[$current]['scope_closer'])) {
// Skip past the nested scope.
$current = $tokens[$current]['scope_closer'];
}
}
// Don't examine this function again.
return $tokens[$function]['scope_closer'];
}
}
@@ -0,0 +1,663 @@
<?php
/**
* \PHPCompatibility\Sniffs\IniDirectives\NewIniDirectivesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\IniDirectives;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\IniDirectives\NewIniDirectivesSniff.
*
* Discourages the use of new INI directives through ini_set() or ini_get().
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
class NewIniDirectivesSniff extends AbstractNewFeatureSniff
{
/**
* A list of new INI directives
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the ini directive appears.
*
* @var array(string)
*/
protected $newIniDirectives = array(
'auto_globals_jit' => array(
'4.4' => false,
'5.0' => true,
),
'com.code_page' => array(
'4.4' => false,
'5.0' => true,
),
'date.default_latitude' => array(
'4.4' => false,
'5.0' => true,
),
'date.default_longitude' => array(
'4.4' => false,
'5.0' => true,
),
'date.sunrise_zenith' => array(
'4.4' => false,
'5.0' => true,
),
'date.sunset_zenith' => array(
'4.4' => false,
'5.0' => true,
),
'ibase.default_charset' => array(
'4.4' => false,
'5.0' => true,
),
'ibase.default_db' => array(
'4.4' => false,
'5.0' => true,
),
'mail.force_extra_parameters' => array(
'4.4' => false,
'5.0' => true,
),
'mime_magic.debug' => array(
'4.4' => false,
'5.0' => true,
),
'mysqli.max_links' => array(
'4.4' => false,
'5.0' => true,
),
'mysqli.default_port' => array(
'4.4' => false,
'5.0' => true,
),
'mysqli.default_socket' => array(
'4.4' => false,
'5.0' => true,
),
'mysqli.default_host' => array(
'4.4' => false,
'5.0' => true,
),
'mysqli.default_user' => array(
'4.4' => false,
'5.0' => true,
),
'mysqli.default_pw' => array(
'4.4' => false,
'5.0' => true,
),
'report_zend_debug' => array(
'4.4' => false,
'5.0' => true,
),
'session.hash_bits_per_character' => array(
'4.4' => false,
'5.0' => true,
),
'session.hash_function' => array(
'4.4' => false,
'5.0' => true,
),
'soap.wsdl_cache_dir' => array(
'4.4' => false,
'5.0' => true,
),
'soap.wsdl_cache_enabled' => array(
'4.4' => false,
'5.0' => true,
),
'soap.wsdl_cache_ttl' => array(
'4.4' => false,
'5.0' => true,
),
'sqlite.assoc_case' => array(
'4.4' => false,
'5.0' => true,
),
'tidy.clean_output' => array(
'4.4' => false,
'5.0' => true,
),
'tidy.default_config' => array(
'4.4' => false,
'5.0' => true,
),
'zend.ze1_compatibility_mode' => array(
'4.4' => false,
'5.0' => true,
),
'date.timezone' => array(
'5.0' => false,
'5.1' => true,
),
'detect_unicode' => array(
'5.0' => false,
'5.1' => true,
),
'fbsql.batchsize' => array(
'5.0' => false,
'5.1' => true,
'alternative' => 'fbsql.batchSize',
),
'realpath_cache_size' => array(
'5.0' => false,
'5.1' => true,
),
'realpath_cache_ttl' => array(
'5.0' => false,
'5.1' => true,
),
'mbstring.strict_detection' => array(
'5.1.1' => false,
'5.1.2' => true,
),
'mssql.charset' => array(
'5.1.1' => false,
'5.1.2' => true,
),
'gd.jpeg_ignore_warning' => array(
'5.1.2' => false,
'5.1.3' => true,
),
'fbsql.show_timestamp_decimals' => array(
'5.1.4' => false,
'5.1.5' => true,
),
'soap.wsdl_cache' => array(
'5.1.4' => false,
'5.1.5' => true,
),
'soap.wsdl_cache_limit' => array(
'5.1.4' => false,
'5.1.5' => true,
),
'allow_url_include' => array(
'5.1' => false,
'5.2' => true,
),
'filter.default' => array(
'5.1' => false,
'5.2' => true,
),
'filter.default_flags' => array(
'5.1' => false,
'5.2' => true,
),
'pcre.backtrack_limit' => array(
'5.1' => false,
'5.2' => true,
),
'pcre.recursion_limit' => array(
'5.1' => false,
'5.2' => true,
),
'session.cookie_httponly' => array(
'5.1' => false,
'5.2' => true,
),
'cgi.check_shebang_line' => array(
'5.2.0' => false,
'5.2.1' => true,
),
'max_input_nesting_level' => array(
'5.2.2' => false,
'5.2.3' => true,
),
'mysqli.allow_local_infile' => array(
'5.2.3' => false,
'5.2.4' => true,
),
'max_file_uploads' => array(
'5.2.11' => false,
'5.2.12' => true,
),
'cgi.discard_path' => array(
'5.2' => false,
'5.3' => true,
),
'exit_on_timeout' => array(
'5.2' => false,
'5.3' => true,
),
'intl.default_locale' => array(
'5.2' => false,
'5.3' => true,
),
'intl.error_level' => array(
'5.2' => false,
'5.3' => true,
),
'mail.add_x_header' => array(
'5.2' => false,
'5.3' => true,
),
'mail.log' => array(
'5.2' => false,
'5.3' => true,
),
'mbstring.http_output_conv_mimetype' => array(
'5.2' => false,
'5.3' => true,
),
'mysqli.allow_persistent' => array(
'5.2' => false,
'5.3' => true,
),
'mysqli.cache_size' => array(
'5.2' => false,
'5.3' => true,
),
'mysqli.max_persistent' => array(
'5.2' => false,
'5.3' => true,
),
'mysqlnd.collect_memory_statistics' => array(
'5.2' => false,
'5.3' => true,
),
'mysqlnd.collect_statistics' => array(
'5.2' => false,
'5.3' => true,
),
'mysqlnd.debug' => array(
'5.2' => false,
'5.3' => true,
),
'mysqlnd.net_read_buffer_size' => array(
'5.2' => false,
'5.3' => true,
),
'odbc.default_cursortype' => array(
'5.2' => false,
'5.3' => true,
),
'request_order' => array(
'5.2' => false,
'5.3' => true,
),
'user_ini.cache_ttl' => array(
'5.2' => false,
'5.3' => true,
),
'user_ini.filename' => array(
'5.2' => false,
'5.3' => true,
),
'zend.enable_gc' => array(
'5.2' => false,
'5.3' => true,
),
'curl.cainfo' => array(
'5.3.6' => false,
'5.3.7' => true,
),
'max_input_vars' => array(
'5.3.8' => false,
'5.3.9' => true,
),
'sqlite3.extension_dir' => array(
'5.3.10' => false,
'5.3.11' => true,
),
'cli.pager' => array(
'5.3' => false,
'5.4' => true,
),
'cli.prompt' => array(
'5.3' => false,
'5.4' => true,
),
'cli_server.color' => array(
'5.3' => false,
'5.4' => true,
),
'enable_post_data_reading' => array(
'5.3' => false,
'5.4' => true,
),
'mysqlnd.mempool_default_size' => array(
'5.3' => false,
'5.4' => true,
),
'mysqlnd.net_cmd_buffer_size' => array(
'5.3' => false,
'5.4' => true,
),
'mysqlnd.net_read_timeout' => array(
'5.3' => false,
'5.4' => true,
),
'phar.cache_list' => array(
'5.3' => false,
'5.4' => true,
),
'session.upload_progress.enabled' => array(
'5.3' => false,
'5.4' => true,
),
'session.upload_progress.cleanup' => array(
'5.3' => false,
'5.4' => true,
),
'session.upload_progress.name' => array(
'5.3' => false,
'5.4' => true,
),
'session.upload_progress.freq' => array(
'5.3' => false,
'5.4' => true,
),
'session.upload_progress.min_freq' => array(
'5.3' => false,
'5.4' => true,
),
'session.upload_progress.prefix' => array(
'5.3' => false,
'5.4' => true,
),
'windows_show_crt_warning' => array(
'5.3' => false,
'5.4' => true,
),
'zend.detect_unicode' => array(
'5.3' => false,
'5.4' => true,
'alternative' => 'detect_unicode',
),
'zend.multibyte' => array(
'5.3' => false,
'5.4' => true,
),
'zend.script_encoding' => array(
'5.3' => false,
'5.4' => true,
),
'zend.signal_check' => array(
'5.3' => false,
'5.4' => true,
),
'mysqlnd.log_mask' => array(
'5.3' => false,
'5.4' => true,
),
'intl.use_exceptions' => array(
'5.4' => false,
'5.5' => true,
),
'mysqlnd.sha256_server_public_key' => array(
'5.4' => false,
'5.5' => true,
),
'mysqlnd.trace_alloc' => array(
'5.4' => false,
'5.5' => true,
),
'sys_temp_dir' => array(
'5.4' => false,
'5.5' => true,
),
'xsl.security_prefs' => array(
'5.4' => false,
'5.5' => true,
),
'session.use_strict_mode' => array(
'5.5.1' => false,
'5.5.2' => true,
),
'mysqli.rollback_on_cached_plink' => array(
'5.5' => false,
'5.6' => true,
),
'assert.exception' => array(
'5.6' => false,
'7.0' => true,
),
'pcre.jit' => array(
'5.6' => false,
'7.0' => true,
),
'session.lazy_write' => array(
'5.6' => false,
'7.0' => true,
),
'zend.assertions' => array(
'5.6' => false,
'7.0' => true,
),
'hard_timeout' => array(
'7.0' => false,
'7.1' => true,
),
'session.sid_length' => array(
'7.0' => false,
'7.1' => true,
),
'session.sid_bits_per_character' => array(
'7.0' => false,
'7.1' => true,
),
'session.trans_sid_hosts' => array(
'7.0' => false,
'7.1' => true,
),
'session.trans_sid_tags' => array(
'7.0' => false,
'7.1' => true,
),
'url_rewriter.hosts' => array(
'7.0' => false,
'7.1' => true,
),
// Introduced in PHP 7.1.25, 7.2.13, 7.3.0.
'imap.enable_insecure_rsh' => array(
'7.1.24' => false,
'7.1.25' => true,
),
'syslog.facility' => array(
'7.2' => false,
'7.3' => true,
),
'syslog.filter' => array(
'7.2' => false,
'7.3' => true,
),
'syslog.ident' => array(
'7.2' => false,
'7.3' => true,
),
'session.cookie_samesite' => array(
'7.2' => false,
'7.3' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CONST => true,
);
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}
$functionLc = strtolower($tokens[$stackPtr]['content']);
if (isset($this->iniFunctions[$functionLc]) === false) {
return;
}
$iniToken = $this->getFunctionCallParameter($phpcsFile, $stackPtr, $this->iniFunctions[$functionLc]);
if ($iniToken === false) {
return;
}
$filteredToken = $this->stripQuotes($iniToken['raw']);
if (isset($this->newIniDirectives[$filteredToken]) === false) {
return;
}
$itemInfo = array(
'name' => $filteredToken,
'functionLc' => $functionLc,
);
$this->handleFeature($phpcsFile, $iniToken['end'], $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newIniDirectives[$itemInfo['name']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('alternative');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['alternative'] = '';
if (isset($itemArray['alternative']) === true) {
$errorInfo['alternative'] = $itemArray['alternative'];
}
// Lower error level to warning if the function used was ini_get.
if ($errorInfo['error'] === true && $itemInfo['functionLc'] === 'ini_get') {
$errorInfo['error'] = false;
}
return $errorInfo;
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return "INI directive '%s' is not present in PHP version %s or earlier";
}
/**
* Allow for concrete child classes to filter the error message before it's passed to PHPCS.
*
* @param string $error The error message which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return string
*/
protected function filterErrorMsg($error, array $itemInfo, array $errorInfo)
{
if ($errorInfo['alternative'] !== '') {
$error .= ". This directive was previously called '%s'.";
}
return $error;
}
/**
* Allow for concrete child classes to filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
if ($errorInfo['alternative'] !== '') {
$data[] = $errorInfo['alternative'];
}
return $data;
}
}
@@ -0,0 +1,346 @@
<?php
/**
* \PHPCompatibility\Sniffs\IniDirectives\RemovedIniDirectivesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\IniDirectives;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\IniDirectives\RemovedIniDirectivesSniff.
*
* Discourages the use of deprecated and removed INI directives through ini_set() or ini_get().
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class RemovedIniDirectivesSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of deprecated INI directives.
*
* The array lists : version number with false (deprecated) and true (removed).
* If's sufficient to list the first version where the ini directive was deprecated/removed.
*
* @var array(string)
*/
protected $deprecatedIniDirectives = array(
'fbsql.batchSize' => array(
'5.1' => true,
'alternative' => 'fbsql.batchsize',
),
'ifx.allow_persistent' => array(
'5.2.1' => true,
),
'ifx.blobinfile' => array(
'5.2.1' => true,
),
'ifx.byteasvarchar' => array(
'5.2.1' => true,
),
'ifx.charasvarchar' => array(
'5.2.1' => true,
),
'ifx.default_host' => array(
'5.2.1' => true,
),
'ifx.default_password' => array(
'5.2.1' => true,
),
'ifx.default_user' => array(
'5.2.1' => true,
),
'ifx.max_links' => array(
'5.2.1' => true,
),
'ifx.max_persistent' => array(
'5.2.1' => true,
),
'ifx.nullformat' => array(
'5.2.1' => true,
),
'ifx.textasvarchar' => array(
'5.2.1' => true,
),
'zend.ze1_compatibility_mode' => array(
'5.3' => true,
),
'allow_call_time_pass_reference' => array(
'5.3' => false,
'5.4' => true,
),
'define_syslog_variables' => array(
'5.3' => false,
'5.4' => true,
),
'detect_unicode' => array(
'5.4' => true,
'alternative' => 'zend.detect_unicode',
),
'highlight.bg' => array(
'5.3' => false,
'5.4' => true,
),
'magic_quotes_gpc' => array(
'5.3' => false,
'5.4' => true,
),
'magic_quotes_runtime' => array(
'5.3' => false,
'5.4' => true,
),
'magic_quotes_sybase' => array(
'5.3' => false,
'5.4' => true,
),
'mbstring.script_encoding' => array(
'5.4' => true,
'alternative' => 'zend.script_encoding',
),
'register_globals' => array(
'5.3' => false,
'5.4' => true,
),
'register_long_arrays' => array(
'5.3' => false,
'5.4' => true,
),
'safe_mode' => array(
'5.3' => false,
'5.4' => true,
),
'safe_mode_allowed_env_vars' => array(
'5.3' => false,
'5.4' => true,
),
'safe_mode_exec_dir' => array(
'5.3' => false,
'5.4' => true,
),
'safe_mode_gid' => array(
'5.3' => false,
'5.4' => true,
),
'safe_mode_include_dir' => array(
'5.3' => false,
'5.4' => true,
),
'safe_mode_protected_env_vars' => array(
'5.3' => false,
'5.4' => true,
),
'session.bug_compat_42' => array(
'5.3' => false,
'5.4' => true,
),
'session.bug_compat_warn' => array(
'5.3' => false,
'5.4' => true,
),
'y2k_compliance' => array(
'5.3' => false,
'5.4' => true,
),
'always_populate_raw_post_data' => array(
'5.6' => false,
'7.0' => true,
),
'iconv.input_encoding' => array(
'5.6' => false,
),
'iconv.output_encoding' => array(
'5.6' => false,
),
'iconv.internal_encoding' => array(
'5.6' => false,
),
'mbstring.http_input' => array(
'5.6' => false,
),
'mbstring.http_output' => array(
'5.6' => false,
),
'mbstring.internal_encoding' => array(
'5.6' => false,
),
'asp_tags' => array(
'7.0' => true,
),
'xsl.security_prefs' => array(
'7.0' => true,
),
'mcrypt.algorithms_dir' => array(
'7.1' => false,
'7.2' => true,
),
'mcrypt.modes_dir' => array(
'7.1' => false,
'7.2' => true,
),
'session.entropy_file' => array(
'7.1' => true,
),
'session.entropy_length' => array(
'7.1' => true,
),
'session.hash_function' => array(
'7.1' => true,
),
'session.hash_bits_per_character' => array(
'7.1' => true,
),
'mbstring.func_overload' => array(
'7.2' => false,
),
'sql.safe_mode' => array(
'7.2' => true,
),
'track_errors' => array(
'7.2' => false,
),
'opcache.fast_shutdown' => array(
'7.2' => true,
),
'birdstep.max_links' => array(
'7.3' => true,
),
'opcache.inherited_hack' => array(
'5.3' => false, // Soft deprecated, i.e. ignored.
'7.3' => true,
),
'pdo_odbc.db2_instance_name' => array(
'7.3' => false, // Has been marked as deprecated in the manual from before this time. Now hard-deprecated.
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CONST => true,
);
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}
$functionLc = strtolower($tokens[$stackPtr]['content']);
if (isset($this->iniFunctions[$functionLc]) === false) {
return;
}
$iniToken = $this->getFunctionCallParameter($phpcsFile, $stackPtr, $this->iniFunctions[$functionLc]);
if ($iniToken === false) {
return;
}
$filteredToken = $this->stripQuotes($iniToken['raw']);
if (isset($this->deprecatedIniDirectives[$filteredToken]) === false) {
return;
}
$itemInfo = array(
'name' => $filteredToken,
'functionLc' => $functionLc,
);
$this->handleFeature($phpcsFile, $iniToken['end'], $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->deprecatedIniDirectives[$itemInfo['name']];
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
// Lower error level to warning if the function used was ini_get.
if ($errorInfo['error'] === true && $itemInfo['functionLc'] === 'ini_get') {
$errorInfo['error'] = false;
}
return $errorInfo;
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return "INI directive '%s' is ";
}
/**
* Get the error message template for suggesting an alternative for a specific sniff.
*
* @return string
*/
protected function getAlternativeOptionTemplate()
{
return str_replace("%s", "'%s'", parent::getAlternativeOptionTemplate());
}
}
@@ -0,0 +1,78 @@
<?php
/**
* \PHPCompatibility\Sniffs\InitialValue\NewConstantArraysUsingConstSniff.
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\InitialValue;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\InitialValue\NewConstantArraysUsingConstSniff.
*
* Constant arrays using the const keyword in PHP 5.6
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewConstantArraysUsingConstSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CONST);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.5') !== true) {
return;
}
$tokens = $phpcsFile->getTokens();
$find = array(
T_ARRAY => T_ARRAY,
T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY,
);
while (($hasArray = $phpcsFile->findNext($find, ($stackPtr + 1), null, false, null, true)) !== false) {
$phpcsFile->addError(
'Constant arrays using the "const" keyword are not allowed in PHP 5.5 or earlier',
$hasArray,
'Found'
);
// Skip past the content of the array.
$stackPtr = $hasArray;
if ($tokens[$hasArray]['code'] === T_OPEN_SHORT_ARRAY && isset($tokens[$hasArray]['bracket_closer'])) {
$stackPtr = $tokens[$hasArray]['bracket_closer'];
} elseif ($tokens[$hasArray]['code'] === T_ARRAY && isset($tokens[$hasArray]['parenthesis_closer'])) {
$stackPtr = $tokens[$hasArray]['parenthesis_closer'];
}
}
}
}
@@ -0,0 +1,97 @@
<?php
/**
* \PHPCompatibility\Sniffs\InitialValue\NewConstantArraysUsingDefineSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\InitialValue;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\InitialValue\NewConstantArraysUsingDefineSniff.
*
* Constant arrays using define in PHP 7.0
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class NewConstantArraysUsingDefineSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.6') !== true) {
return;
}
$tokens = $phpcsFile->getTokens();
$ignore = array(
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CONST => true,
);
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}
$functionLc = strtolower($tokens[$stackPtr]['content']);
if ($functionLc !== 'define') {
return;
}
$secondParam = $this->getFunctionCallParameter($phpcsFile, $stackPtr, 2);
if (isset($secondParam['start'], $secondParam['end']) === false) {
return;
}
$targetNestingLevel = 0;
if (isset($tokens[$secondParam['start']]['nested_parenthesis'])) {
$targetNestingLevel = count($tokens[$secondParam['start']]['nested_parenthesis']);
}
$array = $phpcsFile->findNext(array(T_ARRAY, T_OPEN_SHORT_ARRAY), $secondParam['start'], ($secondParam['end'] + 1));
if ($array !== false) {
if ((isset($tokens[$array]['nested_parenthesis']) === false && $targetNestingLevel === 0) || count($tokens[$array]['nested_parenthesis']) === $targetNestingLevel) {
$phpcsFile->addError(
'Constant arrays using define are not allowed in PHP 5.6 or earlier',
$array,
'Found'
);
}
}
}
}
@@ -0,0 +1,534 @@
<?php
/**
* \PHPCompatibility\Sniffs\InitialValue\NewConstantScalarExpressionsSniff.
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\InitialValue;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\InitialValue\NewConstantScalarExpressionsSniff.
*
* Since PHP 5.6, it is now possible to provide a scalar expression involving
* numeric and string literals and/or constants in contexts where PHP previously
* expected a static value, such as constant and property declarations and
* default function arguments.
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewConstantScalarExpressionsSniff extends Sniff
{
/**
* Error message.
*
* @var string
*/
const ERROR_PHRASE = 'Constant scalar expressions are not allowed %s in PHP 5.5 or earlier.';
/**
* Partial error phrases to be used in combination with the error message constant.
*
* @var array
*/
protected $errorPhrases = array(
'const' => 'when defining constants using the const keyword',
'property' => 'in property declarations',
'staticvar' => 'in static variable declarations',
'default' => 'in default function arguments',
);
/**
* Tokens which were allowed to be used in these declarations prior to PHP 5.6.
*
* This list will be enriched in the setProperties() method.
*
* @var array
*/
protected $safeOperands = array(
T_LNUMBER => T_LNUMBER,
T_DNUMBER => T_DNUMBER,
T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING,
T_TRUE => T_TRUE,
T_FALSE => T_FALSE,
T_NULL => T_NULL,
T_LINE => T_LINE,
T_FILE => T_FILE,
T_DIR => T_DIR,
T_FUNC_C => T_FUNC_C,
T_CLASS_C => T_CLASS_C,
T_TRAIT_C => T_TRAIT_C,
T_METHOD_C => T_METHOD_C,
T_NS_C => T_NS_C,
// Special cases:
T_NS_SEPARATOR => T_NS_SEPARATOR,
/*
* This can be neigh anything, but for any usage except constants,
* the T_STRING will be combined with non-allowed tokens, so we should be good.
*/
T_STRING => T_STRING,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Set the properties up only once.
$this->setProperties();
return array(
T_CONST,
T_VARIABLE,
T_FUNCTION,
T_CLOSURE,
T_STATIC,
);
}
/**
* Make some adjustments to the $safeOperands property.
*
* @return void
*/
public function setProperties()
{
$this->safeOperands += Tokens::$heredocTokens;
$this->safeOperands += Tokens::$emptyTokens;
}
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('5.5') !== true);
}
/**
* 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|int Null or integer stack pointer to skip forward.
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($this->bowOutEarly() === true) {
return;
}
$tokens = $phpcsFile->getTokens();
switch ($tokens[$stackPtr]['type']) {
case 'T_FUNCTION':
case 'T_CLOSURE':
$params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($params)) {
// No parameters.
return;
}
$funcToken = $tokens[$stackPtr];
if (isset($funcToken['parenthesis_owner'], $funcToken['parenthesis_opener'], $funcToken['parenthesis_closer']) === false
|| $funcToken['parenthesis_owner'] !== $stackPtr
|| isset($tokens[$funcToken['parenthesis_opener']], $tokens[$funcToken['parenthesis_closer']]) === false
) {
// Hmm.. something is going wrong as these should all be available & valid.
return;
}
$opener = $funcToken['parenthesis_opener'];
$closer = $funcToken['parenthesis_closer'];
// Which nesting level is the one we are interested in ?
$nestedParenthesisCount = 1;
if (isset($tokens[$opener]['nested_parenthesis'])) {
$nestedParenthesisCount += count($tokens[$opener]['nested_parenthesis']);
}
foreach ($params as $param) {
if (isset($param['default']) === false) {
continue;
}
$end = $param['token'];
while (($end = $phpcsFile->findNext(array(T_COMMA, T_CLOSE_PARENTHESIS), ($end + 1), ($closer + 1))) !== false) {
$maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $nestedParenthesisCount);
if ($maybeSkipTo !== true) {
$end = $maybeSkipTo;
continue;
}
// Ignore closing parenthesis/bracket if not 'ours'.
if ($tokens[$end]['code'] === T_CLOSE_PARENTHESIS && $end !== $closer) {
continue;
}
// Ok, we've found the end of the param default value declaration.
break;
}
if ($this->isValidAssignment($phpcsFile, $param['token'], $end) === false) {
$this->throwError($phpcsFile, $param['token'], 'default', $param['content']);
}
}
/*
* No need for the sniff to be triggered by the T_VARIABLEs in the function
* definition as we've already examined them above, so let's skip over them.
*/
return $closer;
case 'T_VARIABLE':
case 'T_STATIC':
case 'T_CONST':
$type = 'const';
// Filter out non-property declarations.
if ($tokens[$stackPtr]['code'] === T_VARIABLE) {
if ($this->isClassProperty($phpcsFile, $stackPtr) === false) {
return;
}
$type = 'property';
// Move back one token to have the same starting point as the others.
$stackPtr = ($stackPtr - 1);
}
// Filter out late static binding and class properties.
if ($tokens[$stackPtr]['code'] === T_STATIC) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($next === false || $tokens[$next]['code'] !== T_VARIABLE) {
// Late static binding.
return;
}
if ($this->isClassProperty($phpcsFile, $next) === true) {
// Class properties are examined based on the T_VARIABLE token.
return;
}
unset($next);
$type = 'staticvar';
}
$endOfStatement = $phpcsFile->findNext(array(T_SEMICOLON, T_CLOSE_TAG), ($stackPtr + 1));
if ($endOfStatement === false) {
// No semi-colon - live coding.
return;
}
$targetNestingLevel = 0;
if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
$targetNestingLevel = count($tokens[$stackPtr]['nested_parenthesis']);
}
// Examine each variable/constant in multi-declarations.
$start = $stackPtr;
$end = $stackPtr;
while (($end = $phpcsFile->findNext(array(T_COMMA, T_SEMICOLON, T_OPEN_SHORT_ARRAY, T_CLOSE_TAG), ($end + 1), ($endOfStatement + 1))) !== false) {
$maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $targetNestingLevel);
if ($maybeSkipTo !== true) {
$end = $maybeSkipTo;
continue;
}
$start = $phpcsFile->findNext(Tokens::$emptyTokens, ($start + 1), $end, true);
if ($start === false
|| ($tokens[$stackPtr]['code'] === T_CONST && $tokens[$start]['code'] !== T_STRING)
|| ($tokens[$stackPtr]['code'] !== T_CONST && $tokens[$start]['code'] !== T_VARIABLE)
) {
// Shouldn't be possible.
continue;
}
if ($this->isValidAssignment($phpcsFile, $start, $end) === false) {
// Create the "found" snippet.
$content = '';
$tokenCount = ($end - $start);
if ($tokenCount < 20) {
// Prevent large arrays from being added to the error message.
$content = $phpcsFile->getTokensAsString($start, ($tokenCount + 1));
}
$this->throwError($phpcsFile, $start, $type, $content);
}
$start = $end;
}
// Skip to the end of the statement to prevent duplicate messages for multi-declarations.
return $endOfStatement;
}
}
/**
* Is a value declared and is the value declared valid pre-PHP 5.6 ?
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param int $end The end of the value definition.
* This will normally be a comma or semi-colon.
*
* @return bool
*/
protected function isValidAssignment(File $phpcsFile, $stackPtr, $end)
{
$tokens = $phpcsFile->getTokens();
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $end, true);
if ($next === false || $tokens[$next]['code'] !== T_EQUAL) {
// No value assigned.
return true;
}
return $this->isStaticValue($phpcsFile, $tokens, ($next + 1), ($end - 1));
}
/**
* Is a value declared and is the value declared constant as accepted in PHP 5.5 and lower ?
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param array $tokens The token stack of the current file.
* @param int $start The stackPtr from which to start examining.
* @param int $end The end of the value definition (inclusive),
* i.e. this token will be examined as part of
* the snippet.
* @param bool $nestedArrays Optional. Array nesting level when examining
* the content of an array.
*
* @return bool
*/
protected function isStaticValue(File $phpcsFile, $tokens, $start, $end, $nestedArrays = 0)
{
$nextNonSimple = $phpcsFile->findNext($this->safeOperands, $start, ($end + 1), true);
if ($nextNonSimple === false) {
return true;
}
/*
* OK, so we have at least one token which needs extra examination.
*/
switch ($tokens[$nextNonSimple]['code']) {
case T_MINUS:
case T_PLUS:
if ($this->isNumber($phpcsFile, $start, $end, true) !== false) {
// Int or float with sign.
return true;
}
return false;
case T_NAMESPACE:
case T_PARENT:
case T_SELF:
case T_DOUBLE_COLON:
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
if ($tokens[$nextNonSimple]['code'] === T_NAMESPACE) {
// Allow only `namespace\...`.
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_NS_SEPARATOR) {
return false;
}
} elseif ($tokens[$nextNonSimple]['code'] === T_PARENT
|| $tokens[$nextNonSimple]['code'] === T_SELF
) {
// Allow only `parent::` and `self::`.
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_DOUBLE_COLON) {
return false;
}
} elseif ($tokens[$nextNonSimple]['code'] === T_DOUBLE_COLON) {
// Allow only `T_STRING::T_STRING`.
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_STRING) {
return false;
}
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($nextNonSimple - 1), null, true);
// No need to worry about parent/self, that's handled above and
// the double colon is skipped over in that case.
if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== T_STRING) {
return false;
}
}
// Examine what comes after the namespace/parent/self/double colon, if anything.
return $this->isStaticValue($phpcsFile, $tokens, ($nextNonEmpty + 1), $end, $nestedArrays);
case T_ARRAY:
case T_OPEN_SHORT_ARRAY:
++$nestedArrays;
$arrayItems = $this->getFunctionCallParameters($phpcsFile, $nextNonSimple);
if (empty($arrayItems) === false) {
foreach ($arrayItems as $item) {
// Check for a double arrow, but only if it's for this array item, not for a nested array.
$doubleArrow = false;
$maybeDoubleArrow = $phpcsFile->findNext(
array(T_DOUBLE_ARROW, T_ARRAY, T_OPEN_SHORT_ARRAY),
$item['start'],
($item['end'] + 1)
);
if ($maybeDoubleArrow !== false && $tokens[$maybeDoubleArrow]['code'] === T_DOUBLE_ARROW) {
// Double arrow is for this nesting level.
$doubleArrow = $maybeDoubleArrow;
}
if ($doubleArrow === false) {
if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], $item['end'], $nestedArrays) === false) {
return false;
}
} else {
// Examine array key.
if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], ($doubleArrow - 1), $nestedArrays) === false) {
return false;
}
// Examine array value.
if ($this->isStaticValue($phpcsFile, $tokens, ($doubleArrow + 1), $item['end'], $nestedArrays) === false) {
return false;
}
}
}
}
--$nestedArrays;
/*
* Find the end of the array.
* We already know we will have a valid closer as otherwise we wouldn't have been
* able to get the array items.
*/
$closer = ($nextNonSimple + 1);
if ($tokens[$nextNonSimple]['code'] === T_OPEN_SHORT_ARRAY
&& isset($tokens[$nextNonSimple]['bracket_closer']) === true
) {
$closer = $tokens[$nextNonSimple]['bracket_closer'];
} else {
$maybeOpener = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
if ($tokens[$maybeOpener]['code'] === T_OPEN_PARENTHESIS) {
$opener = $maybeOpener;
if (isset($tokens[$opener]['parenthesis_closer']) === true) {
$closer = $tokens[$opener]['parenthesis_closer'];
}
}
}
if ($closer === $end) {
return true;
}
// Examine what comes after the array, if anything.
return $this->isStaticValue($phpcsFile, $tokens, ($closer + 1), $end, $nestedArrays);
}
// Ok, so this unsafe token was not one of the exceptions, i.e. this is a PHP 5.6+ syntax.
return false;
}
/**
* Throw an error if a scalar expression is found.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token to link the error to.
* @param string $type Type of usage found.
* @param string $content Optional. The value for the declaration as found.
*
* @return void
*/
protected function throwError(File $phpcsFile, $stackPtr, $type, $content = '')
{
$error = static::ERROR_PHRASE;
$phrase = '';
$errorCode = 'Found';
if (isset($this->errorPhrases[$type]) === true) {
$errorCode = $this->stringToErrorCode($type) . 'Found';
$phrase = $this->errorPhrases[$type];
}
$data = array($phrase);
if (empty($content) === false) {
$error .= ' Found: %s';
$data[] = $content;
}
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
/**
* Helper function to find the end of multi variable/constant declarations.
*
* Checks whether a certain part of a declaration needs to be skipped over or
* if it is the real end of the declaration.
*
* @param array $tokens Token stack of the current file.
* @param int $endPtr The token to examine as a candidate end pointer.
* @param int $targetLevel Target nesting level.
*
* @return bool|int True if this is the real end. Int stackPtr to skip to if not.
*/
private function isRealEndOfDeclaration($tokens, $endPtr, $targetLevel)
{
// Ignore anything within short array definition brackets for now.
if ($tokens[$endPtr]['code'] === T_OPEN_SHORT_ARRAY
&& (isset($tokens[$endPtr]['bracket_opener'])
&& $tokens[$endPtr]['bracket_opener'] === $endPtr)
&& isset($tokens[$endPtr]['bracket_closer'])
) {
// Skip forward to the end of the short array definition.
return $tokens[$endPtr]['bracket_closer'];
}
// Skip past comma's at a lower nesting level.
if ($tokens[$endPtr]['code'] === T_COMMA) {
// Check if a comma is at the nesting level we're targetting.
$nestingLevel = 0;
if (isset($tokens[$endPtr]['nested_parenthesis']) === true) {
$nestingLevel = count($tokens[$endPtr]['nested_parenthesis']);
}
if ($nestingLevel > $targetLevel) {
return $endPtr;
}
}
return true;
}
}
@@ -0,0 +1,90 @@
<?php
/**
* \PHPCompatibility\Sniffs\InitialValue\NewHeredocSniff.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\InitialValue;
use PHPCompatibility\Sniffs\InitialValue\NewConstantScalarExpressionsSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\InitialValue\NewHeredocSniff.
*
* As of PHP 5.3.0, it's possible to initialize static variables, class properties
* and constants declared using the `const` keyword, using the Heredoc syntax.
* And while not documented, heredoc initialization can now also be used for function param defaults.
* See: https://3v4l.org/JVH8W
*
* These heredocs (still) cannot contain variables. That's, however, outside the scope of the
* PHPCompatibility library until such time as there is a PHP version in which this would be accepted.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewHeredocSniff extends NewConstantScalarExpressionsSniff
{
/**
* Error message.
*
* @var string
*/
const ERROR_PHRASE = 'Initializing %s using the Heredoc syntax was not supported in PHP 5.2 or earlier';
/**
* Partial error phrases to be used in combination with the error message constant.
*
* @var array
*/
protected $errorPhrases = array(
'const' => 'constants',
'property' => 'class properties',
'staticvar' => 'static variables',
'default' => 'default parameter values',
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('5.2') !== true);
}
/**
* Is a value declared and does the declared value not contain an heredoc ?
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param int $end The end of the value definition.
*
* @return bool True if no heredoc (or assignment) is found, false otherwise.
*/
protected function isValidAssignment(File $phpcsFile, $stackPtr, $end)
{
$tokens = $phpcsFile->getTokens();
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $end, true);
if ($next === false || $tokens[$next]['code'] !== T_EQUAL) {
// No value assigned.
return true;
}
return ($phpcsFile->findNext(T_START_HEREDOC, ($next + 1), $end, false, null, true) === false);
}
}
@@ -0,0 +1,92 @@
<?php
/**
* \PHPCompatibility\Sniffs\Interfaces\InternalInterfacesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Interfaces;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Interfaces\InternalInterfacesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class InternalInterfacesSniff extends Sniff
{
/**
* A list of PHP internal interfaces, not intended to be implemented by userland classes.
*
* The array lists : the error message to use.
*
* @var array(string => string)
*/
protected $internalInterfaces = array(
'Traversable' => 'shouldn\'t be implemented directly, implement the Iterator or IteratorAggregate interface instead.',
'DateTimeInterface' => 'is intended for type hints only and is not implementable.',
'Throwable' => 'cannot be implemented directly, extend the Exception class instead.',
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of interface names.
$this->internalInterfaces = $this->arrayKeysToLowercase($this->internalInterfaces);
$targets = array(T_CLASS);
if (defined('T_ANON_CLASS')) {
$targets[] = T_ANON_CLASS;
}
return $targets;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$interfaces = PHPCSHelper::findImplementedInterfaceNames($phpcsFile, $stackPtr);
if (is_array($interfaces) === false || $interfaces === array()) {
return;
}
foreach ($interfaces as $interface) {
$interface = ltrim($interface, '\\');
$interfaceLc = strtolower($interface);
if (isset($this->internalInterfaces[$interfaceLc]) === true) {
$error = 'The interface %s %s';
$errorCode = $this->stringToErrorCode($interfaceLc) . 'Found';
$data = array(
$interface,
$this->internalInterfaces[$interfaceLc],
);
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
}
}
}
@@ -0,0 +1,336 @@
<?php
/**
* \PHPCompatibility\Sniffs\Interfaces\NewInterfacesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Interfaces;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Interfaces\NewInterfacesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewInterfacesSniff extends AbstractNewFeatureSniff
{
/**
* A list of new interfaces, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the interface appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newInterfaces = array(
'Traversable' => array(
'4.4' => false,
'5.0' => true,
),
'Reflector' => array(
'4.4' => false,
'5.0' => true,
),
'Countable' => array(
'5.0' => false,
'5.1' => true,
),
'OuterIterator' => array(
'5.0' => false,
'5.1' => true,
),
'RecursiveIterator' => array(
'5.0' => false,
'5.1' => true,
),
'SeekableIterator' => array(
'5.0' => false,
'5.1' => true,
),
'Serializable' => array(
'5.0' => false,
'5.1' => true,
),
'SplObserver' => array(
'5.0' => false,
'5.1' => true,
),
'SplSubject' => array(
'5.0' => false,
'5.1' => true,
),
'JsonSerializable' => array(
'5.3' => false,
'5.4' => true,
),
'SessionHandlerInterface' => array(
'5.3' => false,
'5.4' => true,
),
'DateTimeInterface' => array(
'5.4' => false,
'5.5' => true,
),
'SessionIdInterface' => array(
'5.5.0' => false,
'5.5.1' => true,
),
'Throwable' => array(
'5.6' => false,
'7.0' => true,
),
'SessionUpdateTimestampHandlerInterface' => array(
'5.6' => false,
'7.0' => true,
),
);
/**
* A list of methods which cannot be used in combination with particular interfaces.
*
* @var array(string => array(string => string))
*/
protected $unsupportedMethods = array(
'Serializable' => array(
'__sleep' => 'http://php.net/serializable',
'__wakeup' => 'http://php.net/serializable',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of interface names.
$this->newInterfaces = $this->arrayKeysToLowercase($this->newInterfaces);
$this->unsupportedMethods = $this->arrayKeysToLowercase($this->unsupportedMethods);
$targets = array(
T_CLASS,
T_FUNCTION,
T_CLOSURE,
);
if (defined('T_ANON_CLASS')) {
$targets[] = T_ANON_CLASS;
}
if (defined('T_RETURN_TYPE')) {
$targets[] = T_RETURN_TYPE;
}
return $targets;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
switch ($tokens[$stackPtr]['type']) {
case 'T_CLASS':
case 'T_ANON_CLASS':
$this->processClassToken($phpcsFile, $stackPtr);
break;
case 'T_FUNCTION':
case 'T_CLOSURE':
$this->processFunctionToken($phpcsFile, $stackPtr);
// Deal with older PHPCS versions which don't recognize return type hints
// as well as newer PHPCS versions (3.3.0+) where the tokenization has changed.
$returnTypeHint = $this->getReturnTypeHintToken($phpcsFile, $stackPtr);
if ($returnTypeHint !== false) {
$this->processReturnTypeToken($phpcsFile, $returnTypeHint);
}
break;
case 'T_RETURN_TYPE':
$this->processReturnTypeToken($phpcsFile, $stackPtr);
break;
default:
// Deliberately left empty.
break;
}
}
/**
* Processes this test for when a class token is encountered.
*
* - Detect classes implementing the new interfaces.
* - Detect classes implementing the new interfaces with unsupported functions.
*
* @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
*/
private function processClassToken(File $phpcsFile, $stackPtr)
{
$interfaces = PHPCSHelper::findImplementedInterfaceNames($phpcsFile, $stackPtr);
if (is_array($interfaces) === false || $interfaces === array()) {
return;
}
$tokens = $phpcsFile->getTokens();
$checkMethods = false;
if (isset($tokens[$stackPtr]['scope_closer'])) {
$checkMethods = true;
$scopeCloser = $tokens[$stackPtr]['scope_closer'];
}
foreach ($interfaces as $interface) {
$interface = ltrim($interface, '\\');
$interfaceLc = strtolower($interface);
if (isset($this->newInterfaces[$interfaceLc]) === true) {
$itemInfo = array(
'name' => $interface,
'nameLc' => $interfaceLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
if ($checkMethods === true && isset($this->unsupportedMethods[$interfaceLc]) === true) {
$nextFunc = $stackPtr;
while (($nextFunc = $phpcsFile->findNext(T_FUNCTION, ($nextFunc + 1), $scopeCloser)) !== false) {
$funcName = $phpcsFile->getDeclarationName($nextFunc);
$funcNameLc = strtolower($funcName);
if ($funcNameLc === '') {
continue;
}
if (isset($this->unsupportedMethods[$interfaceLc][$funcNameLc]) === true) {
$error = 'Classes that implement interface %s do not support the method %s(). See %s';
$errorCode = $this->stringToErrorCode($interface) . 'UnsupportedMethod';
$data = array(
$interface,
$funcName,
$this->unsupportedMethods[$interfaceLc][$funcNameLc],
);
$phpcsFile->addError($error, $nextFunc, $errorCode, $data);
}
}
}
}
}
/**
* Processes this test for when a function token is encountered.
*
* - Detect new interfaces when used as a type hint.
*
* @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
*/
private function processFunctionToken(File $phpcsFile, $stackPtr)
{
$typeHints = $this->getTypeHintsFromFunctionDeclaration($phpcsFile, $stackPtr);
if (empty($typeHints) || is_array($typeHints) === false) {
return;
}
foreach ($typeHints as $hint) {
$typeHintLc = strtolower($hint);
if (isset($this->newInterfaces[$typeHintLc]) === true) {
$itemInfo = array(
'name' => $hint,
'nameLc' => $typeHintLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
}
}
/**
* Processes this test for when a return type token is encountered.
*
* - Detect new interfaces when used as a return type declaration.
*
* @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
*/
private function processReturnTypeToken(File $phpcsFile, $stackPtr)
{
$returnTypeHint = $this->getReturnTypeHintName($phpcsFile, $stackPtr);
$returnTypeHint = ltrim($returnTypeHint, '\\');
$returnTypeHintLc = strtolower($returnTypeHint);
if (isset($this->newInterfaces[$returnTypeHintLc]) === false) {
return;
}
// Still here ? Then this is a return type declaration using a new interface.
$itemInfo = array(
'name' => $returnTypeHint,
'nameLc' => $returnTypeHintLc,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newInterfaces[$itemInfo['nameLc']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The built-in interface ' . parent::getErrorMsgTemplate();
}
}
@@ -0,0 +1,73 @@
<?php
/**
* \PHPCompatibility\Sniffs\Keywords\CaseSensitiveKeywordsSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Keywords;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Keywords\CaseSensitiveKeywordsSniff.
*
* Prior to PHP 5.5, cases existed where the self, parent, and static keywords
* were treated in a case sensitive fashion.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class CaseSensitiveKeywordsSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_SELF,
T_STATIC,
T_PARENT,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$tokenContentLC = strtolower($tokens[$stackPtr]['content']);
if ($tokenContentLC !== $tokens[$stackPtr]['content']) {
$phpcsFile->addError(
'The keyword \'%s\' was treated in a case-sensitive fashion in certain cases in PHP 5.4 or earlier. Use the lowercase version for consistent support.',
$stackPtr,
'NonLowercaseFound',
array($tokenContentLC)
);
}
}
}
@@ -0,0 +1,248 @@
<?php
/**
* \PHPCompatibility\Sniffs\Keywords\ForbiddenNamesAsDeclaredClassSniff.
*
* PHP version 7.0+
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Keywords;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Keywords\ForbiddenNamesAsDeclaredClassSniff.
*
* Prohibits the use of some reserved keywords to name a class, interface, trait or namespace.
* Emits errors for reserved words and warnings for soft-reserved words.
*
* @see http://php.net/manual/en/reserved.other-reserved-words.php
*
* PHP version 7.0+
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ForbiddenNamesAsDeclaredSniff extends Sniff
{
/**
* List of tokens which can not be used as class, interface, trait names or as part of a namespace.
*
* @var array
*/
protected $forbiddenTokens = array(
T_NULL => '7.0',
T_TRUE => '7.0',
T_FALSE => '7.0',
);
/**
* T_STRING keywords to recognize as forbidden names.
*
* @var array
*/
protected $forbiddenNames = array(
'null' => '7.0',
'true' => '7.0',
'false' => '7.0',
'bool' => '7.0',
'int' => '7.0',
'float' => '7.0',
'string' => '7.0',
'iterable' => '7.1',
'void' => '7.1',
'object' => '7.2',
);
/**
* T_STRING keywords to recognize as soft reserved names.
*
* Using any of these keywords to name a class, interface, trait or namespace
* is highly discouraged since they may be used in future versions of PHP.
*
* @var array
*/
protected $softReservedNames = array(
'resource' => '7.0',
'object' => '7.0',
'mixed' => '7.0',
'numeric' => '7.0',
);
/**
* Combined list of the two lists above.
*
* Used for quick check whether or not something is a reserved
* word.
* Set from the `register()` method.
*
* @var array
*/
private $allForbiddenNames = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Do the list merge only once.
$this->allForbiddenNames = array_merge($this->forbiddenNames, $this->softReservedNames);
$targets = array(
T_CLASS,
T_INTERFACE,
T_TRAIT,
T_NAMESPACE,
T_STRING, // Compat for PHPCS < 2.4.0 and PHP < 5.3.
);
return $targets;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$tokenCode = $tokens[$stackPtr]['code'];
$tokenType = $tokens[$stackPtr]['type'];
$tokenContentLc = strtolower($tokens[$stackPtr]['content']);
// For string tokens we only care about 'trait' as that is the only one
// which may not be correctly recognized as it's own token.
// This only happens in older versions of PHP where the token doesn't exist yet as a keyword.
if ($tokenCode === T_STRING && $tokenContentLc !== 'trait') {
return;
}
if (in_array($tokenType, array('T_CLASS', 'T_INTERFACE', 'T_TRAIT'), true)) {
// Check for the declared name being a name which is not tokenized as T_STRING.
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty !== false && isset($this->forbiddenTokens[$tokens[$nextNonEmpty]['code']]) === true) {
$name = $tokens[$nextNonEmpty]['content'];
} else {
// Get the declared name if it's a T_STRING.
$name = $phpcsFile->getDeclarationName($stackPtr);
}
unset($nextNonEmpty);
if (isset($name) === false || is_string($name) === false || $name === '') {
return;
}
$nameLc = strtolower($name);
if (isset($this->allForbiddenNames[$nameLc]) === false) {
return;
}
} elseif ($tokenCode === T_NAMESPACE) {
$namespaceName = $this->getDeclaredNamespaceName($phpcsFile, $stackPtr);
if ($namespaceName === false || $namespaceName === '') {
return;
}
$namespaceParts = explode('\\', $namespaceName);
foreach ($namespaceParts as $namespacePart) {
$partLc = strtolower($namespacePart);
if (isset($this->allForbiddenNames[$partLc]) === true) {
$name = $namespacePart;
$nameLc = $partLc;
break;
}
}
} elseif ($tokenCode === T_STRING) {
// Traits which are not yet tokenized as T_TRAIT.
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false) {
return;
}
$nextNonEmptyCode = $tokens[$nextNonEmpty]['code'];
if ($nextNonEmptyCode !== T_STRING && isset($this->forbiddenTokens[$nextNonEmptyCode]) === true) {
$name = $tokens[$nextNonEmpty]['content'];
$nameLc = strtolower($tokens[$nextNonEmpty]['content']);
} elseif ($nextNonEmptyCode === T_STRING) {
$endOfStatement = $phpcsFile->findNext(array(T_SEMICOLON, T_OPEN_CURLY_BRACKET), ($stackPtr + 1));
if ($endOfStatement === false) {
return;
}
do {
$nextNonEmptyLc = strtolower($tokens[$nextNonEmpty]['content']);
if (isset($this->allForbiddenNames[$nextNonEmptyLc]) === true) {
$name = $tokens[$nextNonEmpty]['content'];
$nameLc = $nextNonEmptyLc;
break;
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $endOfStatement, true);
} while ($nextNonEmpty !== false);
}
unset($nextNonEmptyCode, $nextNonEmptyLc, $endOfStatement);
}
if (isset($name, $nameLc) === false) {
return;
}
// Still here, so this is one of the reserved words.
// Build up the error message.
$error = "'%s' is a";
$isError = null;
$errorCode = $this->stringToErrorCode($nameLc) . 'Found';
$data = array(
$nameLc,
);
if (isset($this->softReservedNames[$nameLc]) === true
&& $this->supportsAbove($this->softReservedNames[$nameLc]) === true
) {
$error .= ' soft reserved keyword as of PHP version %s';
$isError = false;
$data[] = $this->softReservedNames[$nameLc];
}
if (isset($this->forbiddenNames[$nameLc]) === true
&& $this->supportsAbove($this->forbiddenNames[$nameLc]) === true
) {
if (isset($isError) === true) {
$error .= ' and a';
}
$error .= ' reserved keyword as of PHP version %s';
$isError = true;
$data[] = $this->forbiddenNames[$nameLc];
}
if (isset($isError) === true) {
$error .= ' and should not be used to name a class, interface or trait or as part of a namespace (%s)';
$data[] = $tokens[$stackPtr]['type'];
$this->addMessage($phpcsFile, $error, $stackPtr, $isError, $errorCode, $data);
}
}
}
@@ -0,0 +1,180 @@
<?php
/**
* \PHPCompatibility\Sniffs\Keywords\ForbiddenNamesAsInvokedFunctionsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Jansen Price <jansen.price@gmail.com>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Keywords;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Keywords\ForbiddenNamesAsInvokedFunctionsSniff.
*
* Prohibits the use of reserved keywords invoked as functions.
*
* @category PHP
* @package PHPCompatibility
* @author Jansen Price <jansen.price@gmail.com>
* @copyright 2012 Cu.be Solutions bvba
*/
class ForbiddenNamesAsInvokedFunctionsSniff extends Sniff
{
/**
* List of tokens to register.
*
* @var array
*/
protected $targetedTokens = array(
T_ABSTRACT => '5.0',
T_CALLABLE => '5.4',
T_CATCH => '5.0',
T_FINAL => '5.0',
T_FINALLY => '5.5',
T_GOTO => '5.3',
T_IMPLEMENTS => '5.0',
T_INTERFACE => '5.0',
T_INSTANCEOF => '5.0',
T_INSTEADOF => '5.4',
T_NAMESPACE => '5.3',
T_PRIVATE => '5.0',
T_PROTECTED => '5.0',
T_PUBLIC => '5.0',
T_TRAIT => '5.4',
T_TRY => '5.0',
);
/**
* T_STRING keywords to recognize as targetted tokens.
*
* Compatibility for PHP versions where the keyword is not yet recognized
* as its own token and for PHPCS versions which change the token to
* T_STRING when used in a method call.
*
* @var array
*/
protected $targetedStringTokens = array(
'abstract' => '5.0',
'callable' => '5.4',
'catch' => '5.0',
'final' => '5.0',
'finally' => '5.5',
'goto' => '5.3',
'implements' => '5.0',
'interface' => '5.0',
'instanceof' => '5.0',
'insteadof' => '5.4',
'namespace' => '5.3',
'private' => '5.0',
'protected' => '5.0',
'public' => '5.0',
'trait' => '5.4',
'try' => '5.0',
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array_keys($this->targetedTokens);
$tokens[] = T_STRING;
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenCode = $tokens[$stackPtr]['code'];
$tokenContentLc = strtolower($tokens[$stackPtr]['content']);
$isString = false;
/*
* For string tokens we only care if the string is a reserved word used
* as a function. This only happens in older versions of PHP where the
* token doesn't exist yet for that keyword or in later versions when the
* token is used in a method invocation.
*/
if ($tokenCode === T_STRING
&& (isset($this->targetedStringTokens[$tokenContentLc]) === false)
) {
return;
}
if ($tokenCode === T_STRING) {
$isString = true;
}
// Make sure this is a function call.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($next === false || $tokens[$next]['code'] !== T_OPEN_PARENTHESIS) {
// Not a function call.
return;
}
// This sniff isn't concerned about function declarations.
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prev !== false && $tokens[$prev]['code'] === T_FUNCTION) {
return;
}
/*
* Deal with PHP 7 relaxing the rules.
* "As of PHP 7.0.0 these keywords are allowed as property, constant, and method names
* of classes, interfaces and traits...", i.e. they can be invoked as a method call.
*
* Only needed for those keywords which we sniff out via T_STRING.
*/
if (($tokens[$prev]['code'] === T_OBJECT_OPERATOR || $tokens[$prev]['code'] === T_DOUBLE_COLON)
&& $this->supportsBelow('5.6') === false
) {
return;
}
// For the word catch, it is valid to have an open parenthesis
// after it, but only if it is preceded by a right curly brace.
if ($tokenCode === T_CATCH) {
if ($prev !== false && $tokens[$prev]['code'] === T_CLOSE_CURLY_BRACKET) {
// Ok, it's fine.
return;
}
}
if ($isString === true) {
$version = $this->targetedStringTokens[$tokenContentLc];
} else {
$version = $this->targetedTokens[$tokenCode];
}
if ($this->supportsAbove($version)) {
$error = "'%s' is a reserved keyword introduced in PHP version %s and cannot be invoked as a function (%s)";
$errorCode = $this->stringToErrorCode($tokenContentLc) . 'Found';
$data = array(
$tokenContentLc,
$version,
$tokens[$stackPtr]['type'],
);
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
}
}
@@ -0,0 +1,391 @@
<?php
/**
* \PHPCompatibility\Sniffs\Keywords\ForbiddenNamesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Keywords;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Keywords\ForbiddenNamesSniff.
*
* Prohibits the use of reserved keywords as class, function, namespace or constant names.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class ForbiddenNamesSniff extends Sniff
{
/**
* A list of keywords that can not be used as function, class and namespace name or constant name.
* Mentions since which version it's not allowed.
*
* @var array(string => string)
*/
protected $invalidNames = array(
'abstract' => '5.0',
'and' => 'all',
'array' => 'all',
'as' => 'all',
'break' => 'all',
'callable' => '5.4',
'case' => 'all',
'catch' => '5.0',
'class' => 'all',
'clone' => '5.0',
'const' => 'all',
'continue' => 'all',
'declare' => 'all',
'default' => 'all',
'do' => 'all',
'else' => 'all',
'elseif' => 'all',
'enddeclare' => 'all',
'endfor' => 'all',
'endforeach' => 'all',
'endif' => 'all',
'endswitch' => 'all',
'endwhile' => 'all',
'extends' => 'all',
'final' => '5.0',
'finally' => '5.5',
'for' => 'all',
'foreach' => 'all',
'function' => 'all',
'global' => 'all',
'goto' => '5.3',
'if' => 'all',
'implements' => '5.0',
'interface' => '5.0',
'instanceof' => '5.0',
'insteadof' => '5.4',
'namespace' => '5.3',
'new' => 'all',
'or' => 'all',
'private' => '5.0',
'protected' => '5.0',
'public' => '5.0',
'static' => 'all',
'switch' => 'all',
'throw' => '5.0',
'trait' => '5.4',
'try' => '5.0',
'use' => 'all',
'var' => 'all',
'while' => 'all',
'xor' => 'all',
'__class__' => 'all',
'__dir__' => '5.3',
'__file__' => 'all',
'__function__' => 'all',
'__method__' => 'all',
'__namespace__' => '5.3',
);
/**
* A list of keywords that can follow use statements.
*
* @var array(string => string)
*/
protected $validUseNames = array(
'const' => true,
'function' => true,
);
/**
* Scope modifiers and other keywords allowed in trait use statements.
*
* @var array
*/
private $allowedModifiers = array();
/**
* Targeted tokens.
*
* @var array
*/
protected $targetedTokens = array(
T_CLASS,
T_FUNCTION,
T_NAMESPACE,
T_STRING,
T_CONST,
T_USE,
T_AS,
T_EXTENDS,
T_INTERFACE,
T_TRAIT,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$this->allowedModifiers = Tokens::$scopeModifiers;
$this->allowedModifiers[T_FINAL] = T_FINAL;
$tokens = $this->targetedTokens;
if (defined('T_ANON_CLASS')) {
$tokens[] = T_ANON_CLASS;
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
/*
* We distinguish between the class, function and namespace names vs the define statements.
*/
if ($tokens[$stackPtr]['type'] === 'T_STRING') {
$this->processString($phpcsFile, $stackPtr, $tokens);
} else {
$this->processNonString($phpcsFile, $stackPtr, $tokens);
}
}
/**
* 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.
* @param array $tokens The stack of tokens that make up
* the file.
*
* @return void
*/
public function processNonString(File $phpcsFile, $stackPtr, $tokens)
{
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false) {
return;
}
/*
* Deal with anonymous classes - `class` before a reserved keyword is sometimes
* misidentified as `T_ANON_CLASS`.
* In PHPCS < 2.3.4 these were tokenized as T_CLASS no matter what.
*/
if ($tokens[$stackPtr]['type'] === 'T_ANON_CLASS' || $tokens[$stackPtr]['type'] === 'T_CLASS') {
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['type'] === 'T_NEW') {
return;
}
}
/*
* PHP 5.6 allows for use const and use function, but only if followed by the function/constant name.
* - `use function HelloWorld` => move to the next token (HelloWorld) to verify.
* - `use const HelloWorld` => move to the next token (HelloWorld) to verify.
*/
elseif ($tokens[$stackPtr]['type'] === 'T_USE'
&& isset($this->validUseNames[strtolower($tokens[$nextNonEmpty]['content'])]) === true
) {
$maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true);
if ($maybeUseNext !== false && $this->isEndOfUseStatement($tokens[$maybeUseNext]) === false) {
$nextNonEmpty = $maybeUseNext;
}
}
/*
* Deal with visibility modifiers.
* - `use HelloWorld { sayHello as protected; }` => valid, bow out.
* - `use HelloWorld { sayHello as private myPrivateHello; }` => move to the next token to verify.
*/
elseif ($tokens[$stackPtr]['type'] === 'T_AS'
&& isset($this->allowedModifiers[$tokens[$nextNonEmpty]['code']]) === true
&& $phpcsFile->hasCondition($stackPtr, T_USE) === true
) {
$maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true);
if ($maybeUseNext === false || $this->isEndOfUseStatement($tokens[$maybeUseNext]) === true) {
return;
}
$nextNonEmpty = $maybeUseNext;
}
/*
* Deal with functions declared to return by reference.
*/
elseif ($tokens[$stackPtr]['type'] === 'T_FUNCTION'
&& $tokens[$nextNonEmpty]['type'] === 'T_BITWISE_AND'
) {
$maybeUseNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true, null, true);
if ($maybeUseNext === false) {
// Live coding.
return;
}
$nextNonEmpty = $maybeUseNext;
}
/*
* Deal with nested namespaces.
*/
elseif ($tokens[$stackPtr]['type'] === 'T_NAMESPACE') {
if ($tokens[$stackPtr + 1]['code'] === T_NS_SEPARATOR) {
// Not a namespace declaration, but use of, i.e. namespace\someFunction();
return;
}
$endToken = $phpcsFile->findNext(array(T_SEMICOLON, T_OPEN_CURLY_BRACKET), ($stackPtr + 1), null, false, null, true);
$namespaceName = trim($phpcsFile->getTokensAsString(($stackPtr + 1), ($endToken - $stackPtr - 1)));
if (empty($namespaceName) === true) {
return;
}
$namespaceParts = explode('\\', $namespaceName);
foreach ($namespaceParts as $namespacePart) {
$partLc = strtolower($namespacePart);
if (isset($this->invalidNames[$partLc]) === false) {
continue;
}
// Find the token position of the part which matched.
for ($i = ($stackPtr + 1); $i < $endToken; $i++) {
if ($tokens[$i]['content'] === $namespacePart) {
$nextNonEmpty = $i;
break;
}
}
}
unset($i, $namespacePart, $partLc);
}
$nextContentLc = strtolower($tokens[$nextNonEmpty]['content']);
if (isset($this->invalidNames[$nextContentLc]) === false) {
return;
}
/*
* Deal with PHP 7 relaxing the rules.
* "As of PHP 7.0.0 these keywords are allowed as property, constant, and method names
* of classes, interfaces and traits, except that class may not be used as constant name."
*/
if ((($tokens[$stackPtr]['type'] === 'T_FUNCTION'
&& $this->inClassScope($phpcsFile, $stackPtr, false) === true)
|| ($tokens[$stackPtr]['type'] === 'T_CONST'
&& $this->isClassConstant($phpcsFile, $stackPtr) === true
&& $nextContentLc !== 'class'))
&& $this->supportsBelow('5.6') === false
) {
return;
}
if ($this->supportsAbove($this->invalidNames[$nextContentLc])) {
$data = array(
$tokens[$nextNonEmpty]['content'],
$this->invalidNames[$nextContentLc],
);
$this->addError($phpcsFile, $stackPtr, $tokens[$nextNonEmpty]['content'], $data);
}
}
/**
* 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.
* @param array $tokens The stack of tokens that make up
* the file.
*
* @return void
*/
public function processString(File $phpcsFile, $stackPtr, $tokens)
{
$tokenContentLc = strtolower($tokens[$stackPtr]['content']);
/*
* Special case for PHP versions where the target is not yet identified as
* its own token, but presents as T_STRING.
* - trait keyword in PHP < 5.4
*/
if (version_compare(PHP_VERSION_ID, '50400', '<') && $tokenContentLc === 'trait') {
$this->processNonString($phpcsFile, $stackPtr, $tokens);
return;
}
// Look for any define/defined tokens (both T_STRING ones, blame Tokenizer).
if ($tokenContentLc !== 'define' && $tokenContentLc !== 'defined') {
return;
}
// Retrieve the define(d) constant name.
$firstParam = $this->getFunctionCallParameter($phpcsFile, $stackPtr, 1);
if ($firstParam === false) {
return;
}
$defineName = $this->stripQuotes($firstParam['raw']);
$defineNameLc = strtolower($defineName);
if (isset($this->invalidNames[$defineNameLc]) && $this->supportsAbove($this->invalidNames[$defineNameLc])) {
$data = array(
$defineName,
$this->invalidNames[$defineNameLc],
);
$this->addError($phpcsFile, $stackPtr, $defineNameLc, $data);
}
}
/**
* Add the error message.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param string $content The token content found.
* @param array $data The data to pass into the error message.
*
* @return void
*/
protected function addError(File $phpcsFile, $stackPtr, $content, $data)
{
$error = "Function name, class name, namespace name or constant name can not be reserved keyword '%s' (since version %s)";
$errorCode = $this->stringToErrorCode($content) . 'Found';
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
/**
* Check if the current token code is for a token which can be considered
* the end of a (partial) use statement.
*
* @param int $token The current token information.
*
* @return bool
*/
protected function isEndOfUseStatement($token)
{
return in_array($token['code'], array(T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_COMMA), true);
}
}
@@ -0,0 +1,366 @@
<?php
/**
* \PHPCompatibility\Sniffs\Keywords\NewKeywordsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Keywords;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Keywords\NewKeywordsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
class NewKeywordsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new keywords, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the last version which did not contain the keyword.
*
* Description will be used as part of the error message.
* Condition is the name of a callback method within this class or the parent class
* which checks whether the token complies with a certain condition.
* The callback function will be passed the $phpcsFile and the $stackPtr.
* The callback function should return `true` if the condition is met and the
* error should *not* be thrown.
*
* @var array(string => array(string => int|string|null))
*/
protected $newKeywords = array(
'T_HALT_COMPILER' => array(
'5.0' => false,
'5.1' => true,
'description' => '"__halt_compiler" keyword',
),
'T_CONST' => array(
'5.2' => false,
'5.3' => true,
'description' => '"const" keyword',
'condition' => 'isClassConstant', // Keyword is only new when not in class context.
),
'T_CALLABLE' => array(
'5.3' => false,
'5.4' => true,
'description' => '"callable" keyword',
'content' => 'callable',
),
'T_DIR' => array(
'5.2' => false,
'5.3' => true,
'description' => '__DIR__ magic constant',
'content' => '__DIR__',
),
'T_GOTO' => array(
'5.2' => false,
'5.3' => true,
'description' => '"goto" keyword',
'content' => 'goto',
),
'T_INSTEADOF' => array(
'5.3' => false,
'5.4' => true,
'description' => '"insteadof" keyword (for traits)',
'content' => 'insteadof',
),
'T_NAMESPACE' => array(
'5.2' => false,
'5.3' => true,
'description' => '"namespace" keyword',
'content' => 'namespace',
),
'T_NS_C' => array(
'5.2' => false,
'5.3' => true,
'description' => '__NAMESPACE__ magic constant',
'content' => '__NAMESPACE__',
),
'T_USE' => array(
'5.2' => false,
'5.3' => true,
'description' => '"use" keyword (for traits/namespaces/anonymous functions)',
),
'T_START_NOWDOC' => array(
'5.2' => false,
'5.3' => true,
'description' => 'nowdoc functionality',
),
'T_END_NOWDOC' => array(
'5.2' => false,
'5.3' => true,
'description' => 'nowdoc functionality',
),
'T_START_HEREDOC' => array(
'5.2' => false,
'5.3' => true,
'description' => '(Double) quoted Heredoc identifier',
'condition' => 'isNotQuoted', // Heredoc is only new with quoted identifier.
),
'T_TRAIT' => array(
'5.3' => false,
'5.4' => true,
'description' => '"trait" keyword',
'content' => 'trait',
),
'T_TRAIT_C' => array(
'5.3' => false,
'5.4' => true,
'description' => '__TRAIT__ magic constant',
'content' => '__TRAIT__',
),
// The specifics for distinguishing between 'yield' and 'yield from' are dealt
// with in the translation logic.
// This token has to be placed above the `T_YIELD` token in this array to allow for this.
'T_YIELD_FROM' => array(
'5.6' => false,
'7.0' => true,
'description' => '"yield from" keyword (for generators)',
'content' => 'yield',
),
'T_YIELD' => array(
'5.4' => false,
'5.5' => true,
'description' => '"yield" keyword (for generators)',
'content' => 'yield',
),
'T_FINALLY' => array(
'5.4' => false,
'5.5' => true,
'description' => '"finally" keyword (in exception handling)',
'content' => 'finally',
),
);
/**
* Translation table for T_STRING tokens.
*
* Will be set up from the register() method.
*
* @var array(string => string)
*/
protected $translateContentToToken = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array();
$translate = array();
foreach ($this->newKeywords as $token => $versions) {
if (defined($token)) {
$tokens[] = constant($token);
}
if (isset($versions['content'])) {
$translate[strtolower($versions['content'])] = $token;
}
}
/*
* Deal with tokens not recognized by the PHP version the sniffer is run
* under and (not correctly) compensated for by PHPCS.
*/
if (empty($translate) === false) {
$this->translateContentToToken = $translate;
$tokens[] = T_STRING;
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenType = $tokens[$stackPtr]['type'];
// Allow for dealing with multi-token keywords, like "yield from".
$end = $stackPtr;
// Translate T_STRING token if necessary.
if ($tokens[$stackPtr]['type'] === 'T_STRING') {
$content = strtolower($tokens[$stackPtr]['content']);
if (isset($this->translateContentToToken[$content]) === false) {
// Not one of the tokens we're looking for.
return;
}
$tokenType = $this->translateContentToToken[$content];
}
/*
* Special case: distinguish between `yield` and `yield from`.
*
* PHPCS currently (at least up to v 3.0.1) does not backfill for the
* `yield` nor the `yield from` keywords.
* See: https://github.com/squizlabs/PHP_CodeSniffer/issues/1524
*
* In PHP < 5.5, both `yield` as well as `from` are tokenized as T_STRING.
* In PHP 5.5 - 5.6, `yield` is tokenized as T_YIELD and `from` as T_STRING,
* but the `T_YIELD_FROM` token *is* defined in PHP.
* In PHP 7.0+ both are tokenized as their respective token, however,
* a multi-line "yield from" is tokenized as two tokens.
*/
if ($tokenType === 'T_YIELD') {
$nextToken = $phpcsFile->findNext(T_WHITESPACE, ($end + 1), null, true);
if ($tokens[$nextToken]['code'] === T_STRING
&& $tokens[$nextToken]['content'] === 'from'
) {
$tokenType = 'T_YIELD_FROM';
$end = $nextToken;
}
unset($nextToken);
}
if ($tokenType === 'T_YIELD_FROM' && $tokens[($stackPtr - 1)]['type'] === 'T_YIELD_FROM') {
// Multi-line "yield from", no need to report it twice.
return;
}
if (isset($this->newKeywords[$tokenType]) === false) {
return;
}
$nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prevToken !== false
&& ($tokens[$prevToken]['code'] === T_DOUBLE_COLON
|| $tokens[$prevToken]['code'] === T_OBJECT_OPERATOR)
) {
// Class property of the same name as one of the keywords. Ignore.
return;
}
// Skip attempts to use keywords as functions or class names - the former
// will be reported by ForbiddenNamesAsInvokedFunctionsSniff, whilst the
// latter will be (partially) reported by the ForbiddenNames sniff.
// Either type will result in false-positives when targetting lower versions
// of PHP where the name was not reserved, unless we explicitly check for
// them.
if (($nextToken === false
|| $tokens[$nextToken]['type'] !== 'T_OPEN_PARENTHESIS')
&& ($prevToken === false
|| $tokens[$prevToken]['type'] !== 'T_CLASS'
|| $tokens[$prevToken]['type'] !== 'T_INTERFACE')
) {
// Skip based on token scope condition.
if (isset($this->newKeywords[$tokenType]['condition'])
&& call_user_func(array($this, $this->newKeywords[$tokenType]['condition']), $phpcsFile, $stackPtr) === true
) {
return;
}
$itemInfo = array(
'name' => $tokenType,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newKeywords[$itemInfo['name']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array(
'description',
'condition',
'content',
);
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['description'] = $itemArray['description'];
return $errorInfo;
}
/**
* Allow for concrete child classes to filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
$data[0] = $errorInfo['description'];
return $data;
}
/**
* Callback for the quoted heredoc identifier condition.
*
* A double quoted identifier will have the opening quote on position 3
* in the string: `<<<"ID"`.
*
* @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 bool
*/
public function isNotQuoted(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
return ($tokens[$stackPtr]['content'][3] !== '"');
}
}
@@ -0,0 +1,84 @@
<?php
/**
* \PHPCompatibility\Sniffs\LanguageConstructs\NewEmptyNonVariableSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\LanguageConstructs;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\LanguageConstructs\NewEmptyNonVariableSniff.
*
* Verify that nothing but variables are passed to empty().
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewEmptyNonVariableSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_EMPTY);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$open = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($open === false
|| $tokens[$open]['code'] !== T_OPEN_PARENTHESIS
|| isset($tokens[$open]['parenthesis_closer']) === false
) {
return;
}
$close = $tokens[$open]['parenthesis_closer'];
$nestingLevel = 0;
if ($close !== ($open + 1) && isset($tokens[$open + 1]['nested_parenthesis'])) {
$nestingLevel = count($tokens[$open + 1]['nested_parenthesis']);
}
if ($this->isVariable($phpcsFile, ($open + 1), $close, $nestingLevel) === true) {
return;
}
$phpcsFile->addError(
'Only variables can be passed to empty() prior to PHP 5.5.',
$stackPtr,
'Found'
);
}
}
@@ -0,0 +1,140 @@
<?php
/**
* \PHPCompatibility\Sniffs\LanguageConstructs\NewLanguageConstructsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\LanguageConstructs;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\LanguageConstructs\NewLanguageConstructsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
class NewLanguageConstructsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new language constructs, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the keyword appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newConstructs = array(
'T_NS_SEPARATOR' => array(
'5.2' => false,
'5.3' => true,
'description' => 'the \ operator (for namespaces)',
),
'T_ELLIPSIS' => array(
'5.5' => false,
'5.6' => true,
'description' => 'variadic functions using ...',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array();
foreach ($this->newConstructs as $token => $versions) {
$tokens[] = constant($token);
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenType = $tokens[$stackPtr]['type'];
$itemInfo = array(
'name' => $tokenType,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newConstructs[$itemInfo['name']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('description');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['description'] = $itemArray['description'];
return $errorInfo;
}
/**
* Allow for concrete child classes to filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
$data[0] = $errorInfo['description'];
return $data;
}
}
@@ -0,0 +1,183 @@
<?php
/**
* \PHPCompatibility\Sniffs\Lists\AssignmentOrderSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Lists;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* List assignment order.
*
* The list() construct no longer assigns variables in reverse order.
* This affects all list constructs where non-unique variables are used.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class AssignmentOrderSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_LIST,
T_OPEN_SHORT_ARRAY,
);
}
/**
* 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|int Void if not a valid list. If a list construct has been
* examined, the stack pointer to the list closer to skip
* passed any nested lists which don't need to be examined again.
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_OPEN_SHORT_ARRAY
&& $this->isShortList($phpcsFile, $stackPtr) === false
) {
// Short array, not short list.
return;
}
if ($tokens[$stackPtr]['code'] === T_LIST) {
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false
|| $tokens[$nextNonEmpty]['code'] !== T_OPEN_PARENTHESIS
|| isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false
) {
// Parse error or live coding.
return;
}
$opener = $nextNonEmpty;
$closer = $tokens[$nextNonEmpty]['parenthesis_closer'];
} else {
// Short list syntax.
$opener = $stackPtr;
if (isset($tokens[$stackPtr]['bracket_closer'])) {
$closer = $tokens[$stackPtr]['bracket_closer'];
}
}
if (isset($opener, $closer) === false) {
return;
}
/*
* OK, so we have the opener & closer, now we need to check all the variables in the
* list() to see if there are duplicates as that's the problem.
*/
$hasVars = $phpcsFile->findNext(array(T_VARIABLE, T_DOLLAR), ($opener + 1), $closer);
if ($hasVars === false) {
// Empty list, not our concern.
return ($closer + 1);
}
// Set the variable delimiters based on the list type being examined.
$stopPoints = array(T_COMMA);
if ($tokens[$stackPtr]['code'] === T_OPEN_SHORT_ARRAY) {
$stopPoints[] = T_CLOSE_SHORT_ARRAY;
} else {
$stopPoints[] = T_CLOSE_PARENTHESIS;
}
$listVars = array();
$lastStopPoint = $opener;
/*
* Create a list of all variables used within the `list()` construct.
* We're not concerned with whether these are nested or not, as any duplicate
* variable name used will be problematic, independent of nesting.
*/
do {
$nextStopPoint = $phpcsFile->findNext($stopPoints, ($lastStopPoint + 1), $closer);
if ($nextStopPoint === false) {
$nextStopPoint = $closer;
}
// Also detect this in PHP 7.1 keyed lists.
$hasDoubleArrow = $phpcsFile->findNext(T_DOUBLE_ARROW, ($lastStopPoint + 1), $nextStopPoint);
if ($hasDoubleArrow !== false) {
$lastStopPoint = $hasDoubleArrow;
}
// Find the start of the variable, allowing for variable variables.
$nextStartPoint = $phpcsFile->findNext(array(T_VARIABLE, T_DOLLAR), ($lastStopPoint + 1), $nextStopPoint);
if ($nextStartPoint === false) {
// Skip past empty bits in the list, i.e. `list( $a, , ,)`.
$lastStopPoint = $nextStopPoint;
continue;
}
/*
* Gather the content of all non-empty tokens to determine the "variable name".
* Variable name in this context includes array or object property syntaxes, such
* as `$a['name']` and `$b->property`.
*/
$varContent = '';
for ($i = $nextStartPoint; $i < $nextStopPoint; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
continue;
}
$varContent .= $tokens[$i]['content'];
}
if ($varContent !== '') {
$listVars[] = $varContent;
}
$lastStopPoint = $nextStopPoint;
} while ($lastStopPoint < $closer);
if (empty($listVars)) {
// Shouldn't be possible, but just in case.
return ($closer + 1);
}
// Verify that all variables used in the list() construct are unique.
if (count($listVars) !== count(array_unique($listVars))) {
$phpcsFile->addError(
'list() will assign variable from left-to-right since PHP 7.0. Ensure all variables in list() are unique to prevent unexpected results.',
$stackPtr,
'Affected'
);
}
return ($closer + 1);
}
}
@@ -0,0 +1,111 @@
<?php
/**
* \PHPCompatibility\Sniffs\Lists\ForbiddenEmptyListAssignmentSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\Lists;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Lists\ForbiddenEmptyListAssignmentSniff.
*
* Empty list() assignments have been removed in PHP 7.0
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class ForbiddenEmptyListAssignmentSniff extends Sniff
{
/**
* List of tokens to disregard when determining whether the list() is empty.
*
* @var array
*/
protected $ignoreTokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Set up a list of tokens to disregard when determining whether the list() is empty.
// Only needs to be set up once.
$this->ignoreTokens = Tokens::$emptyTokens;
$this->ignoreTokens[T_COMMA] = T_COMMA;
$this->ignoreTokens[T_OPEN_PARENTHESIS] = T_OPEN_PARENTHESIS;
$this->ignoreTokens[T_CLOSE_PARENTHESIS] = T_CLOSE_PARENTHESIS;
return array(
T_LIST,
T_OPEN_SHORT_ARRAY,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_OPEN_SHORT_ARRAY) {
if ($this->isShortList($phpcsFile, $stackPtr) === false) {
return;
}
$open = $stackPtr;
$close = $tokens[$stackPtr]['bracket_closer'];
} else {
// T_LIST.
$open = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr, null, false, null, true);
if ($open === false || isset($tokens[$open]['parenthesis_closer']) === false) {
return;
}
$close = $tokens[$open]['parenthesis_closer'];
}
$error = true;
if (($close - $open) > 1) {
for ($cnt = $open + 1; $cnt < $close; $cnt++) {
if (isset($this->ignoreTokens[$tokens[$cnt]['code']]) === false) {
$error = false;
break;
}
}
}
if ($error === true) {
$phpcsFile->addError(
'Empty list() assignments are not allowed since PHP 7.0',
$stackPtr,
'Found'
);
}
}
}
@@ -0,0 +1,211 @@
<?php
/**
* \PHPCompatibility\Sniffs\Lists\NewKeyedListSniff.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Lists;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Lists\NewKeyedListSniff.
*
* "You can now specify keys in list(), or its new shorthand [] syntax. "
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewKeyedListSniff extends Sniff
{
/**
* Tokens which represent the start of a list construct.
*
* @var array
*/
protected $sniffTargets = array(
T_LIST => T_LIST,
T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY,
);
/**
* The token(s) within the list construct which is being targeted.
*
* @var array
*/
protected $targetsInList = array(
T_DOUBLE_ARROW => T_DOUBLE_ARROW,
);
/**
* All tokens needed to walk through the list construct and
* determine whether the target token is contained within.
*
* Set by the setUpAllTargets() method which is called from within register().
*
* @var array
*/
protected $allTargets;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$this->setUpAllTargets();
return $this->sniffTargets;
}
/**
* Prepare the $allTargets array only once.
*
* @return array
*/
public function setUpAllTargets()
{
$this->allTargets = $this->sniffTargets + $this->targetsInList;
}
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('7.0') === false);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->bowOutEarly() === true) {
return;
}
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_OPEN_SHORT_ARRAY
&& $this->isShortList($phpcsFile, $stackPtr) === false
) {
// Short array, not short list.
return;
}
if ($tokens[$stackPtr]['code'] === T_LIST) {
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false
|| $tokens[$nextNonEmpty]['code'] !== T_OPEN_PARENTHESIS
|| isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false
) {
// Parse error or live coding.
return;
}
$opener = $nextNonEmpty;
$closer = $tokens[$nextNonEmpty]['parenthesis_closer'];
} else {
// Short list syntax.
$opener = $stackPtr;
if (isset($tokens[$stackPtr]['bracket_closer'])) {
$closer = $tokens[$stackPtr]['bracket_closer'];
}
}
if (isset($opener, $closer) === false) {
return;
}
$this->examineList($phpcsFile, $opener, $closer);
}
/**
* Examine the contents of a list construct to determine whether an error needs to be thrown.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $opener The position of the list open token.
* @param int $closer The position of the list close token.
*
* @return void
*/
protected function examineList(File $phpcsFile, $opener, $closer)
{
$start = $opener;
while (($start = $this->hasTargetInList($phpcsFile, $start, $closer)) !== false) {
$phpcsFile->addError(
'Specifying keys in list constructs is not supported in PHP 7.0 or earlier.',
$start,
'Found'
);
}
}
/**
* Check whether a certain target token exists within a list construct.
*
* Skips past nested list constructs, so these can be examined based on their own token.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $start The position of the list open token or a token
* within the list to start (resume) the examination from.
* @param int $closer The position of the list close token.
*
* @return int|bool Stack pointer to the target token if encountered. False otherwise.
*/
protected function hasTargetInList(File $phpcsFile, $start, $closer)
{
$tokens = $phpcsFile->getTokens();
for ($i = ($start + 1); $i < $closer; $i++) {
if (isset($this->allTargets[$tokens[$i]['code']]) === false) {
continue;
}
if (isset($this->targetsInList[$tokens[$i]['code']]) === true) {
return $i;
}
// Skip past nested list constructs.
if ($tokens[$i]['code'] === T_LIST) {
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
if ($nextNonEmpty !== false
&& $tokens[$nextNonEmpty]['code'] === T_OPEN_PARENTHESIS
&& isset($tokens[$nextNonEmpty]['parenthesis_closer']) === true
) {
$i = $tokens[$nextNonEmpty]['parenthesis_closer'];
}
} elseif ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY
&& isset($tokens[$i]['bracket_closer'])
) {
$i = $tokens[$i]['bracket_closer'];
}
}
return false;
}
}
@@ -0,0 +1,67 @@
<?php
/**
* \PHPCompatibility\Sniffs\Lists\NewListReferenceAssignmentSniff.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Lists;
use PHPCompatibility\Sniffs\Lists\NewKeyedListSniff;
use PHP_CodeSniffer_File as File;
/**
* Detect reference assignments in array destructuring using (short) list.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewListReferenceAssignmentSniff extends NewKeyedListSniff
{
/**
* The token(s) within the list construct which is being targeted.
*
* @var array
*/
protected $targetsInList = array(
T_BITWISE_AND => T_BITWISE_AND,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('7.2') === false);
}
/**
* Examine the contents of a list construct to determine whether an error needs to be thrown.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $opener The position of the list open token.
* @param int $closer The position of the list close token.
*
* @return void
*/
protected function examineList(File $phpcsFile, $opener, $closer)
{
$start = $opener;
while (($start = $this->hasTargetInList($phpcsFile, $start, $closer)) !== false) {
$phpcsFile->addError(
'Reference assignments within list constructs are not supported in PHP 7.2 or earlier.',
$start,
'Found'
);
}
}
}
@@ -0,0 +1,79 @@
<?php
/**
* \PHPCompatibility\Sniffs\Lists\NewShortListSniff.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Lists;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Lists\NewShortListSniff.
*
* "The shorthand array syntax ([]) may now be used to destructure arrays for
* assignments (including within foreach), as an alternative to the existing
* list() syntax, which is still supported."
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewShortListSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_OPEN_SHORT_ARRAY);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('7.0') === false) {
return;
}
if ($this->isShortList($phpcsFile, $stackPtr) === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$closer = $tokens[$stackPtr]['bracket_closer'];
$hasVariable = $phpcsFile->findNext(T_VARIABLE, ($stackPtr + 1), $closer);
if ($hasVariable === false) {
// List syntax is only valid if there are variables in it.
return;
}
$phpcsFile->addError(
'The shorthand list syntax "[]" to destructure arrays is not available in PHP 7.0 or earlier.',
$stackPtr,
'Found'
);
return ($closer + 1);
}
}
@@ -0,0 +1,97 @@
<?php
/**
* PHPCompatibility, an external standard for PHP_CodeSniffer.
*
* @package PHPCompatibility
* @copyright 2012-2018 PHPCompatibility Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCompatibility/PHPCompatibility
*/
namespace PHPCompatibility\Sniffs\MethodUse;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* Detect direct calls to the __clone() magic method which is allowed since PHP 7.0.
*
* "Doing calls like $obj->__clone() is now allowed. This was the only magic method
* that had a compile-time check preventing some calls to it, which doesn't make sense.
* If we allow all other magic methods to be called, there's no reason to forbid this one."
*
* PHP version 7.0
*
* @link https://wiki.php.net/rfc/abstract_syntax_tree#directly_calling_clone_is_allowed
*
* @since 9.1.0
*/
class NewDirectCallsToCloneSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 9.1.0
*
* @return array
*/
public function register()
{
return array(
T_DOUBLE_COLON,
T_OBJECT_OPERATOR,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 9.1.0
*
* @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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.6') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== T_STRING) {
/*
* Not a method call.
*
* Note: This disregards method calls with the method name in a variable, like:
* $method = '__clone';
* $obj->$method();
* However, that would be very hard to examine reliably anyway.
*/
return;
}
if (strtolower($tokens[$nextNonEmpty]['content']) !== '__clone') {
// Not a call to the __clone() method.
return;
}
$nextNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true);
if ($nextNextNonEmpty === false || $tokens[$nextNextNonEmpty]['code'] !== T_OPEN_PARENTHESIS) {
// Not a method call.
return;
}
$phpcsFile->addError(
'Direct calls to the __clone() magic method are not allowed in PHP 5.6 or earlier.',
$nextNonEmpty,
'Found'
);
}
}
@@ -0,0 +1,166 @@
<?php
/**
* \PHPCompatibility\Sniffs\Miscellaneous\RemovedAlternativePHPTags.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Miscellaneous;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Miscellaneous\RemovedAlternativePHPTags.
*
* Check for usage of alternative PHP tags - removed in PHP 7.0.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*
* Based on `Generic_Sniffs_PHP_DisallowAlternativePHPTags` by Juliette Reinders Folmer
* which was merged into PHPCS 2.7.0.
*/
class RemovedAlternativePHPTagsSniff extends Sniff
{
/**
* Whether ASP tags are enabled or not.
*
* @var bool
*/
private $aspTags = false;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
if (version_compare(PHP_VERSION_ID, '70000', '<') === true) {
// phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.asp_tagsRemoved
$this->aspTags = (bool) ini_get('asp_tags');
}
return array(
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
T_INLINE_HTML,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$openTag = $tokens[$stackPtr];
$content = trim($openTag['content']);
if ($content === '' || $content === '<?php') {
return;
}
if ($openTag['code'] === T_OPEN_TAG || $openTag['code'] === T_OPEN_TAG_WITH_ECHO) {
if ($content === '<%' || $content === '<%=') {
$data = array(
'ASP',
$content,
);
$errorCode = 'ASPOpenTagFound';
} elseif (strpos($content, '<script ') !== false) {
$data = array(
'Script',
$content,
);
$errorCode = 'ScriptOpenTagFound';
} else {
return;
}
}
// Account for incorrect script open tags.
// The "(?:<s)?" in the regex is to work-around a bug in the tokenizer in PHP 5.2.
elseif ($openTag['code'] === T_INLINE_HTML
&& preg_match('`((?:<s)?cript (?:[^>]+)?language=[\'"]?php[\'"]?(?:[^>]+)?>)`i', $content, $match) === 1
) {
$found = $match[1];
$data = array(
'Script',
$found,
);
$errorCode = 'ScriptOpenTagFound';
}
if (isset($errorCode, $data)) {
$phpcsFile->addError(
'%s style opening tags have been removed in PHP 7.0. Found "%s"',
$stackPtr,
$errorCode,
$data
);
return;
}
// If we're still here, we can't be sure if what we find was really intended as ASP open tags.
if ($openTag['code'] === T_INLINE_HTML && $this->aspTags === false) {
if (strpos($content, '<%') !== false) {
$error = 'Possible use of ASP style opening tags detected. ASP style opening tags have been removed in PHP 7.0. Found: %s';
$snippet = $this->getSnippet($content, '<%');
$data = array('<%' . $snippet);
$phpcsFile->addWarning($error, $stackPtr, 'MaybeASPOpenTagFound', $data);
}
}
}
/**
* Get a snippet from a HTML token.
*
* @param string $content The content of the HTML token.
* @param string $startAt Partial string to use as a starting point for the snippet.
* @param int $length The target length of the snippet to get. Defaults to 25.
*
* @return string
*/
protected function getSnippet($content, $startAt = '', $length = 25)
{
$startPos = 0;
if ($startAt !== '') {
$startPos = strpos($content, $startAt);
if ($startPos !== false) {
$startPos += strlen($startAt);
}
}
$snippet = substr($content, $startPos, $length);
if ((strlen($content) - $startPos) > $length) {
$snippet .= '...';
}
return $snippet;
}
}
@@ -0,0 +1,220 @@
<?php
/**
* \PHPCompatibility\Sniffs\Miscellaneous\ValidIntegersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Miscellaneous;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Miscellaneous\ValidIntegersSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ValidIntegersSniff extends Sniff
{
/**
* Whether PHPCS is run on a PHP < 5.4.
*
* @var bool
*/
protected $isLowPHPVersion = false;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$this->isLowPHPVersion = version_compare(PHP_VERSION_ID, '50400', '<');
return array(
T_LNUMBER, // Binary, octal integers.
T_CONSTANT_ENCAPSED_STRING, // Hex numeric string.
);
}
/**
* 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(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->addWarning($error, $stackPtr, 'InvalidBinaryIntegerFound', $data);
}
return;
}
$isError = $this->supportsAbove('7.0');
$data = array( $token['content'] );
if ($this->isInvalidOctalInteger($tokens, $stackPtr) === true) {
$this->addMessage(
$phpcsFile,
'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',
$stackPtr,
$isError,
'InvalidOctalIntegerFound',
$data
);
return;
}
if ($this->isHexidecimalNumericString($tokens, $stackPtr) === true) {
$this->addMessage(
$phpcsFile,
'The behaviour of hexadecimal numeric strings was inconsistent prior to PHP 7 and support has been removed in PHP 7. Found: %s',
$stackPtr,
$isError,
'HexNumericStringFound',
$data
);
return;
}
}
/**
* Could the current token an potentially be a binary integer ?
*
* @param array $tokens 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 $tokens 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 $tokens Token stack.
* @param int $stackPtr The position of the current token in
* the stack.
*
* @return string
*/
private function getBinaryInteger(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 $tokens 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 $tokens 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]+$`iD', $this->stripQuotes($token['content'])) === 1) {
return true;
}
return false;
}
}
@@ -0,0 +1,104 @@
<?php
/**
* \PHPCompatibility\Sniffs\Operators\ForbiddenNegativeBitshift.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\Operators;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Operators\ForbiddenNegativeBitshift.
*
* Bitwise shifts by negative number will throw an ArithmeticError in PHP 7.0.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class ForbiddenNegativeBitshiftSniff extends Sniff
{
/**
* Potential end tokens for which the end pointer has to be set back by one.
*
* {@internal The PHPCS `findEndOfStatement()` method is not completely consistent
* in how it returns the statement end. This is just a simple way to bypass
* the inconsistency for our purposes.}}
*
* @var array
*/
private $inclusiveStopPoints = array(
T_COLON => true,
T_COMMA => true,
T_DOUBLE_ARROW => true,
T_SEMICOLON => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_SL,
T_SL_EQUAL,
T_SR,
T_SR_EQUAL,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
// Determine the start and end of the part of the statement we need to examine.
$start = ($stackPtr + 1);
$next = $phpcsFile->findNext(Tokens::$emptyTokens, $start, null, true);
if ($next !== false && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) {
$start = ($next + 1);
}
$end = PHPCSHelper::findEndOfStatement($phpcsFile, $start);
if (isset($this->inclusiveStopPoints[$tokens[$end]['code']]) === true) {
--$end;
}
if ($this->isNegativeNumber($phpcsFile, $start, $end, true) !== true) {
// Not a negative number or undetermined.
return;
}
$phpcsFile->addError(
'Bitwise shifts by negative number will throw an ArithmeticError in PHP 7.0. Found: %s',
$stackPtr,
'Found',
array($phpcsFile->getTokensAsString($start, ($end - $start + 1)))
);
}
}
@@ -0,0 +1,294 @@
<?php
/**
* \PHPCompatibility\Sniffs\Operators\NewOperatorsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Operators;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Operators\NewOperatorsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2013 Cu.be Solutions bvba
*/
class NewOperatorsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new operators, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the keyword appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newOperators = array(
'T_POW' => array(
'5.5' => false,
'5.6' => true,
'description' => 'power operator (**)',
), // Identified in PHP < 5.6 icw PHPCS < 2.4.0 as T_MULTIPLY + T_MULTIPLY.
'T_POW_EQUAL' => array(
'5.5' => false,
'5.6' => true,
'description' => 'power assignment operator (**=)',
), // Identified in PHP < 5.6 icw PHPCS < 2.6.0 as T_MULTIPLY + T_MUL_EQUAL.
'T_SPACESHIP' => array(
'5.6' => false,
'7.0' => true,
'description' => 'spaceship operator (<=>)',
), // Identified in PHP < 7.0 icw PHPCS < 2.5.1 as T_IS_SMALLER_OR_EQUAL + T_GREATER_THAN.
'T_COALESCE' => array(
'5.6' => false,
'7.0' => true,
'description' => 'null coalescing operator (??)',
), // Identified in PHP < 7.0 icw PHPCS < 2.6.2 as T_INLINE_THEN + T_INLINE_THEN.
/*
* Was slated for 7.2, but still not implemented. PHPCS however does already tokenize it.
* @link https://wiki.php.net/rfc/null_coalesce_equal_operator
*/
'T_COALESCE_EQUAL' => array(
'7.3' => false,
'7.4' => true,
'description' => 'null coalesce equal operator (??=)',
), // Identified in PHP < 7.0 icw PHPCS < 2.6.2 as T_INLINE_THEN + T_INLINE_THEN + T_EQUAL and between PHPCS 2.6.2 and PHPCS 2.8.1 as T_COALESCE + T_EQUAL.
);
/**
* A list of new operators which are not recognized in older PHPCS versions.
*
* The array lists an alternative token to listen for.
*
* @var array(string => int)
*/
protected $newOperatorsPHPCSCompat = array(
'T_POW' => T_MULTIPLY,
'T_POW_EQUAL' => T_MUL_EQUAL,
'T_SPACESHIP' => T_GREATER_THAN,
'T_COALESCE' => T_INLINE_THEN,
'T_COALESCE_EQUAL' => T_EQUAL,
);
/**
* Token translation table for older PHPCS versions.
*
* The 'before' index lists the token which would have to be directly before the
* token found for it to be one of the new operators.
* The 'real_token' index indicates which operator was found in that case.
*
* If the token combination has multi-layer complexity, such as is the case
* with T_COALESCE(_EQUAL), a 'callback' index is added instead pointing to a
* separate function which can determine whether this is the targetted token across
* PHP and PHPCS versions.
*
* {@internal 'before' was chosen rather than 'after' as that allowed for a 1-on-1
* translation list with the current tokens.}}
*
* @var array(string => array(string => string))
*/
protected $PHPCSCompatTranslate = array(
'T_MULTIPLY' => array(
'before' => 'T_MULTIPLY',
'real_token' => 'T_POW',
),
'T_MUL_EQUAL' => array(
'before' => 'T_MULTIPLY',
'real_token' => 'T_POW_EQUAL',
),
'T_GREATER_THAN' => array(
'before' => 'T_IS_SMALLER_OR_EQUAL',
'real_token' => 'T_SPACESHIP',
),
'T_INLINE_THEN' => array(
'callback' => 'isTCoalesce',
'real_token' => 'T_COALESCE',
),
'T_EQUAL' => array(
'callback' => 'isTCoalesceEqual',
'real_token' => 'T_COALESCE_EQUAL',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array();
foreach ($this->newOperators as $token => $versions) {
if (defined($token)) {
$tokens[] = constant($token);
} elseif (isset($this->newOperatorsPHPCSCompat[$token])) {
$tokens[] = $this->newOperatorsPHPCSCompat[$token];
}
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenType = $tokens[$stackPtr]['type'];
// Translate older PHPCS token combis for new operators to the actual operator.
if (isset($this->newOperators[$tokenType]) === false) {
if (isset($this->PHPCSCompatTranslate[$tokenType])
&& ((isset($this->PHPCSCompatTranslate[$tokenType]['before'], $tokens[$stackPtr - 1]) === true
&& $tokens[$stackPtr - 1]['type'] === $this->PHPCSCompatTranslate[$tokenType]['before'])
|| (isset($this->PHPCSCompatTranslate[$tokenType]['callback']) === true
&& call_user_func(array($this, $this->PHPCSCompatTranslate[$tokenType]['callback']), $tokens, $stackPtr) === true))
) {
$tokenType = $this->PHPCSCompatTranslate[$tokenType]['real_token'];
}
} elseif ($tokenType === 'T_COALESCE') {
// Make sure that T_COALESCE is not confused with T_COALESCE_EQUAL.
if (isset($tokens[($stackPtr + 1)]) !== false && $tokens[($stackPtr + 1)]['code'] === T_EQUAL) {
// Ignore as will be dealt with via the T_EQUAL token.
return;
}
}
// If the translation did not yield one of the tokens we are looking for, bow out.
if (isset($this->newOperators[$tokenType]) === false) {
return;
}
$itemInfo = array(
'name' => $tokenType,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newOperators[$itemInfo['name']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('description');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['description'] = $itemArray['description'];
return $errorInfo;
}
/**
* Allow for concrete child classes to filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
$data[0] = $errorInfo['description'];
return $data;
}
/**
* Callback function to determine whether a T_EQUAL token is really a T_COALESCE_EQUAL token.
*
* @param array $tokens The token stack.
* @param int $stackPtr The current position in the token stack.
*
* @return bool
*/
private function isTCoalesceEqual($tokens, $stackPtr)
{
if ($tokens[$stackPtr]['code'] !== T_EQUAL || isset($tokens[($stackPtr - 1)]) === false) {
// Function called for wrong token or token has no predecesor.
return false;
}
if ($tokens[($stackPtr - 1)]['type'] === 'T_COALESCE') {
return true;
}
if ($tokens[($stackPtr - 1)]['type'] === 'T_INLINE_THEN'
&& (isset($tokens[($stackPtr - 2)]) && $tokens[($stackPtr - 2)]['type'] === 'T_INLINE_THEN')
) {
return true;
}
return false;
}
/**
* Callback function to determine whether a T_INLINE_THEN token is really a T_COALESCE token.
*
* @param array $tokens The token stack.
* @param int $stackPtr The current position in the token stack.
*
* @return bool
*/
private function isTCoalesce($tokens, $stackPtr)
{
if ($tokens[$stackPtr]['code'] !== T_INLINE_THEN || isset($tokens[($stackPtr - 1)]) === false) {
// Function called for wrong token or token has no predecesor.
return false;
}
if ($tokens[($stackPtr - 1)]['code'] === T_INLINE_THEN) {
// Make sure not to confuse it with the T_COALESCE_EQUAL token.
if (isset($tokens[($stackPtr + 1)]) === false || $tokens[($stackPtr + 1)]['code'] !== T_EQUAL) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,74 @@
<?php
/**
* \PHPCompatibility\Sniffs\Operators\NewShortTernarySniff.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Ben Selby <bselby@plus.net>
* @copyright 2012 Ben Selby
*/
namespace PHPCompatibility\Sniffs\Operators;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Operators\NewShortTernarySniff.
*
* Performs checks on ternary operators, specifically that the middle expression
* is not omitted for versions that don't support this.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Ben Selby <bselby@plus.net>
* @copyright 2012 Ben Selby
*/
class NewShortTernarySniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_INLINE_THEN);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.2') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
// Get next non-whitespace token, and check it isn't the related inline else
// symbol, which is not allowed prior to PHP 5.3.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($next !== false && $tokens[$next]['code'] === T_INLINE_ELSE) {
$phpcsFile->addError(
'Middle may not be omitted from ternary operators in PHP < 5.3',
$stackPtr,
'MiddleMissing'
);
}
}
}
@@ -0,0 +1,80 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\ForbiddenGetClassNullSniff.
*
* PHP version 7.2
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\ForbiddenGetClassNullSniff.
*
* Detect: Passing `null` to get_class() is no longer allowed as of PHP 7.2.
* This will now result in an E_WARNING being thrown.
*
* PHP version 7.2
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class ForbiddenGetClassNullSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'get_class' => true,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsAbove('7.2') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[1]) === false) {
return;
}
if ($parameters[1]['raw'] !== 'null') {
return;
}
$phpcsFile->addError(
'Passing "null" as the $object to get_class() is not allowed since PHP 7.2.',
$parameters[1]['start'],
'Found'
);
}
}
@@ -0,0 +1,106 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewArrayReduceInitialTypeSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewArrayReduceInitialTypeSniff.
*
* Detect: In PHP 5.2 and lower, the $initial parameter had to be an integer.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewArrayReduceInitialTypeSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'array_reduce' => true,
);
/**
* Tokens which, for the purposes of this sniff, indicate that there is
* a variable element to the value passed.
*
* @var array
*/
private $variableValueTokens = array(
T_VARIABLE,
T_STRING,
T_SELF,
T_PARENT,
T_STATIC,
T_DOUBLE_QUOTED_STRING,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('5.2') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[3]) === false) {
return;
}
$targetParam = $parameters[3];
if ($this->isNumber($phpcsFile, $targetParam['start'], $targetParam['end'], true) !== false) {
return;
}
if ($this->isNumericCalculation($phpcsFile, $targetParam['start'], $targetParam['end']) === true) {
return;
}
$error = 'Passing a non-integer as the value for $initial to array_reduce() is not supported in PHP 5.2 or lower.';
if ($phpcsFile->findNext($this->variableValueTokens, $targetParam['start'], ($targetParam['end'] + 1)) === false) {
$phpcsFile->addError(
$error . ' Found %s',
$targetParam['start'],
'InvalidTypeFound',
array($targetParam['raw'])
);
} else {
$phpcsFile->addWarning(
$error . ' Variable value found. Found %s',
$targetParam['start'],
'VariableFound',
array($targetParam['raw'])
);
}
}
}
@@ -0,0 +1,112 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewFopenModesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewFopenModesSniff.
*
* Detect: Changes in allowed values for the fopen() $mode parameter.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewFopenModesSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'fopen' => true,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
// Version used here should be (above) the highest version from the `newModes` control,
// structure below, i.e. the last PHP version in which a new mode was introduced.
return ($this->supportsBelow('7.1') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[2]) === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$targetParam = $parameters[2];
$errors = array();
for ($i = $targetParam['start']; $i <= $targetParam['end']; $i++) {
if ($tokens[$i]['code'] !== T_CONSTANT_ENCAPSED_STRING) {
continue;
}
if (strpos($tokens[$i]['content'], 'c+') !== false && $this->supportsBelow('5.2.5')) {
$errors['cplusFound'] = array(
'c+',
'5.2.5',
$targetParam['raw'],
);
} elseif (strpos($tokens[$i]['content'], 'c') !== false && $this->supportsBelow('5.2.5')) {
$errors['cFound'] = array(
'c',
'5.2.5',
$targetParam['raw'],
);
}
if (strpos($tokens[$i]['content'], 'e') !== false && $this->supportsBelow('7.0.15')) {
$errors['eFound'] = array(
'e',
'7.0.15',
$targetParam['raw'],
);
}
}
if (empty($errors) === true) {
return;
}
foreach ($errors as $errorCode => $errorData) {
$phpcsFile->addError(
'Passing "%s" as the $mode to fopen() is not supported in PHP %s or lower. Found %s',
$targetParam['start'],
$errorCode,
$errorData
);
}
}
}
@@ -0,0 +1,167 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewHashAlgorithmsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewHashAlgorithmsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewHashAlgorithmsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new hash algorithms, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the hash algorithm appears.
*
* @var array(string => array(string => bool))
*/
protected $newAlgorithms = array(
'md2' => array(
'5.2' => false,
'5.3' => true,
),
'ripemd256' => array(
'5.2' => false,
'5.3' => true,
),
'ripemd320' => array(
'5.2' => false,
'5.3' => true,
),
'salsa10' => array(
'5.2' => false,
'5.3' => true,
),
'salsa20' => array(
'5.2' => false,
'5.3' => true,
),
'snefru256' => array(
'5.2' => false,
'5.3' => true,
),
'sha224' => array(
'5.2' => false,
'5.3' => true,
),
'joaat' => array(
'5.3' => false,
'5.4' => true,
),
'fnv132' => array(
'5.3' => false,
'5.4' => true,
),
'fnv164' => array(
'5.3' => false,
'5.4' => true,
),
'gost-crypto' => array(
'5.5' => false,
'5.6' => true,
),
'sha512/224' => array(
'7.0' => false,
'7.1' => true,
),
'sha512/256' => array(
'7.0' => false,
'7.1' => true,
),
'sha3-224' => array(
'7.0' => false,
'7.1' => true,
),
'sha3-256' => array(
'7.0' => false,
'7.1' => true,
),
'sha3-384' => array(
'7.0' => false,
'7.1' => true,
),
'sha3-512' => array(
'7.0' => false,
'7.1' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$algo = $this->getHashAlgorithmParameter($phpcsFile, $stackPtr);
if (empty($algo) || is_string($algo) === false) {
return;
}
// Bow out if not one of the algorithms we're targetting.
if (isset($this->newAlgorithms[$algo]) === false) {
return;
}
// Check if the algorithm used is new.
$itemInfo = array(
'name' => $algo,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newAlgorithms[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The %s hash algorithm is not present in PHP version %s or earlier';
}
}
@@ -0,0 +1,126 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewNegativeStringOffsetSniff.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewNegativeStringOffsetSniff.
*
* Detect: negative string offsets as parameters passed to functions where this
* was not allowed prior to PHP 7.1.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewNegativeStringOffsetSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array Function name => 1-based parameter offset of the affected parameters => parameter name.
*/
protected $targetFunctions = array(
'file_get_contents' => array(
4 => 'offset',
),
'grapheme_extract' => array(
4 => 'start',
),
'grapheme_stripos' => array(
3 => 'offset',
),
'grapheme_strpos' => array(
3 => 'offset',
),
'iconv_strpos' => array(
3 => 'offset',
),
'mb_ereg_search_setpos' => array(
1 => 'position',
),
'mb_strimwidth' => array(
2 => 'start',
3 => 'width',
),
'mb_stripos' => array(
3 => 'offset',
),
'mb_strpos' => array(
3 => 'offset',
),
'stripos' => array(
3 => 'offset',
),
'strpos' => array(
3 => 'offset',
),
'substr_count' => array(
3 => 'offset',
4 => 'length',
),
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('7.0') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
$functionLC = strtolower($functionName);
foreach ($this->targetFunctions[$functionLC] as $pos => $name) {
if (isset($parameters[$pos]) === false) {
continue;
}
$targetParam = $parameters[$pos];
if ($this->isNegativeNumber($phpcsFile, $targetParam['start'], $targetParam['end']) === false) {
continue;
}
$phpcsFile->addError(
'Negative string offsets were not supported for the $%s parameter in %s() in PHP 7.0 or lower. Found %s',
$targetParam['start'],
'Found',
array(
$name,
$functionName,
$targetParam['raw'],
)
);
}
}
}
@@ -0,0 +1,115 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewPCREModifiers.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\Sniffs\ParameterValues\RemovedPCREModifiersSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewPCREModifiers.
*
* Check for usage of newly added regex modifiers for PCRE functions.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewPCREModifiersSniff extends RemovedPCREModifiersSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'preg_replace' => true,
'preg_filter' => true,
'preg_grep' => true,
'preg_match_all' => true,
'preg_match' => true,
'preg_replace_callback_array' => true,
'preg_replace_callback' => true,
'preg_replace' => true,
'preg_split' => true,
);
/**
* Array listing newly introduced regex modifiers.
*
* The key should be the modifier (case-sensitive!).
* The value should be the PHP version in which the modifier was introduced.
*
* @var array
*/
protected $newModifiers = array(
'J' => array(
'7.1' => false,
'7.2' => true,
),
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
// Version used here should be the highest version from the `$newModifiers` array,
// i.e. the last PHP version in which a new modifier was introduced.
return ($this->supportsBelow('7.2') === false);
}
/**
* Examine the regex modifier string.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param string $functionName The function which contained the pattern.
* @param string $modifiers The regex modifiers found.
*
* @return void
*/
protected function examineModifiers(File $phpcsFile, $stackPtr, $functionName, $modifiers)
{
$error = 'The PCRE regex modifier "%s" is not present in PHP version %s or earlier';
foreach ($this->newModifiers as $modifier => $versionArray) {
if (strpos($modifiers, $modifier) === false) {
continue;
}
$notInVersion = '';
foreach ($versionArray as $version => $present) {
if ($notInVersion === '' && $present === false
&& $this->supportsBelow($version) === true
) {
$notInVersion = $version;
}
}
if ($notInVersion === '') {
continue;
}
$errorCode = $modifier . 'ModifierFound';
$data = array(
$modifier,
$notInVersion,
);
$phpcsFile->addError($error, $stackPtr, $errorCode, $data);
}
}
}
@@ -0,0 +1,123 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewPackFormatSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\NewPackFormatSniff.
*
* Detect: Changes in the allowed values for $format passed to pack().
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewPackFormatSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'pack' => true,
);
/**
* List of new format character codes added to pack().
*
* @var array Regex pattern => Version array.
*/
protected $newFormats = array(
'`([Z])`' => array(
'5.4' => false,
'5.5' => true,
),
'`([qQJP])`' => array(
'5.6.2' => false,
'5.6.3' => true,
),
'`([eEgG])`' => array(
'7.0.14' => false,
'7.0.15' => true, // And 7.1.1.
),
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsBelow('7.1') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[1]) === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$targetParam = $parameters[1];
for ($i = $targetParam['start']; $i <= $targetParam['end']; $i++) {
if ($tokens[$i]['code'] !== T_CONSTANT_ENCAPSED_STRING
&& $tokens[$i]['code'] !== T_DOUBLE_QUOTED_STRING
) {
continue;
}
$content = $tokens[$i]['content'];
if ($tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING) {
$content = $this->stripVariables($content);
}
foreach ($this->newFormats as $pattern => $versionArray) {
if (preg_match($pattern, $content, $matches) !== 1) {
continue;
}
foreach ($versionArray as $version => $present) {
if ($present === false && $this->supportsBelow($version) === true) {
$phpcsFile->addError(
'Passing the $format(s) "%s" to pack() is not supported in PHP %s or lower. Found %s',
$targetParam['start'],
'NewFormatFound',
array(
$matches[1],
$version,
$targetParam['raw'],
)
);
continue 2;
}
}
}
}
}
}
@@ -0,0 +1,111 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedHashAlgorithmsSniff.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedHashAlgorithmsSniff.
*
* Discourages the use of deprecated and removed hash algorithms.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class RemovedHashAlgorithmsSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of removed hash algorithms, which were present in older versions.
*
* The array lists : version number with false (deprecated) and true (removed).
* If's sufficient to list the first version where the hash algorithm was deprecated/removed.
*
* @var array(string => array(string => bool))
*/
protected $removedAlgorithms = array(
'salsa10' => array(
'5.4' => true,
),
'salsa20' => array(
'5.4' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$algo = $this->getHashAlgorithmParameter($phpcsFile, $stackPtr);
if (empty($algo) || is_string($algo) === false) {
return;
}
// Bow out if not one of the algorithms we're targetting.
if (isset($this->removedAlgorithms[$algo]) === false) {
return;
}
$itemInfo = array(
'name' => $algo,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->removedAlgorithms[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The %s hash algorithm is ';
}
}
@@ -0,0 +1,80 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedIconvEncodingSniff.
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedIconvEncodingSniff.
*
* Detect: "The iconv and mbstring configuration options related to encoding
* have been deprecated in favour of default_charset."
*
* {@internal It is unclear which mbstring functions should be targetted, so for now,
* only the iconv function is handled.}}
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedIconvEncodingSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'iconv_set_encoding' => true,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsAbove('5.6') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[1]) === false) {
return;
}
$phpcsFile->addWarning(
'All previously accepted values for the $type parameter of iconv_set_encoding() have been deprecated since PHP 5.6. Found %s',
$parameters[1]['start'],
'DeprecatedValueFound',
$parameters[1]['raw']
);
}
}
@@ -0,0 +1,122 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedMbstringModifiersSniff.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedMbstringModifiersSniff.
*
* PHP version 7.1
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedMbstringModifiersSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* Key is the function name, value the parameter position of the options parameter.
*
* @var array
*/
protected $targetFunctions = array(
'mb_ereg_replace' => 4,
'mb_eregi_replace' => 4,
'mb_regex_set_options' => 1,
'mbereg_replace' => 4, // Undocumented, but valid function alias.
'mberegi_replace' => 4, // Undocumented, but valid function alias.
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
// Version used here should be the highest version from the `$newModifiers` array,
// i.e. the last PHP version in which a new modifier was introduced.
return ($this->supportsAbove('7.1') === false);
}
/**
* Process the parameters of a matched function.
*
* This method has to be made concrete in child classes.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
$tokens = $phpcsFile->getTokens();
$functionNameLc = strtolower($functionName);
// Check whether the options parameter in the function call is passed.
if (isset($parameters[$this->targetFunctions[$functionNameLc]]) === false) {
return;
}
$optionsParam = $parameters[$this->targetFunctions[$functionNameLc]];
$stringToken = $phpcsFile->findNext(Tokens::$stringTokens, $optionsParam['start'], $optionsParam['end'] + 1);
if ($stringToken === false) {
// No string token found in the options parameter, so skip it (e.g. variable passed in).
return;
}
$options = '';
/*
* Get the content of any string tokens in the options parameter and remove the quotes and variables.
*/
for ($i = $stringToken; $i <= $optionsParam['end']; $i++) {
if (isset(Tokens::$stringTokens[$tokens[$i]['code']]) === false) {
continue;
}
$content = $this->stripQuotes($tokens[$i]['content']);
if ($tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING) {
$content = $this->stripVariables($content);
}
$content = trim($content);
if (empty($content) === false) {
$options .= $content;
}
}
if (strpos($options, 'e') !== false) {
$error = 'The Mbstring regex "e" modifier is deprecated since PHP 7.1.';
// The alternative mb_ereg_replace_callback() function is only available since 5.4.1.
if ($this->supportsBelow('5.4.1') === false) {
$error .= ' Use mb_ereg_replace_callback() instead (PHP 5.4.1+).';
}
$phpcsFile->addWarning($error, $stackPtr, 'Deprecated');
}
}
}
@@ -0,0 +1,114 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedNonCryptoHashSniff.
*
* PHP version 7.2
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedNonCryptoHashSniff.
*
* Detect: "The hash_hmac(), hash_hmac_file(), hash_pbkdf2(), and hash_init()
* (with HASH_HMAC) functions no longer accept non-cryptographic hashes."
*
* PHP version 7.2
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedNonCryptoHashSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'hash_hmac' => true,
'hash_hmac_file' => true,
'hash_init' => true,
'hash_pbkdf2' => true,
);
/**
* List of the non-cryptographic hashes.
*
* @var array
*/
protected $disabledCryptos = array(
'adler32' => true,
'crc32' => true,
'crc32b' => true,
'fnv132' => true,
'fnv1a32' => true,
'fnv164' => true,
'fnv1a64' => true,
'joaat' => true,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsAbove('7.2') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[1]) === false) {
return;
}
$targetParam = $parameters[1];
if (isset($this->disabledCryptos[$this->stripQuotes($targetParam['raw'])]) === false) {
return;
}
if (strtolower($functionName) === 'hash_init'
&& (isset($parameters[2]) === false
|| ($parameters[2]['raw'] !== 'HASH_HMAC'
&& $parameters[2]['raw'] !== (string) HASH_HMAC))
) {
// For hash_init(), these hashes are only disabled with HASH_HMAC set.
return;
}
$phpcsFile->addError(
'Non-cryptographic hashes are no longer accepted by function %s() since PHP 7.2. Found: %s',
$targetParam['start'],
$this->stringToErrorCode($functionName),
array(
$functionName,
$targetParam['raw'],
)
);
}
}
@@ -0,0 +1,219 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedPCREModifiersSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2014 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedPCREModifiersSniff.
*
* Check for usage of the `e` modifier with PCRE functions which is deprecated since PHP 5.5
* and removed as of PHP 7.0.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2014 Cu.be Solutions bvba
*/
class RemovedPCREModifiersSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'preg_replace' => true,
'preg_filter' => true,
);
/**
* Regex bracket delimiters.
*
* @var array
*/
protected $doublesSeparators = array(
'{' => '}',
'[' => ']',
'(' => ')',
'<' => '>',
);
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
// Check the first parameter in the function call as that should contain the regex(es).
if (isset($parameters[1]) === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$functionNameLc = strtolower($functionName);
$firstParam = $parameters[1];
// Differentiate between an array of patterns passed and a single pattern.
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $firstParam['start'], ($firstParam['end'] + 1), true);
if ($nextNonEmpty !== false && ($tokens[$nextNonEmpty]['code'] === T_ARRAY || $tokens[$nextNonEmpty]['code'] === T_OPEN_SHORT_ARRAY)) {
$arrayValues = $this->getFunctionCallParameters($phpcsFile, $nextNonEmpty);
if ($functionNameLc === 'preg_replace_callback_array') {
// For preg_replace_callback_array(), the patterns will be in the array keys.
foreach ($arrayValues as $value) {
$hasKey = $phpcsFile->findNext(T_DOUBLE_ARROW, $value['start'], ($value['end'] + 1));
if ($hasKey === false) {
continue;
}
$value['end'] = ($hasKey - 1);
$value['raw'] = trim($phpcsFile->getTokensAsString($value['start'], ($hasKey - $value['start'])));
$this->processRegexPattern($value, $phpcsFile, $value['end'], $functionName);
}
} else {
// Otherwise, the patterns will be in the array values.
foreach ($arrayValues as $value) {
$hasKey = $phpcsFile->findNext(T_DOUBLE_ARROW, $value['start'], ($value['end'] + 1));
if ($hasKey !== false) {
$value['start'] = ($hasKey + 1);
$value['raw'] = trim($phpcsFile->getTokensAsString($value['start'], (($value['end'] + 1) - $value['start'])));
}
$this->processRegexPattern($value, $phpcsFile, $value['end'], $functionName);
}
}
} else {
$this->processRegexPattern($firstParam, $phpcsFile, $stackPtr, $functionName);
}
}
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsAbove('5.5') === false);
}
/**
* Analyse a potential regex pattern for usage of the /e modifier.
*
* @param array $pattern Array containing the start and end token
* pointer of the potential regex pattern and
* the raw string value of the pattern.
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param string $functionName The function which contained the pattern.
*
* @return void
*/
protected function processRegexPattern($pattern, File $phpcsFile, $stackPtr, $functionName)
{
$tokens = $phpcsFile->getTokens();
/*
* The pattern might be build up of a combination of strings, variables
* and function calls. We are only concerned with the strings.
*/
$regex = '';
for ($i = $pattern['start']; $i <= $pattern['end']; $i++) {
if (isset(Tokens::$stringTokens[$tokens[$i]['code']]) === true) {
$content = $this->stripQuotes($tokens[$i]['content']);
if ($tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING) {
$content = $this->stripVariables($content);
}
$regex .= trim($content);
}
}
// Deal with multi-line regexes which were broken up in several string tokens.
if ($tokens[$pattern['start']]['line'] !== $tokens[$pattern['end']]['line']) {
$regex = $this->stripQuotes($regex);
}
if ($regex === '') {
// No string token found in the first parameter, so skip it (e.g. if variable passed in).
return;
}
$regexFirstChar = substr($regex, 0, 1);
// Make sure that the character identified as the delimiter is valid.
// Otherwise, it is a false positive caused by the string concatenation.
if (preg_match('`[a-z0-9\\\\ ]`i', $regexFirstChar) === 1) {
return;
}
if (isset($this->doublesSeparators[$regexFirstChar])) {
$regexEndPos = strrpos($regex, $this->doublesSeparators[$regexFirstChar]);
} else {
$regexEndPos = strrpos($regex, $regexFirstChar);
}
if ($regexEndPos !== false) {
$modifiers = substr($regex, $regexEndPos + 1);
$this->examineModifiers($phpcsFile, $stackPtr, $functionName, $modifiers);
}
}
/**
* Examine the regex modifier string.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param string $functionName The function which contained the pattern.
* @param string $modifiers The regex modifiers found.
*
* @return void
*/
protected function examineModifiers(File $phpcsFile, $stackPtr, $functionName, $modifiers)
{
if (strpos($modifiers, 'e') !== false) {
$error = '%s() - /e modifier is deprecated since PHP 5.5';
$isError = false;
$errorCode = 'Deprecated';
$data = array($functionName);
if ($this->supportsAbove('7.0')) {
$error .= ' and removed since PHP 7.0';
$isError = true;
$errorCode = 'Removed';
}
$this->addMessage($phpcsFile, $error, $stackPtr, $isError, $errorCode, $data);
}
}
}
@@ -0,0 +1,99 @@
<?php
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedSetlocaleStringSniff.
*
* PHP version 4.2
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\ParameterValues;
use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\ParameterValues\RemovedSetlocaleStringSniff.
*
* Detect: Support for the category parameter passed as a string has been removed.
* Only LC_* constants can be used as of this version [7.0.0].
*
* PHP version 4.2
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedSetlocaleStringSniff extends AbstractFunctionCallParameterSniff
{
/**
* Functions to check for.
*
* @var array
*/
protected $targetFunctions = array(
'setlocale' => true,
);
/**
* Do a version check to determine if this sniff needs to run at all.
*
* @return bool
*/
protected function bowOutEarly()
{
return ($this->supportsAbove('4.2') === false);
}
/**
* Process the parameters of a matched function.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack.
* @param string $functionName The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
{
if (isset($parameters[1]) === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$targetParam = $parameters[1];
for ($i = $targetParam['start']; $i <= $targetParam['end']; $i++) {
if ($tokens[$i]['code'] !== T_CONSTANT_ENCAPSED_STRING
&& $tokens[$i]['code'] !== T_DOUBLE_QUOTED_STRING
) {
continue;
}
$message = 'Passing the $category as a string to setlocale() has been deprecated since PHP 4.2';
$isError = false;
$errorCode = 'Deprecated';
$data = array($targetParam['raw']);
if ($this->supportsAbove('7.0') === true) {
$message .= ' and is removed since PHP 7.0';
$isError = true;
$errorCode = 'Removed';
}
$message .= '; Pass one of the LC_* constants instead. Found: %s';
$this->addMessage($phpcsFile, $message, $i, $isError, $errorCode, $data);
break;
}
}
}
@@ -0,0 +1,253 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\ForbiddenCallTimePassByReference.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Gary Rogers <gmrwebde@gmail.com>
* @author Florian Grandel <jerico.dev@gmail.com>
* @copyright 2009 Florian Grandel
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\ForbiddenCallTimePassByReference.
*
* Discourages the use of call time pass by references
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Gary Rogers <gmrwebde@gmail.com>
* @author Florian Grandel <jerico.dev@gmail.com>
* @copyright 2009 Florian Grandel
*/
class ForbiddenCallTimePassByReferenceSniff extends Sniff
{
/**
* Tokens that represent assignments or equality comparisons.
*
* Near duplicate of Tokens::$assignmentTokens + Tokens::$equalityTokens.
* Copied in for PHPCS cross-version compatibility.
*
* @var array
*/
private $assignOrCompare = array(
// Equality tokens.
'T_IS_EQUAL' => true,
'T_IS_NOT_EQUAL' => true,
'T_IS_IDENTICAL' => true,
'T_IS_NOT_IDENTICAL' => true,
'T_IS_SMALLER_OR_EQUAL' => true,
'T_IS_GREATER_OR_EQUAL' => true,
// Assignment tokens.
'T_EQUAL' => true,
'T_AND_EQUAL' => true,
'T_OR_EQUAL' => true,
'T_CONCAT_EQUAL' => true,
'T_DIV_EQUAL' => true,
'T_MINUS_EQUAL' => true,
'T_POW_EQUAL' => true,
'T_MOD_EQUAL' => true,
'T_MUL_EQUAL' => true,
'T_PLUS_EQUAL' => true,
'T_XOR_EQUAL' => true,
'T_DOUBLE_ARROW' => true,
'T_SL_EQUAL' => true,
'T_SR_EQUAL' => true,
'T_COALESCE_EQUAL' => true,
'T_ZSR_EQUAL' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_STRING,
T_VARIABLE,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('5.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
// Skip tokens that are the names of functions or classes
// within their definitions. For example: function myFunction...
// "myFunction" is T_STRING but we should skip because it is not a
// function or method *call*.
$findTokens = Tokens::$emptyTokens;
$findTokens[] = T_BITWISE_AND;
$prevNonEmpty = $phpcsFile->findPrevious(
$findTokens,
($stackPtr - 1),
null,
true
);
if ($prevNonEmpty !== false && in_array($tokens[$prevNonEmpty]['type'], array('T_FUNCTION', 'T_CLASS', 'T_INTERFACE', 'T_TRAIT'), true)) {
return;
}
// If the next non-whitespace token after the function or method call
// is not an opening parenthesis then it can't really be a *call*.
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($openBracket === false || $tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS
|| isset($tokens[$openBracket]['parenthesis_closer']) === false
) {
return;
}
// Get the function call parameters.
$parameters = $this->getFunctionCallParameters($phpcsFile, $stackPtr);
if (count($parameters) === 0) {
return;
}
// Which nesting level is the one we are interested in ?
$nestedParenthesisCount = 1;
if (isset($tokens[$openBracket]['nested_parenthesis'])) {
$nestedParenthesisCount = count($tokens[$openBracket]['nested_parenthesis']) + 1;
}
foreach ($parameters as $parameter) {
if ($this->isCallTimePassByReferenceParam($phpcsFile, $parameter, $nestedParenthesisCount) === true) {
// T_BITWISE_AND represents a pass-by-reference.
$error = 'Using a call-time pass-by-reference is deprecated since PHP 5.3';
$isError = false;
$errorCode = 'Deprecated';
if ($this->supportsAbove('5.4')) {
$error .= ' and prohibited since PHP 5.4';
$isError = true;
$errorCode = 'NotAllowed';
}
$this->addMessage($phpcsFile, $error, $parameter['start'], $isError, $errorCode);
}
}
}
/**
* Determine whether a parameter is passed by reference.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param array $parameter Information on the current parameter
* to be examined.
* @param int $nestingLevel Target nesting level.
*
* @return bool
*/
protected function isCallTimePassByReferenceParam(File $phpcsFile, $parameter, $nestingLevel)
{
$tokens = $phpcsFile->getTokens();
$searchStartToken = $parameter['start'] - 1;
$searchEndToken = $parameter['end'] + 1;
$nextVariable = $searchStartToken;
do {
$nextVariable = $phpcsFile->findNext(array(T_VARIABLE, T_OPEN_SHORT_ARRAY, T_CLOSURE), ($nextVariable + 1), $searchEndToken);
if ($nextVariable === false) {
return false;
}
// Ignore anything within short array definition brackets.
if ($tokens[$nextVariable]['type'] === 'T_OPEN_SHORT_ARRAY'
&& (isset($tokens[$nextVariable]['bracket_opener'])
&& $tokens[$nextVariable]['bracket_opener'] === $nextVariable)
&& isset($tokens[$nextVariable]['bracket_closer'])
) {
// Skip forward to the end of the short array definition.
$nextVariable = $tokens[$nextVariable]['bracket_closer'];
continue;
}
// Skip past closures passed as function parameters.
if ($tokens[$nextVariable]['type'] === 'T_CLOSURE'
&& (isset($tokens[$nextVariable]['scope_condition'])
&& $tokens[$nextVariable]['scope_condition'] === $nextVariable)
&& isset($tokens[$nextVariable]['scope_closer'])
) {
// Skip forward to the end of the closure declaration.
$nextVariable = $tokens[$nextVariable]['scope_closer'];
continue;
}
// Make sure the variable belongs directly to this function call
// and is not inside a nested function call or array.
if (isset($tokens[$nextVariable]['nested_parenthesis']) === false
|| (count($tokens[$nextVariable]['nested_parenthesis']) !== $nestingLevel)
) {
continue;
}
// Checking this: $value = my_function(...[*]$arg...).
$tokenBefore = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($nextVariable - 1),
$searchStartToken,
true
);
if ($tokenBefore === false || $tokens[$tokenBefore]['code'] !== T_BITWISE_AND) {
// Nothing before the token or no &.
continue;
}
if ($phpcsFile->isReference($tokenBefore) === false) {
continue;
}
// Checking this: $value = my_function(...[*]&$arg...).
$tokenBefore = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($tokenBefore - 1),
$searchStartToken,
true
);
// Prevent false positive on assign by reference and compare with reference
// within function call parameters.
if (isset($this->assignOrCompare[$tokens[$tokenBefore]['type']])) {
continue;
}
// The found T_BITWISE_AND represents a pass-by-reference.
return true;
} while ($nextVariable < $searchEndToken);
// This code should never be reached, but here in case of weird bugs.
return false;
}
}
@@ -0,0 +1,108 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewArrayStringDereferencingSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\NewArrayStringDereferencingSniff.
*
* Array and string literals can now be dereferenced directly to access individual elements and characters.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewArrayStringDereferencingSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_ARRAY,
T_OPEN_SHORT_ARRAY,
T_CONSTANT_ENCAPSED_STRING,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.4') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
switch ($tokens[$stackPtr]['code']) {
case T_CONSTANT_ENCAPSED_STRING:
$type = 'string literals';
$end = $stackPtr;
break;
case T_ARRAY:
if (isset($tokens[$stackPtr]['parenthesis_closer']) === false) {
// Live coding.
return;
} else {
$type = 'arrays';
$end = $tokens[$stackPtr]['parenthesis_closer'];
}
break;
case T_OPEN_SHORT_ARRAY:
if (isset($tokens[$stackPtr]['bracket_closer']) === false) {
// Live coding.
return;
} else {
$type = 'arrays';
$end = $tokens[$stackPtr]['bracket_closer'];
}
break;
}
if (isset($type, $end) === false) {
// Shouldn't happen, but for some reason did.
return;
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true, null, true);
if ($nextNonEmpty !== false
&& ($tokens[$nextNonEmpty]['type'] === 'T_OPEN_SQUARE_BRACKET'
|| $tokens[$nextNonEmpty]['type'] === 'T_OPEN_SHORT_ARRAY') // Work around bug #1381 in PHPCS 2.8.1 and lower.
) {
$phpcsFile->addError(
'Direct array dereferencing of %s is not present in PHP version 5.4 or earlier',
$nextNonEmpty,
'Found',
array($type)
);
}
}
}
@@ -0,0 +1,113 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewClassMemberAccessSniff.
*
* PHP version 5.4
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\NewClassMemberAccessSniff.
*
* PHP 5.4: Class member access on instantiation has been added, e.g. (new Foo)->bar().
* PHP 7.0: Class member access on cloning has been added, e.g. (clone $foo)->bar().
*
* PHP version 5.4
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewClassMemberAccessSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_NEW,
T_CLONE,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_NEW && $this->supportsBelow('5.3') !== true) {
return;
} elseif ($tokens[$stackPtr]['code'] === T_CLONE && $this->supportsBelow('5.6') !== true) {
return;
}
if (isset($tokens[$stackPtr]['nested_parenthesis']) === false) {
// The `new className/clone $a` has to be in parentheses, without is not supported.
return;
}
$parenthesisCloser = end($tokens[$stackPtr]['nested_parenthesis']);
$parenthesisOpener = key($tokens[$stackPtr]['nested_parenthesis']);
if (isset($tokens[$parenthesisOpener]['parenthesis_owner']) === true) {
// If there is an owner, these parentheses are for a different purpose.
return;
}
$prevBeforeParenthesis = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($parenthesisOpener - 1), null, true);
if ($prevBeforeParenthesis !== false && $tokens[$prevBeforeParenthesis]['code'] === T_STRING) {
// This is most likely a function call with the new/cloned object as a parameter.
return;
}
$nextAfterParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($parenthesisCloser + 1), null, true);
if ($nextAfterParenthesis === false) {
// Live coding.
return;
}
if ($tokens[$nextAfterParenthesis]['code'] !== T_OBJECT_OPERATOR
&& $tokens[$nextAfterParenthesis]['code'] !== T_OPEN_SQUARE_BRACKET
) {
return;
}
$data = array('instantiation', '5.3');
$errorCode = 'OnNewFound';
if ($tokens[$stackPtr]['code'] === T_CLONE) {
$data = array('cloning', '5.6');
$errorCode = 'OnCloneFound';
}
$phpcsFile->addError(
'Class member access on object %s was not supported in PHP %s or earlier',
$parenthesisCloser,
$errorCode,
$data
);
}
}
@@ -0,0 +1,85 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewDynamicAccessToStaticSniff.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\NewDynamicAccessToStaticSniff.
*
* As of PHP 5.3, static properties and methods as well as class constants
* can be accessed using a dynamic (variable) class name.
*
* PHP version 5.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewDynamicAccessToStaticSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_DOUBLE_COLON,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.2') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
// Disregard `static::` as well. Late static binding is reported by another sniff.
if ($tokens[$prevNonEmpty]['code'] === T_SELF
|| $tokens[$prevNonEmpty]['code'] === T_PARENT
|| $tokens[$prevNonEmpty]['code'] === T_STATIC
) {
return;
}
if ($tokens[$prevNonEmpty]['code'] === T_STRING) {
$prevPrevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true);
if ($tokens[$prevPrevNonEmpty]['code'] !== T_OBJECT_OPERATOR) {
return;
}
}
$phpcsFile->addError(
'Static class properties and methods, as well as class constants, could not be accessed using a dynamic (variable) classname in PHP 5.2 or earlier.',
$stackPtr,
'Found'
);
}
}
@@ -0,0 +1,247 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewFlexibleHeredocNowdocSniff.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* New Flexible Heredoc Nowdoc.
*
* As of PHP 7.3:
* - The body and the closing marker of a Heredoc/nowdoc can be indented;
* - The closing marker no longer needs to be on a line by itself;
* - The heredoc/nowdoc body may no longer contain the closing marker at the
* start of any of its lines.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewFlexibleHeredocNowdocSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$targets = array(
T_END_HEREDOC,
T_END_NOWDOC,
);
if (version_compare(PHP_VERSION_ID, '70299', '>') === false) {
// Start identifier of a PHP 7.3 flexible heredoc/nowdoc.
$targets[] = T_STRING;
}
return $targets;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
/*
* Due to a tokenizer bug which gets hit when the PHP 7.3 heredoc/nowdoc syntax
* is used, this part of the sniff cannot possibly work on PHPCS < 2.6.0.
* See upstream issue #928.
*/
if ($this->supportsBelow('7.2') === true && version_compare(PHPCSHelper::getVersion(), '2.6.0', '>=')) {
$this->detectIndentedNonStandAloneClosingMarker($phpcsFile, $stackPtr);
}
$tokens = $phpcsFile->getTokens();
if ($this->supportsAbove('7.3') === true && $tokens[$stackPtr]['code'] !== T_STRING) {
$this->detectClosingMarkerInBody($phpcsFile, $stackPtr);
}
}
/**
* Detect indented and/or non-stand alone closing markers.
*
* @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
*/
protected function detectIndentedNonStandAloneClosingMarker(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$indentError = 'Heredoc/nowdoc with an indented closing marker is not supported in PHP 7.2 or earlier.';
$indentErrorCode = 'IndentedClosingMarker';
$trailingError = 'Having code - other than a semi-colon or new line - after the closing marker of a heredoc/nowdoc is not supported in PHP 7.2 or earlier.';
$trailingErrorCode = 'ClosingMarkerNoNewLine';
if (version_compare(PHP_VERSION_ID, '70299', '>') === true) {
/*
* Check for indented closing marker.
*/
if (ltrim($tokens[$stackPtr]['content']) !== $tokens[$stackPtr]['content']) {
$phpcsFile->addError($indentError, $stackPtr, $indentErrorCode);
}
/*
* Check for tokens after the closing marker.
*/
$nextNonWhitespace = $phpcsFile->findNext(array(T_WHITESPACE, T_SEMICOLON), ($stackPtr + 1), null, true);
if ($tokens[$stackPtr]['line'] === $tokens[$nextNonWhitespace]['line']) {
$phpcsFile->addError($trailingError, $stackPtr, $trailingErrorCode);
}
} else {
// For PHP < 7.3, we're only interested in T_STRING tokens.
if ($tokens[$stackPtr]['code'] !== T_STRING) {
return;
}
if (preg_match('`^<<<([\'"]?)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\1[\r\n]+`', $tokens[$stackPtr]['content'], $matches) !== 1) {
// Not the start of a PHP 7.3 flexible heredoc/nowdoc.
return;
}
$identifier = $matches[2];
for ($i = ($stackPtr + 1); $i <= $phpcsFile->numTokens; $i++) {
if ($tokens[$i]['code'] !== T_ENCAPSED_AND_WHITESPACE) {
continue;
}
$trimmed = ltrim($tokens[$i]['content']);
if (strpos($trimmed, $identifier) !== 0) {
continue;
}
// OK, we've found the PHP 7.3 flexible heredoc/nowdoc closing marker.
/*
* Check for indented closing marker.
*/
if ($trimmed !== $tokens[$i]['content']) {
// Indent found before closing marker.
$phpcsFile->addError($indentError, $i, $indentErrorCode);
}
/*
* Check for tokens after the closing marker.
*/
// Remove the identifier.
$afterMarker = substr($trimmed, strlen($identifier));
// Remove a potential semi-colon at the beginning of what's left of the string.
$afterMarker = ltrim($afterMarker, ';');
// Remove new line characters at the end of the string.
$afterMarker = rtrim($afterMarker, "\r\n");
if ($afterMarker !== '') {
$phpcsFile->addError($trailingError, $i, $trailingErrorCode);
}
break;
}
}
}
/**
* Detect heredoc/nowdoc identifiers at the start of lines in the heredoc/nowdoc body.
*
* @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
*/
protected function detectClosingMarkerInBody(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$error = 'The body of a heredoc/nowdoc can not contain the heredoc/nowdoc closing marker as text at the start of a line since PHP 7.3.';
$errorCode = 'ClosingMarkerNoNewLine';
if (version_compare(PHP_VERSION_ID, '70299', '>') === true) {
$nextNonWhitespace = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true, null, true);
if ($nextNonWhitespace === false
|| $tokens[$nextNonWhitespace]['code'] === T_SEMICOLON
|| (($tokens[$nextNonWhitespace]['code'] === T_COMMA
|| $tokens[$nextNonWhitespace]['code'] === T_STRING_CONCAT)
&& $tokens[$nextNonWhitespace]['line'] !== $tokens[$stackPtr]['line'])
) {
// This is most likely a correctly identified closing marker.
return;
}
// The real closing tag has to be before the next heredoc/nowdoc.
$nextHereNowDoc = $phpcsFile->findNext(array(T_START_HEREDOC, T_START_NOWDOC), ($stackPtr + 1));
if ($nextHereNowDoc === false) {
$nextHereNowDoc = null;
}
$identifier = trim($tokens[$stackPtr]['content']);
$realClosingMarker = $stackPtr;
while (($realClosingMarker = $phpcsFile->findNext(T_STRING, ($realClosingMarker + 1), $nextHereNowDoc, false, $identifier)) !== false) {
$prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($realClosingMarker - 1), null, true);
if ($prevNonWhitespace === false
|| $tokens[$prevNonWhitespace]['line'] === $tokens[$realClosingMarker]['line']
) {
// Marker text found, but not at the start of the line.
continue;
}
// The original T_END_HEREDOC/T_END_NOWDOC was most likely incorrect as we've found
// a possible alternative closing marker.
$phpcsFile->addError($error, $stackPtr, $errorCode);
break;
}
} else {
if (isset($tokens[$stackPtr]['scope_closer'], $tokens[$stackPtr]['scope_opener']) === true
&& $tokens[$stackPtr]['scope_closer'] === $stackPtr
) {
$opener = $tokens[$stackPtr]['scope_opener'];
} else {
// PHPCS < 3.0.2 did not add scope_* values for Nowdocs.
$opener = $phpcsFile->findPrevious(T_START_NOWDOC, ($stackPtr - 1));
if ($opener === false) {
return;
}
}
$quotedIdentifier = preg_quote($tokens[$stackPtr]['content'], '`');
// Throw an error for each line in the body which starts with the identifier.
for ($i = ($opener + 1); $i < $stackPtr; $i++) {
if (preg_match('`^[ \t]*' . $quotedIdentifier . '\b`', $tokens[$i]['content']) === 1) {
$phpcsFile->addError($error, $i, $errorCode);
}
}
}
}
}
@@ -0,0 +1,95 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewFunctionArrayDereferencingSniff.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\NewFunctionArrayDereferencingSniff.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class NewFunctionArrayDereferencingSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_STRING);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
// Next non-empty token should be the open parenthesis.
$openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($openParenthesis === false || $tokens[$openParenthesis]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
// Don't throw errors during live coding.
if (isset($tokens[$openParenthesis]['parenthesis_closer']) === false) {
return;
}
// Is this T_STRING really a function or method call ?
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prevToken !== false && in_array($tokens[$prevToken]['code'], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true) === false) {
$ignore = array(
T_FUNCTION => true,
T_CONST => true,
T_USE => true,
T_NEW => true,
T_CLASS => true,
T_INTERFACE => true,
);
if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function or method.
return;
}
}
$closeParenthesis = $tokens[$openParenthesis]['parenthesis_closer'];
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenthesis + 1), null, true, null, true);
if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['type'] === 'T_OPEN_SQUARE_BRACKET') {
$phpcsFile->addError(
'Function array dereferencing is not present in PHP version 5.3 or earlier',
$nextNonEmpty,
'Found'
);
}
}
}
@@ -0,0 +1,115 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewFunctionCallTrailingCommaSniff.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\NewFunctionCallTrailingCommaSniff.
*
* PHP version 7.3
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewFunctionCallTrailingCommaSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_STRING,
T_VARIABLE,
T_ISSET,
T_UNSET,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('7.2') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($tokens[$nextNonEmpty]['code'] !== T_OPEN_PARENTHESIS
|| isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false
) {
return;
}
if ($tokens[$stackPtr]['code'] === T_STRING) {
$ignore = array(
T_FUNCTION => true,
T_CONST => true,
T_USE => true,
);
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if (isset($ignore[$tokens[$prevNonEmpty]['code']]) === true) {
// Not a function call.
return;
}
}
$closer = $tokens[$nextNonEmpty]['parenthesis_closer'];
$lastInParenthesis = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($closer - 1), $nextNonEmpty, true);
if ($tokens[$lastInParenthesis]['code'] !== T_COMMA) {
return;
}
$data = array();
switch ($tokens[$stackPtr]['code']) {
case T_ISSET:
$data[] = 'calls to isset()';
$errorCode = 'FoundInIsset';
break;
case T_UNSET:
$data[] = 'calls to unset()';
$errorCode = 'FoundInUnset';
break;
default:
$data[] = 'function calls';
$errorCode = 'FoundInFunctionCall';
break;
}
$phpcsFile->addError(
'Trailing comma\'s are not allowed in %s in PHP 7.2 or earlier',
$lastInParenthesis,
$errorCode,
$data
);
}
}
@@ -0,0 +1,74 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\NewShortArray.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Alex Miroshnikov <unknown@example.com>
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Syntax\NewShortArray.
*
* Short array syntax is available since PHP 5.4
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Alex Miroshnikov <unknown@example.com>
*/
class NewShortArraySniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_OPEN_SHORT_ARRAY,
T_CLOSE_SHORT_ARRAY,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
$error = '%s is available since 5.4';
$data = array();
if ($token['type'] === 'T_OPEN_SHORT_ARRAY') {
$data[] = 'Short array syntax (open)';
} elseif ($token['type'] === 'T_CLOSE_SHORT_ARRAY') {
$data[] = 'Short array syntax (close)';
}
$phpcsFile->addError($error, $stackPtr, 'Found', $data);
}
}
@@ -0,0 +1,77 @@
<?php
/**
* \PHPCompatibility\Sniffs\Syntax\RemovedNewReferenceSniff.
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
namespace PHPCompatibility\Sniffs\Syntax;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Syntax\RemovedNewReferenceSniff.
*
* Discourages the use of assigning the return value of new by reference
*
* PHP version 5.4
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @copyright 2012 Cu.be Solutions bvba
*/
class RemovedNewReferenceSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_NEW);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('5.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['type'] !== 'T_BITWISE_AND') {
return;
}
$error = 'Assigning the return value of new by reference is deprecated in PHP 5.3';
$isError = false;
$errorCode = 'Deprecated';
if ($this->supportsAbove('7.0') === true) {
$error .= ' and has been removed in PHP 7.0';
$isError = true;
$errorCode = 'Removed';
}
$this->addMessage($phpcsFile, $error, $stackPtr, $isError, $errorCode);
}
}
@@ -0,0 +1,203 @@
<?php
/**
* \PHPCompatibility\Sniffs\TypeCasts\NewTypeCastsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\TypeCasts;
use PHPCompatibility\AbstractNewFeatureSniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\TypeCasts\NewTypeCastsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewTypeCastsSniff extends AbstractNewFeatureSniff
{
/**
* A list of new type casts, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the keyword appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newTypeCasts = array(
'T_UNSET_CAST' => array(
'4.4' => false,
'5.0' => true,
'description' => 'The unset cast',
),
'T_BINARY_CAST' => array(
'5.2.0' => false,
'5.2.1' => true,
'description' => 'The binary cast',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array();
foreach ($this->newTypeCasts as $token => $versions) {
if (defined($token)) {
$tokens[] = constant($token);
}
}
/*
* Work around tokenizer issues.
*
* - (binary) cast is incorrectly tokenized as T_STRING_CAST by PHP and PHPCS.
* - b"something" binary cast is incorrectly tokenized as T_CONSTANT_ENCAPSED_STRING by PHP and PHPCS.
* - Since PHPCS 3.4.0, PHPCS *will* tokenize these correctly.
*
* @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1574
*/
if (version_compare(PHPCSHelper::getVersion(), '3.4.0', '<') === true) {
$tokens[] = T_STRING_CAST;
$tokens[] = T_CONSTANT_ENCAPSED_STRING;
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenType = $tokens[$stackPtr]['type'];
// Detect incorrectly tokenized binary casts.
if (isset($this->newTypeCasts[$tokenType]) === false) {
$tokenContent = $tokens[$stackPtr]['content'];
switch ($tokenType) {
case 'T_STRING_CAST':
if (preg_match('`^\(\s*binary\s*\)$`i', $tokenContent) !== 1) {
return;
}
$tokenType = 'T_BINARY_CAST';
break;
case 'T_CONSTANT_ENCAPSED_STRING':
if ((strpos($tokenContent, 'b"') === 0 && substr($tokenContent, -1) === '"')
|| (strpos($tokenContent, "b'") === 0 && substr($tokenContent, -1) === "'")
) {
$tokenType = 'T_BINARY_CAST';
} else {
return;
}
break;
}
}
// If the translation did not yield one of the tokens we are looking for, bow out.
if (isset($this->newTypeCasts[$tokenType]) === false) {
return;
}
$itemInfo = array(
'name' => $tokenType,
'content' => $tokens[$stackPtr]['content'],
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->newTypeCasts[$itemInfo['name']];
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('description');
}
/**
* Retrieve the relevant detail (version) information for use in an error message.
*
* @param array $itemArray Version and other information about the item.
* @param array $itemInfo Base information about the item.
*
* @return array
*/
public function getErrorInfo(array $itemArray, array $itemInfo)
{
$errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
$errorInfo['description'] = $itemArray['description'];
return $errorInfo;
}
/**
* Filter the error message before it's passed to PHPCS.
*
* @param string $error The error message which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return string
*/
protected function filterErrorMsg($error, array $itemInfo, array $errorInfo)
{
return $error . '. Found: %s';
}
/**
* Filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
$data[0] = $errorInfo['description'];
$data[] = $itemInfo['content'];
return $data;
}
}
@@ -0,0 +1,127 @@
<?php
/**
* \PHPCompatibility\Sniffs\TypeCasts\RemovedTypeCastsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\TypeCasts;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\TypeCasts\RemovedTypeCastsSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class RemovedTypeCastsSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of deprecated and removed type casts with their alternatives.
*
* The array lists : version number with false (deprecated) or true (removed) and an alternative function.
* If no alternative exists, it is NULL, i.e, the function should just not be used.
*
* @var array(string => array(string => bool|string|null))
*/
protected $deprecatedTypeCasts = array(
'T_UNSET_CAST' => array(
'7.2' => false,
'alternative' => 'unset()',
'description' => 'unset',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
$tokens = array();
foreach ($this->deprecatedTypeCasts as $token => $versions) {
$tokens[] = constant($token);
}
return $tokens;
}
/**
* 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(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenType = $tokens[$stackPtr]['type'];
$itemInfo = array(
'name' => $tokenType,
'description' => $this->deprecatedTypeCasts[$tokenType]['description'],
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get an array of the non-PHP-version array keys used in a sub-array.
*
* @return array
*/
protected function getNonVersionArrayKeys()
{
return array('description', 'alternative');
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->deprecatedTypeCasts[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return 'The %s cast is ';
}
/**
* Filter the error data before it's passed to PHPCS.
*
* @param array $data The error data array which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return array
*/
protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
{
$data[0] = $itemInfo['description'];
return $data;
}
}
@@ -0,0 +1,171 @@
<?php
/**
* \PHPCompatibility\Sniffs\Upgrade\LowPHPCSSniff.
*
* @category Upgrade
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Upgrade;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
/**
* \PHPCompatibility\Sniffs\Upgrade\LowPHPCSSniff.
*
* Add a notification for users of low PHPCS versions.
*
* Originally PHPCompatibility supported PHPCS 1.5.x, 2.x and since PHPCompatibility 8.0.0, 3.x.
* Support for PHPCS < 2.3.0 has been dropped in PHPCompatibility 9.0.0.
*
* The standard will - up to a point - still work for users of lower
* PHPCS versions, but will give less accurate results and may throw
* notices and warnings (or even fatal out).
*
* This sniff adds an explicit error/warning for users of the standard
* using a PHPCS version below the recommended version.
*
* @category Upgrade
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class LowPHPCSSniff extends Sniff
{
/**
* The minimum supported PHPCS version.
*
* Users on PHPCS versions below this will see an ERROR message.
*
* @var string
*/
protected $minSupportedVersion = '2.3.0';
/**
* The minimum recommended PHPCS version.
*
* Users on PHPCS versions below this will see a WARNING.
*
* @var string
*/
protected $minRecommendedVersion = '2.6.0';
/**
* Keep track of whether this sniff needs to actually run.
*
* This will be set to `false` when either a high enough PHPCS
* version is detected or once the error/warning has been thrown,
* to make sure that the notice will only be thrown once per run.
*
* @var bool
*/
private $examine = true;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_OPEN_TAG,
);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
// Don't do anything if the warning has already been thrown or is not necessary.
if ($this->examine === false) {
return ($phpcsFile->numTokens + 1);
}
$phpcsVersion = PHPCSHelper::getVersion();
// Don't do anything if the PHPCS version used is above the minimum recommended version.
if (version_compare($phpcsVersion, $this->minRecommendedVersion, '>=')) {
$this->examine = false;
return ($phpcsFile->numTokens + 1);
}
if (version_compare($phpcsVersion, $this->minSupportedVersion, '<')) {
$isError = true;
$message = "IMPORTANT: Please be advised that the minimum PHP_CodeSniffer version the PHPCompatibility standard supports is %s. You are currently using PHP_CodeSniffer %s. Please upgrade your PHP_CodeSniffer installation. The recommended version of PHP_CodeSniffer for PHPCompatibility is %s or higher.";
$errorCode = 'Unsupported_' . $this->stringToErrorCode($this->minSupportedVersion);
$replacements = array(
$this->minSupportedVersion,
$phpcsVersion,
$this->minRecommendedVersion,
$errorCode,
);
} else {
$isError = false;
$message = "IMPORTANT: Please be advised that for the most reliable PHPCompatibility results, PHP_CodeSniffer %s or higher should be used. Support for lower versions will be dropped in the foreseeable future. You are currently using PHP_CodeSniffer %s. Please upgrade your PHP_CodeSniffer installation to version %s or higher.";
$errorCode = 'BelowRecommended_' . $this->stringToErrorCode($this->minRecommendedVersion);
$replacements = array(
$this->minRecommendedVersion,
$phpcsVersion,
$this->minRecommendedVersion,
$errorCode,
);
}
/*
* Figure out the report width to determine how long the delimiter lines should be.
*
* This is not an exact calculation as there are a number of unknowns at the time the
* notice is thrown (whether there are other notices for the file, whether those are
* warnings or errors, whether there are auto-fixable issues etc).
*
* In other words, this is just an approximation to get a reasonably stable and
* readable message layout format.
*
* {@internal
* PHPCS has had some changes as to how the messages display over the years.
* Most significantly in 2.4.0 it was attempted to solve an issue with messages
* containing new lines. Unfortunately, that solution is buggy.
* An improved version has been pulled upstream and will hopefully make it
* into PHPCS 3.3.1/3.4.0.
*
* Anyway, this means that instead of new lines, delimiter lines will be used to improved
* the readability of the (long) message.
*
* Also, as of PHPCS 2.2.0, the report width when using the `-s` option is 8 wider than
* it should be. A patch for that is included in the same upstream PR.
*
* If/when the upstream PR has been merged and the minimum supported/recommended version
* of PHPCompatibility would go beyond that, the below code should be adjusted.}}
*/
$reportWidth = PHPCSHelper::getCommandLineData($phpcsFile, 'reportWidth');
$showSources = PHPCSHelper::getCommandLineData($phpcsFile, 'showSources');
if ($showSources === true && version_compare($phpcsVersion, '2.3.0', '>=')) {
$reportWidth += 6;
}
$messageWidth = ($reportWidth - 15); // 15 is length of " # | WARNING | ".
$delimiterLine = str_repeat('-', ($messageWidth));
$disableNotice = 'To disable this notice, add --exclude=PHPCompatibility.Upgrade.LowPHPCS to your command or add <exclude name="PHPCompatibility.Upgrade.LowPHPCS.%s"/> to your custom ruleset. ';
$thankYou = 'Thank you for using PHPCompatibility!';
$message .= ' ' . $delimiterLine;
$message .= ' ' . $disableNotice;
$message .= ' ' . $delimiterLine;
$message .= ' ' . $thankYou;
$this->addMessage($phpcsFile, $message, 0, $isError, $errorCode, $replacements);
$this->examine = false;
}
}
@@ -0,0 +1,105 @@
<?php
/**
* \PHPCompatibility\Sniffs\UseDeclarations\NewGroupUseDeclarationsSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\UseDeclarations;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\UseDeclarations\NewGroupUseDeclarationsSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class NewGroupUseDeclarationsSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
if (defined('T_OPEN_USE_GROUP')) {
return array(T_OPEN_USE_GROUP);
} else {
return array(T_USE);
}
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('7.1') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Deal with PHPCS pre-2.6.0.
if ($token['code'] === T_USE) {
$hasCurlyBrace = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, ($stackPtr + 1), null, false, null, true);
if ($hasCurlyBrace === false) {
return;
}
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($hasCurlyBrace - 1), null, true);
if ($prevToken === false || $tokens[$prevToken]['code'] !== T_NS_SEPARATOR) {
return;
}
$stackPtr = $hasCurlyBrace;
}
// Still here ? In that case, it is a group use statement.
if ($this->supportsBelow('5.6') === true) {
$phpcsFile->addError(
'Group use declarations are not allowed in PHP 5.6 or earlier',
$stackPtr,
'Found'
);
}
$closers = array(T_CLOSE_CURLY_BRACKET);
if (defined('T_CLOSE_USE_GROUP')) {
$closers[] = T_CLOSE_USE_GROUP;
}
$closeCurly = $phpcsFile->findNext($closers, ($stackPtr + 1), null, false, null, true);
if ($closeCurly === false) {
return;
}
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($closeCurly - 1), null, true);
if ($tokens[$prevToken]['code'] === T_COMMA) {
$phpcsFile->addError(
'Trailing comma\'s are not allowed in group use statements in PHP 7.1 or earlier',
$prevToken,
'TrailingCommaFound'
);
}
}
}
@@ -0,0 +1,102 @@
<?php
/**
* \PHPCompatibility\Sniffs\UseDeclarations\NewUseConstFunctionSniff.
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\UseDeclarations;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\UseDeclarations\NewUseConstFunctionSniff.
*
* The use operator has been extended to support importing functions and
* constants in addition to classes. This is achieved via the use function
* and use const constructs, respectively.
*
* PHP version 5.6
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewUseConstFunctionSniff extends Sniff
{
/**
* A list of keywords that can follow use statements.
*
* @var array(string => string)
*/
protected $validUseNames = array(
'const' => true,
'function' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_USE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.5') !== true) {
return;
}
$tokens = $phpcsFile->getTokens();
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty === false) {
// Live coding.
return;
}
if (isset($this->validUseNames[strtolower($tokens[$nextNonEmpty]['content'])]) === false) {
// Not a `use const` or `use function` statement.
return;
}
// `use const` and `use function` have to be followed by the function/constant name.
$functionOrConstName = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true);
if ($functionOrConstName === false
// Identifies as T_AS or T_STRING, this covers both.
|| ($tokens[$functionOrConstName]['content'] === 'as'
|| $tokens[$functionOrConstName]['code'] === T_COMMA)
) {
// Live coding or incorrect use of reserved keyword, but that is
// covered by the ForbiddenNames sniff.
return;
}
// Still here ? In that case we have encountered a `use const` or `use function` statement.
$phpcsFile->addError(
'Importing functions and constants through a "use" statement is not supported in PHP 5.5 or lower.',
$nextNonEmpty,
'Found'
);
}
}
@@ -0,0 +1,124 @@
<?php
/**
* \PHPCompatibility\Sniffs\Variables\ForbiddenGlobalVariableVariableSniff.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
namespace PHPCompatibility\Sniffs\Variables;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Variables\ForbiddenGlobalVariableVariableSniff.
*
* Variable variables are forbidden with global
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim@cu.be>
*/
class ForbiddenGlobalVariableVariableSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_GLOBAL);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$endOfStatement = $phpcsFile->findNext(array(T_SEMICOLON, T_CLOSE_TAG), ($stackPtr + 1));
if ($endOfStatement === false) {
// No semi-colon - live coding.
return;
}
for ($ptr = ($stackPtr + 1); $ptr <= $endOfStatement; $ptr++) {
$errorThrown = false;
$nextComma = $phpcsFile->findNext(T_COMMA, $ptr, $endOfStatement, false, null, true);
$varEnd = ($nextComma === false) ? $endOfStatement : $nextComma;
$variable = $phpcsFile->findNext(T_VARIABLE, $ptr, $varEnd);
$varString = trim($phpcsFile->getTokensAsString($ptr, ($varEnd - $ptr)));
$data = array($varString);
if ($variable !== false) {
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($variable - 1), $ptr, true);
if ($prev !== false && $tokens[$prev]['type'] === 'T_DOLLAR') {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($variable + 1), $varEnd, true);
if ($next !== false
&& in_array($tokens[$next]['code'], array(T_OPEN_SQUARE_BRACKET, T_OBJECT_OPERATOR, T_DOUBLE_COLON), true) === true
) {
$phpcsFile->addError(
'Global with variable variables is not allowed since PHP 7.0. Found %s',
$variable,
'Found',
$data
);
$errorThrown = true;
} else {
$phpcsFile->addWarning(
'Global with anything other than bare variables is discouraged since PHP 7.0. Found %s',
$variable,
'NonBareVariableFound',
$data
);
$errorThrown = true;
}
}
}
if ($errorThrown === false) {
$dollar = $phpcsFile->findNext(T_DOLLAR, $ptr, $varEnd);
if ($dollar !== false) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($dollar + 1), $varEnd, true);
if ($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET) {
$phpcsFile->addWarning(
'Global with anything other than bare variables is discouraged since PHP 7.0. Found %s',
$dollar,
'NonBareVariableFound',
$data
);
}
}
}
// Move the stack pointer forward to the next variable for multi-variable statements.
if ($nextComma === false) {
break;
}
$ptr = $nextComma;
}
}
}
@@ -0,0 +1,424 @@
<?php
/**
* PHPCompatibility, an external standard for PHP_CodeSniffer.
*
* @package PHPCompatibility
* @copyright 2012-2018 PHPCompatibility Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCompatibility/PHPCompatibility
*/
namespace PHPCompatibility\Sniffs\Variables;
use PHPCompatibility\Sniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* Detect using $this in incompatible contexts.
*
* "Whilst $this is considered a special variable in PHP, it lacked proper checks
* to ensure it wasn't used as a variable name or reassigned. This has now been
* rectified to ensure that $this cannot be a user-defined variable, reassigned
* to a different value, or be globalised."
*
* This sniff only addresses those situations which did *not* throw an error prior
* to PHP 7.1, either at all or only in PHP 7.0.
* In other words, the following situation, while mentioned in the RFC, will NOT
* be sniffed for:
* - Using $this as static variable. (error _message_ change only).
*
* Also, the changes with relation to assigning $this dynamically can not be
* sniffed for reliably, so are not covered by this sniff.
* - Disable ability to re-assign $this indirectly through $$.
* - Disable ability to re-assign $this indirectly through reference.
* - Disable ability to re-assign $this indirectly through extract() and parse_str().
*
* Other changes not (yet) covered:
* - get_defined_vars() always doesn't show value of variable $this.
* - Always show true $this value in magic method __call().
* {@internal This could possibly be covered. Similar logic as "outside object context",
* but with function name check and supportsBelow('7.0').}}
*
* PHP version 7.1
*
* @link https://wiki.php.net/rfc/this_var
*
* @since 9.1.0
*/
class ForbiddenThisUseContextsSniff extends Sniff
{
/**
* OO scope tokens.
*
* Duplicate of Tokens::$ooScopeTokens array in PHPCS which was added in 3.1.0.
*
* @since 9.1.0
*
* @var array
*/
private $ooScopeTokens = array(
'T_CLASS' => T_CLASS,
'T_INTERFACE' => T_INTERFACE,
'T_TRAIT' => T_TRAIT,
);
/**
* Scopes to skip over when examining the contents of functions.
*
* @since 9.1.0
*
* @var array
*/
private $skipOverScopes = array(
'T_FUNCTION' => true,
'T_CLOSURE' => true,
);
/**
* Valid uses of $this in plain functions or methods outside object context.
*
* @since 9.1.0
*
* @var array
*/
private $validUseOutsideObject = array(
T_ISSET => true,
T_EMPTY => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 9.1.0
*
* @return array
*/
public function register()
{
if (defined('T_ANON_CLASS')) {
$this->ooScopeTokens['T_ANON_CLASS'] = T_ANON_CLASS;
}
$this->skipOverScopes += $this->ooScopeTokens;
return array(
T_FUNCTION,
T_CLOSURE,
T_GLOBAL,
T_CATCH,
T_FOREACH,
T_UNSET,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 9.1.0
*
* @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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.1') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
switch ($tokens[$stackPtr]['code']) {
case T_FUNCTION:
$this->isThisUsedAsParameter($phpcsFile, $stackPtr);
$this->isThisUsedOutsideObjectContext($phpcsFile, $stackPtr);
break;
case T_CLOSURE:
$this->isThisUsedAsParameter($phpcsFile, $stackPtr);
break;
case T_GLOBAL:
/*
* $this can no longer be imported using the `global` keyword.
* This worked in PHP 7.0, though in PHP 5.x, it would throw a
* fatal "Cannot re-assign $this" error.
*/
$endOfStatement = $phpcsFile->findNext(array(T_SEMICOLON, T_CLOSE_TAG), ($stackPtr + 1));
if ($endOfStatement === false) {
// No semi-colon - live coding.
return;
}
for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE || $tokens[$i]['content'] !== '$this') {
continue;
}
$phpcsFile->addError(
'"$this" can no longer be used with the "global" keyword since PHP 7.1.',
$i,
'Global'
);
}
break;
case T_CATCH:
/*
* $this can no longer be used as a catch variable.
*/
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
return;
}
$varPtr = $phpcsFile->findNext(
T_VARIABLE,
($tokens[$stackPtr]['parenthesis_opener'] + 1),
$tokens[$stackPtr]['parenthesis_closer']
);
if ($varPtr === false || $tokens[$varPtr]['content'] !== '$this') {
return;
}
$phpcsFile->addError(
'"$this" can no longer be used as a catch variable since PHP 7.1.',
$varPtr,
'Catch'
);
break;
case T_FOREACH:
/*
* $this can no longer be used as a foreach *value* variable.
* This worked in PHP 7.0, though in PHP 5.x, it would throw a
* fatal "Cannot re-assign $this" error.
*/
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
return;
}
$stopPtr = $phpcsFile->findPrevious(
array(T_AS, T_DOUBLE_ARROW),
($tokens[$stackPtr]['parenthesis_closer'] - 1),
$tokens[$stackPtr]['parenthesis_opener']
);
if ($stopPtr === false) {
return;
}
$valueVarPtr = $phpcsFile->findNext(
T_VARIABLE,
($stopPtr + 1),
$tokens[$stackPtr]['parenthesis_closer']
);
if ($valueVarPtr === false || $tokens[$valueVarPtr]['content'] !== '$this') {
return;
}
$afterThis = $phpcsFile->findNext(
Tokens::$emptyTokens,
($valueVarPtr + 1),
$tokens[$stackPtr]['parenthesis_closer'],
true
);
if ($afterThis !== false
&& ($tokens[$afterThis]['code'] === T_OBJECT_OPERATOR
|| $tokens[$afterThis]['code'] === T_DOUBLE_COLON)
) {
return;
}
$phpcsFile->addError(
'"$this" can no longer be used as value variable in a foreach control structure since PHP 7.1.',
$valueVarPtr,
'ForeachValueVar'
);
break;
case T_UNSET:
/*
* $this can no longer be unset.
*/
$openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($openParenthesis === false
|| $tokens[$openParenthesis]['code'] !== T_OPEN_PARENTHESIS
|| isset($tokens[$openParenthesis]['parenthesis_closer']) === false
) {
return;
}
for ($i = ($openParenthesis + 1); $i < $tokens[$openParenthesis]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE || $tokens[$i]['content'] !== '$this') {
continue;
}
$afterThis = $phpcsFile->findNext(
Tokens::$emptyTokens,
($i + 1),
$tokens[$openParenthesis]['parenthesis_closer'],
true
);
if ($afterThis !== false
&& ($tokens[$afterThis]['code'] === T_OBJECT_OPERATOR
|| $tokens[$afterThis]['code'] === T_DOUBLE_COLON
|| $tokens[$afterThis]['code'] === T_OPEN_SQUARE_BRACKET)
) {
$i = $afterThis;
continue;
}
$phpcsFile->addError(
'"$this" can no longer be unset since PHP 7.1.',
$i,
'Unset'
);
}
break;
}
}
/**
* Check if $this is used as a parameter in a function declaration.
*
* $this can no longer be used as a parameter in a *global* function.
* Use as a parameter in a method was already an error prior to PHP 7.1.
*
* @since 9.1.0
*
* @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
*/
protected function isThisUsedAsParameter(File $phpcsFile, $stackPtr)
{
if ($this->validDirectScope($phpcsFile, $stackPtr, $this->ooScopeTokens) !== false) {
return;
}
$params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
if (empty($params)) {
return;
}
$tokens = $phpcsFile->getTokens();
foreach ($params as $param) {
if ($param['name'] !== '$this') {
continue;
}
if ($tokens[$stackPtr]['code'] === T_FUNCTION) {
$phpcsFile->addError(
'"$this" can no longer be used as a parameter since PHP 7.1.',
$param['token'],
'FunctionParam'
);
} else {
$phpcsFile->addError(
'"$this" can no longer be used as a closure parameter since PHP 7.0.7.',
$param['token'],
'ClosureParam'
);
}
}
}
/**
* Check if $this is used in a plain function or method.
*
* Prior to PHP 7.1, this would result in an "undefined variable" notice
* and execution would continue with $this regarded as `null`.
* As of PHP 7.1, this throws an exception.
*
* Note: use within isset() and empty() to check object context is still allowed.
* Note: $this can still be used within a closure.
*
* @since 9.1.0
*
* @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
*/
protected function isThisUsedOutsideObjectContext(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
return;
}
if ($this->validDirectScope($phpcsFile, $stackPtr, $this->ooScopeTokens) !== false) {
$methodProps = $phpcsFile->getMethodProperties($stackPtr);
if ($methodProps['is_static'] === false) {
return;
} else {
$methodName = $phpcsFile->getDeclarationName($stackPtr);
if ($methodName === '__call') {
/*
* This is an exception.
* @link https://wiki.php.net/rfc/this_var#always_show_true_this_value_in_magic_method_call
*/
return;
}
}
}
for ($i = ($tokens[$stackPtr]['scope_opener'] + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) {
if (isset($this->skipOverScopes[$tokens[$i]['type']])) {
if (isset($tokens[$i]['scope_closer']) === false) {
// Live coding or parse error, will only lead to inaccurate results.
return;
}
// Skip over nested structures.
$i = $tokens[$i]['scope_closer'];
continue;
}
if ($tokens[$i]['code'] !== T_VARIABLE || $tokens[$i]['content'] !== '$this') {
continue;
}
if (isset($tokens[$i]['nested_parenthesis']) === true) {
$nestedParenthesis = $tokens[$i]['nested_parenthesis'];
$nestedOpenParenthesis = array_keys($nestedParenthesis);
$lastOpenParenthesis = array_pop($nestedOpenParenthesis);
$previousNonEmpty = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($lastOpenParenthesis - 1),
null,
true,
null,
true
);
if (isset($this->validUseOutsideObject[$tokens[$previousNonEmpty]['code']])) {
continue;
}
}
$phpcsFile->addError(
'"$this" can no longer be used in a plain function or method since PHP 7.1.',
$i,
'OutsideObjectContext'
);
}
}
}
@@ -0,0 +1,111 @@
<?php
/**
* \PHPCompatibility\Sniffs\Variables\NewUniformVariableSyntax.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
namespace PHPCompatibility\Sniffs\Variables;
use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Variables\NewUniformVariableSyntax.
*
* The interpretation of variable variables has changed in PHP 7.0.
*
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class NewUniformVariableSyntaxSniff extends Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_VARIABLE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('7.0') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
// Verify that the next token is a square open bracket. If not, bow out.
$nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($nextToken === false || $tokens[$nextToken]['code'] !== T_OPEN_SQUARE_BRACKET || isset($tokens[$nextToken]['bracket_closer']) === false) {
return;
}
// The previous non-empty token has to be a $, -> or ::.
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true, null, true);
if ($prevToken === false || in_array($tokens[$prevToken]['code'], array(T_DOLLAR, T_OBJECT_OPERATOR, T_DOUBLE_COLON), true) === false) {
return;
}
// For static object calls, it only applies when this is a function call.
if ($tokens[$prevToken]['code'] === T_DOUBLE_COLON) {
$hasBrackets = $tokens[$nextToken]['bracket_closer'];
while (($hasBrackets = $phpcsFile->findNext(Tokens::$emptyTokens, ($hasBrackets + 1), null, true, null, true)) !== false) {
if ($tokens[$hasBrackets]['code'] === T_OPEN_SQUARE_BRACKET) {
if (isset($tokens[$hasBrackets]['bracket_closer'])) {
$hasBrackets = $tokens[$hasBrackets]['bracket_closer'];
continue;
} else {
// Live coding.
return;
}
} elseif ($tokens[$hasBrackets]['code'] === T_OPEN_PARENTHESIS) {
// Caught!
break;
} else {
// Not a function call, so bow out.
return;
}
}
// Now let's also prevent false positives when used with self and static which still work fine.
$classToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), null, true, null, true);
if ($classToken !== false) {
if ($tokens[$classToken]['code'] === T_STATIC || $tokens[$classToken]['code'] === T_SELF) {
return;
} elseif ($tokens[$classToken]['code'] === T_STRING && $tokens[$classToken]['content'] === 'self') {
return;
}
}
}
$phpcsFile->addError(
'Indirect access to variables, properties and methods will be evaluated strictly in left-to-right order since PHP 7.0. Use curly braces to remove ambiguity.',
$stackPtr,
'Found'
);
}
}
@@ -0,0 +1,293 @@
<?php
/**
* \PHPCompatibility\Sniffs\Variables\RemovedPredefinedGlobalVariablesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
namespace PHPCompatibility\Sniffs\Variables;
use PHPCompatibility\AbstractRemovedFeatureSniff;
use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* \PHPCompatibility\Sniffs\Variables\RemovedPredefinedGlobalVariablesSniff.
*
* Discourages the use of removed global variables. Suggests alternative extensions if available
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
*/
class RemovedPredefinedGlobalVariablesSniff extends AbstractRemovedFeatureSniff
{
/**
* A list of removed global variables with their alternative, if any.
*
* The array lists : version number with false (deprecated) and true (removed).
* If's sufficient to list the first version where the variable was deprecated/removed.
*
* @var array(string|null)
*/
protected $removedGlobalVariables = array(
'HTTP_POST_VARS' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_POST',
),
'HTTP_GET_VARS' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_GET',
),
'HTTP_ENV_VARS' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_ENV',
),
'HTTP_SERVER_VARS' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_SERVER',
),
'HTTP_COOKIE_VARS' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_COOKIE',
),
'HTTP_SESSION_VARS' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_SESSION',
),
'HTTP_POST_FILES' => array(
'5.3' => false,
'5.4' => true,
'alternative' => '$_FILES',
),
'HTTP_RAW_POST_DATA' => array(
'5.6' => false,
'7.0' => true,
'alternative' => 'php://input',
),
'php_errormsg' => array(
'7.2' => false,
'alternative' => 'error_get_last()',
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_VARIABLE);
}
/**
* 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(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('5.3') === false) {
return;
}
$tokens = $phpcsFile->getTokens();
$varName = substr($tokens[$stackPtr]['content'], 1);
if (isset($this->removedGlobalVariables[$varName]) === false) {
return;
}
if ($this->isClassProperty($phpcsFile, $stackPtr) === true) {
// Ok, so this was a class property declaration, not our concern.
return;
}
// Check for static usage of class properties shadowing the removed global variables.
if ($this->inClassScope($phpcsFile, $stackPtr, false) === true) {
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true, null, true);
if ($prevToken !== false && $tokens[$prevToken]['code'] === T_DOUBLE_COLON) {
return;
}
}
// Do some additional checks for the $php_errormsg variable.
if ($varName === 'php_errormsg'
&& $this->isTargetPHPErrormsgVar($phpcsFile, $stackPtr, $tokens) === false
) {
return;
}
// Still here, so throw an error/warning.
$itemInfo = array(
'name' => $varName,
);
$this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
}
/**
* Get the relevant sub-array for a specific item from a multi-dimensional array.
*
* @param array $itemInfo Base information about the item.
*
* @return array Version and other information about the item.
*/
public function getItemArray(array $itemInfo)
{
return $this->removedGlobalVariables[$itemInfo['name']];
}
/**
* Get the error message template for this sniff.
*
* @return string
*/
protected function getErrorMsgTemplate()
{
return "Global variable '\$%s' is ";
}
/**
* Filter the error message before it's passed to PHPCS.
*
* @param string $error The error message which was created.
* @param array $itemInfo Base information about the item this error message applies to.
* @param array $errorInfo Detail information about an item this error message applies to.
*
* @return string
*/
protected function filterErrorMsg($error, array $itemInfo, array $errorInfo)
{
if ($itemInfo['name'] === 'php_errormsg') {
$error = str_replace('Global', 'The', $error);
}
return $error;
}
/**
* Run some additional checks for the `$php_errormsg` variable.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param array $tokens Token array of the current file.
*
* @return bool
*/
private function isTargetPHPErrormsgVar(File $phpcsFile, $stackPtr, array $tokens)
{
$scopeStart = 0;
/*
* If the variable is detected within the scope of a function/closure, limit the checking.
*/
$function = $phpcsFile->getCondition($stackPtr, T_CLOSURE);
if ($function === false) {
$function = $phpcsFile->getCondition($stackPtr, T_FUNCTION);
}
// It could also be a function param, which is not in the function scope.
if ($function === false && isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
$nestedParentheses = $tokens[$stackPtr]['nested_parenthesis'];
$parenthesisCloser = end($nestedParentheses);
if (isset($tokens[$parenthesisCloser]['parenthesis_owner'])
&& ($tokens[$tokens[$parenthesisCloser]['parenthesis_owner']]['code'] === T_FUNCTION
|| $tokens[$tokens[$parenthesisCloser]['parenthesis_owner']]['code'] === T_CLOSURE)
) {
$function = $tokens[$parenthesisCloser]['parenthesis_owner'];
}
}
if ($function !== false) {
$scopeStart = $tokens[$function]['scope_opener'];
}
/*
* Now, let's do some additional checks.
*/
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
// Is the variable being used as an array ?
if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === T_OPEN_SQUARE_BRACKET) {
// The PHP native variable is a string, so this is probably not it
// (except for array access to string, but why would you in this case ?).
return false;
}
// Is this a variable assignment ?
if ($nextNonEmpty !== false
&& isset(Tokens::$assignmentTokens[$tokens[$nextNonEmpty]['code']]) === true
) {
return false;
}
// Is this a function param shadowing the PHP native one ?
if ($function !== false) {
$parameters = PHPCSHelper::getMethodParameters($phpcsFile, $function);
if (is_array($parameters) === true && empty($parameters) === false) {
foreach ($parameters as $param) {
if ($param['name'] === '$php_errormsg') {
return false;
}
}
}
}
$skipPast = array(
'T_CLASS' => true,
'T_ANON_CLASS' => true,
'T_INTERFACE' => true,
'T_TRAIT' => true,
'T_FUNCTION' => true,
'T_CLOSURE' => true,
);
// Walk back and see if there is an assignment to the variable within the same scope.
for ($i = ($stackPtr - 1); $i >= $scopeStart; $i--) {
if ($tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
&& isset($tokens[$i]['scope_condition'])
&& isset($skipPast[$tokens[$tokens[$i]['scope_condition']]['type']])
) {
// Skip past functions, classes etc.
$i = $tokens[$i]['scope_condition'];
continue;
}
if ($tokens[$i]['code'] !== T_VARIABLE || $tokens[$i]['content'] !== '$php_errormsg') {
continue;
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
if ($nextNonEmpty !== false
&& isset(Tokens::$assignmentTokens[$tokens[$nextNonEmpty]['code']]) === true
) {
return false;
}
}
return true;
}
}