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