ForbiddenThisUseContextsSniff.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <?php
  2. /**
  3. * PHPCompatibility, an external standard for PHP_CodeSniffer.
  4. *
  5. * @package PHPCompatibility
  6. * @copyright 2012-2019 PHPCompatibility Contributors
  7. * @license https://opensource.org/licenses/LGPL-3.0 LGPL3
  8. * @link https://github.com/PHPCompatibility/PHPCompatibility
  9. */
  10. namespace PHPCompatibility\Sniffs\Variables;
  11. use PHPCompatibility\Sniff;
  12. use PHPCompatibility\PHPCSHelper;
  13. use PHP_CodeSniffer_File as File;
  14. use PHP_CodeSniffer_Tokens as Tokens;
  15. /**
  16. * Detect using `$this` in incompatible contexts.
  17. *
  18. * "Whilst `$this` is considered a special variable in PHP, it lacked proper checks
  19. * to ensure it wasn't used as a variable name or reassigned. This has now been
  20. * rectified to ensure that `$this` cannot be a user-defined variable, reassigned
  21. * to a different value, or be globalised."
  22. *
  23. * This sniff only addresses those situations which did *not* throw an error prior
  24. * to PHP 7.1, either at all or only in PHP 7.0.
  25. * In other words, the following situation, while mentioned in the RFC, will NOT
  26. * be sniffed for:
  27. * - Using $this as static variable. (error _message_ change only).
  28. *
  29. * Also, the changes with relation to assigning `$this` dynamically can not be
  30. * sniffed for reliably, so are not covered by this sniff.
  31. * - Disable ability to re-assign `$this` indirectly through `$$`.
  32. * - Disable ability to re-assign `$this` indirectly through reference.
  33. * - Disable ability to re-assign `$this` indirectly through `extract()` and `parse_str()`.
  34. *
  35. * Other changes not (yet) covered:
  36. * - `get_defined_vars()` always doesn't show value of variable `$this`.
  37. * - Always show true `$this` value in magic method `__call()`.
  38. * {@internal This could possibly be covered. Similar logic as "outside object context",
  39. * but with function name check and supportsBelow('7.0').}
  40. *
  41. * PHP version 7.1
  42. *
  43. * @link https://www.php.net/manual/en/migration71.other-changes.php#migration71.other-changes.inconsistency-fixes-to-this
  44. * @link https://wiki.php.net/rfc/this_var
  45. *
  46. * @since 9.1.0
  47. */
  48. class ForbiddenThisUseContextsSniff extends Sniff
  49. {
  50. /**
  51. * OO scope tokens.
  52. *
  53. * Duplicate of Tokens::$ooScopeTokens array in PHPCS which was added in 3.1.0.
  54. *
  55. * @since 9.1.0
  56. *
  57. * @var array
  58. */
  59. private $ooScopeTokens = array(
  60. 'T_CLASS' => \T_CLASS,
  61. 'T_INTERFACE' => \T_INTERFACE,
  62. 'T_TRAIT' => \T_TRAIT,
  63. );
  64. /**
  65. * Scopes to skip over when examining the contents of functions.
  66. *
  67. * @since 9.1.0
  68. *
  69. * @var array
  70. */
  71. private $skipOverScopes = array(
  72. 'T_FUNCTION' => true,
  73. 'T_CLOSURE' => true,
  74. );
  75. /**
  76. * Valid uses of $this in plain functions or methods outside object context.
  77. *
  78. * @since 9.1.0
  79. *
  80. * @var array
  81. */
  82. private $validUseOutsideObject = array(
  83. \T_ISSET => true,
  84. \T_EMPTY => true,
  85. );
  86. /**
  87. * Returns an array of tokens this test wants to listen for.
  88. *
  89. * @since 9.1.0
  90. *
  91. * @return array
  92. */
  93. public function register()
  94. {
  95. if (\defined('T_ANON_CLASS')) {
  96. $this->ooScopeTokens['T_ANON_CLASS'] = \T_ANON_CLASS;
  97. }
  98. $this->skipOverScopes += $this->ooScopeTokens;
  99. return array(
  100. \T_FUNCTION,
  101. \T_CLOSURE,
  102. \T_GLOBAL,
  103. \T_CATCH,
  104. \T_FOREACH,
  105. \T_UNSET,
  106. );
  107. }
  108. /**
  109. * Processes this test, when one of its tokens is encountered.
  110. *
  111. * @since 9.1.0
  112. *
  113. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  114. * @param int $stackPtr The position of the current token in
  115. * the stack passed in $tokens.
  116. *
  117. * @return void
  118. */
  119. public function process(File $phpcsFile, $stackPtr)
  120. {
  121. if ($this->supportsAbove('7.1') === false) {
  122. return;
  123. }
  124. $tokens = $phpcsFile->getTokens();
  125. switch ($tokens[$stackPtr]['code']) {
  126. case \T_FUNCTION:
  127. $this->isThisUsedAsParameter($phpcsFile, $stackPtr);
  128. $this->isThisUsedOutsideObjectContext($phpcsFile, $stackPtr);
  129. break;
  130. case \T_CLOSURE:
  131. $this->isThisUsedAsParameter($phpcsFile, $stackPtr);
  132. break;
  133. case \T_GLOBAL:
  134. /*
  135. * $this can no longer be imported using the `global` keyword.
  136. * This worked in PHP 7.0, though in PHP 5.x, it would throw a
  137. * fatal "Cannot re-assign $this" error.
  138. */
  139. $endOfStatement = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($stackPtr + 1));
  140. if ($endOfStatement === false) {
  141. // No semi-colon - live coding.
  142. return;
  143. }
  144. for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) {
  145. if ($tokens[$i]['code'] !== \T_VARIABLE || $tokens[$i]['content'] !== '$this') {
  146. continue;
  147. }
  148. $phpcsFile->addError(
  149. '"$this" can no longer be used with the "global" keyword since PHP 7.1.',
  150. $i,
  151. 'Global'
  152. );
  153. }
  154. break;
  155. case \T_CATCH:
  156. /*
  157. * $this can no longer be used as a catch variable.
  158. */
  159. if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
  160. return;
  161. }
  162. $varPtr = $phpcsFile->findNext(
  163. \T_VARIABLE,
  164. ($tokens[$stackPtr]['parenthesis_opener'] + 1),
  165. $tokens[$stackPtr]['parenthesis_closer']
  166. );
  167. if ($varPtr === false || $tokens[$varPtr]['content'] !== '$this') {
  168. return;
  169. }
  170. $phpcsFile->addError(
  171. '"$this" can no longer be used as a catch variable since PHP 7.1.',
  172. $varPtr,
  173. 'Catch'
  174. );
  175. break;
  176. case \T_FOREACH:
  177. /*
  178. * $this can no longer be used as a foreach *value* variable.
  179. * This worked in PHP 7.0, though in PHP 5.x, it would throw a
  180. * fatal "Cannot re-assign $this" error.
  181. */
  182. if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
  183. return;
  184. }
  185. $stopPtr = $phpcsFile->findPrevious(
  186. array(\T_AS, \T_DOUBLE_ARROW),
  187. ($tokens[$stackPtr]['parenthesis_closer'] - 1),
  188. $tokens[$stackPtr]['parenthesis_opener']
  189. );
  190. if ($stopPtr === false) {
  191. return;
  192. }
  193. $valueVarPtr = $phpcsFile->findNext(
  194. \T_VARIABLE,
  195. ($stopPtr + 1),
  196. $tokens[$stackPtr]['parenthesis_closer']
  197. );
  198. if ($valueVarPtr === false || $tokens[$valueVarPtr]['content'] !== '$this') {
  199. return;
  200. }
  201. $afterThis = $phpcsFile->findNext(
  202. Tokens::$emptyTokens,
  203. ($valueVarPtr + 1),
  204. $tokens[$stackPtr]['parenthesis_closer'],
  205. true
  206. );
  207. if ($afterThis !== false
  208. && ($tokens[$afterThis]['code'] === \T_OBJECT_OPERATOR
  209. || $tokens[$afterThis]['code'] === \T_DOUBLE_COLON)
  210. ) {
  211. return;
  212. }
  213. $phpcsFile->addError(
  214. '"$this" can no longer be used as value variable in a foreach control structure since PHP 7.1.',
  215. $valueVarPtr,
  216. 'ForeachValueVar'
  217. );
  218. break;
  219. case \T_UNSET:
  220. /*
  221. * $this can no longer be unset.
  222. */
  223. $openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
  224. if ($openParenthesis === false
  225. || $tokens[$openParenthesis]['code'] !== \T_OPEN_PARENTHESIS
  226. || isset($tokens[$openParenthesis]['parenthesis_closer']) === false
  227. ) {
  228. return;
  229. }
  230. for ($i = ($openParenthesis + 1); $i < $tokens[$openParenthesis]['parenthesis_closer']; $i++) {
  231. if ($tokens[$i]['code'] !== \T_VARIABLE || $tokens[$i]['content'] !== '$this') {
  232. continue;
  233. }
  234. $afterThis = $phpcsFile->findNext(
  235. Tokens::$emptyTokens,
  236. ($i + 1),
  237. $tokens[$openParenthesis]['parenthesis_closer'],
  238. true
  239. );
  240. if ($afterThis !== false
  241. && ($tokens[$afterThis]['code'] === \T_OBJECT_OPERATOR
  242. || $tokens[$afterThis]['code'] === \T_DOUBLE_COLON
  243. || $tokens[$afterThis]['code'] === \T_OPEN_SQUARE_BRACKET)
  244. ) {
  245. $i = $afterThis;
  246. continue;
  247. }
  248. $phpcsFile->addError(
  249. '"$this" can no longer be unset since PHP 7.1.',
  250. $i,
  251. 'Unset'
  252. );
  253. }
  254. break;
  255. }
  256. }
  257. /**
  258. * Check if $this is used as a parameter in a function declaration.
  259. *
  260. * $this can no longer be used as a parameter in a *global* function.
  261. * Use as a parameter in a method was already an error prior to PHP 7.1.
  262. *
  263. * @since 9.1.0
  264. *
  265. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  266. * @param int $stackPtr The position of the current token in
  267. * the stack passed in $tokens.
  268. *
  269. * @return void
  270. */
  271. protected function isThisUsedAsParameter(File $phpcsFile, $stackPtr)
  272. {
  273. if ($this->validDirectScope($phpcsFile, $stackPtr, $this->ooScopeTokens) !== false) {
  274. return;
  275. }
  276. $params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
  277. if (empty($params)) {
  278. return;
  279. }
  280. $tokens = $phpcsFile->getTokens();
  281. foreach ($params as $param) {
  282. if ($param['name'] !== '$this') {
  283. continue;
  284. }
  285. if ($tokens[$stackPtr]['code'] === \T_FUNCTION) {
  286. $phpcsFile->addError(
  287. '"$this" can no longer be used as a parameter since PHP 7.1.',
  288. $param['token'],
  289. 'FunctionParam'
  290. );
  291. } else {
  292. $phpcsFile->addError(
  293. '"$this" can no longer be used as a closure parameter since PHP 7.0.7.',
  294. $param['token'],
  295. 'ClosureParam'
  296. );
  297. }
  298. }
  299. }
  300. /**
  301. * Check if $this is used in a plain function or method.
  302. *
  303. * Prior to PHP 7.1, this would result in an "undefined variable" notice
  304. * and execution would continue with $this regarded as `null`.
  305. * As of PHP 7.1, this throws an exception.
  306. *
  307. * Note: use within isset() and empty() to check object context is still allowed.
  308. * Note: $this can still be used within a closure.
  309. *
  310. * @since 9.1.0
  311. *
  312. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  313. * @param int $stackPtr The position of the current token in
  314. * the stack passed in $tokens.
  315. *
  316. * @return void
  317. */
  318. protected function isThisUsedOutsideObjectContext(File $phpcsFile, $stackPtr)
  319. {
  320. $tokens = $phpcsFile->getTokens();
  321. if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
  322. return;
  323. }
  324. if ($this->validDirectScope($phpcsFile, $stackPtr, $this->ooScopeTokens) !== false) {
  325. $methodProps = $phpcsFile->getMethodProperties($stackPtr);
  326. if ($methodProps['is_static'] === false) {
  327. return;
  328. } else {
  329. $methodName = $phpcsFile->getDeclarationName($stackPtr);
  330. if ($methodName === '__call') {
  331. /*
  332. * This is an exception.
  333. * @link https://wiki.php.net/rfc/this_var#always_show_true_this_value_in_magic_method_call
  334. */
  335. return;
  336. }
  337. }
  338. }
  339. for ($i = ($tokens[$stackPtr]['scope_opener'] + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) {
  340. if (isset($this->skipOverScopes[$tokens[$i]['type']])) {
  341. if (isset($tokens[$i]['scope_closer']) === false) {
  342. // Live coding or parse error, will only lead to inaccurate results.
  343. return;
  344. }
  345. // Skip over nested structures.
  346. $i = $tokens[$i]['scope_closer'];
  347. continue;
  348. }
  349. if ($tokens[$i]['code'] !== \T_VARIABLE || $tokens[$i]['content'] !== '$this') {
  350. continue;
  351. }
  352. if (isset($tokens[$i]['nested_parenthesis']) === true) {
  353. $nestedParenthesis = $tokens[$i]['nested_parenthesis'];
  354. $nestedOpenParenthesis = array_keys($nestedParenthesis);
  355. $lastOpenParenthesis = array_pop($nestedOpenParenthesis);
  356. $previousNonEmpty = $phpcsFile->findPrevious(
  357. Tokens::$emptyTokens,
  358. ($lastOpenParenthesis - 1),
  359. null,
  360. true,
  361. null,
  362. true
  363. );
  364. if (isset($this->validUseOutsideObject[$tokens[$previousNonEmpty]['code']])) {
  365. continue;
  366. }
  367. }
  368. $phpcsFile->addError(
  369. '"$this" can no longer be used in a plain function or method since PHP 7.1.',
  370. $i,
  371. 'OutsideObjectContext'
  372. );
  373. }
  374. }
  375. }