*/ private $skipOverScopes = [ \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 = [ \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() { $this->skipOverScopes += BCTokens::ooScopeTokens(); return [ \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\Files\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 (ScannedCode::shouldRunOnOrAbove('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([\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( [\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 && isset(Collections::objectOperators()[$tokens[$afterThis]['code']]) === true ) { 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++) { // Ignore anything within square brackets (array access keys). if (isset($tokens[$i]['bracket_closer'])) { $i = $tokens[$i]['bracket_closer']; continue; } 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 && (isset(Collections::objectOperators()[$tokens[$afterThis]['code']]) === true || $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\Files\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 (Scopes::validDirectScope($phpcsFile, $stackPtr, BCTokens::ooScopeTokens()) !== false) { return; } $params = FunctionDeclarations::getParameters($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\Files\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 (Scopes::validDirectScope($phpcsFile, $stackPtr, BCTokens::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]['code']])) { 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' ); } } }