123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- <?php
- /**
- * PHPCompatibility, an external standard for PHP_CodeSniffer.
- *
- * @package PHPCompatibility
- * @copyright 2012-2019 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://www.php.net/manual/en/migration71.other-changes.php#migration71.other-changes.inconsistency-fixes-to-this
- * @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'
- );
- }
- }
- }
|