StringPositionSniff.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Sniffs\Strings;
  7. use PHP_CodeSniffer\Sniffs\Sniff;
  8. use PHP_CodeSniffer\Files\File;
  9. /**
  10. * Detects misusing of IS_IDENTICAL operators when `strpos` and `stripos` functions are used in a condition.
  11. *
  12. * Examples:
  13. * if (!strpos($haystack, $needle)) {}
  14. * if (stripos($haystack, $needle) == false) {}
  15. */
  16. class StringPositionSniff implements Sniff
  17. {
  18. /**
  19. * String representation of error.
  20. *
  21. * @var string
  22. */
  23. protected $errorMessage = 'Identical operator === is not used for testing the return value of %s function';
  24. /**
  25. * Error violation code.
  26. *
  27. * @var string
  28. */
  29. protected $errorCode = 'ImproperValueTesting';
  30. /**
  31. * Searched functions.
  32. *
  33. * @var array
  34. */
  35. protected $functions = [
  36. 'strpos',
  37. 'stripos',
  38. ];
  39. /**
  40. * All tokens from current file.
  41. *
  42. * @var array
  43. */
  44. protected $tokens = [];
  45. /**
  46. * PHP_CodeSniffer file.
  47. *
  48. * @var File
  49. */
  50. protected $file;
  51. /**
  52. * Left limit for search of identical operators.
  53. *
  54. * @var int
  55. */
  56. protected $leftLimit;
  57. /**
  58. * Right limit for search of identical operators.
  59. *
  60. * @var int
  61. */
  62. protected $rightLimit;
  63. /**
  64. * List of tokens which declares left bound of current scope.
  65. *
  66. * @var array
  67. */
  68. protected $leftRangeTokens = [
  69. T_IS_IDENTICAL,
  70. T_IS_NOT_IDENTICAL,
  71. T_OPEN_PARENTHESIS,
  72. T_BOOLEAN_AND,
  73. T_BOOLEAN_OR,
  74. ];
  75. /**
  76. * List of tokens which declares right bound of current scope.
  77. *
  78. * @var array
  79. */
  80. protected $rightRangeTokens = [
  81. T_IS_IDENTICAL,
  82. T_IS_NOT_IDENTICAL,
  83. T_CLOSE_PARENTHESIS,
  84. T_BOOLEAN_AND,
  85. T_BOOLEAN_OR,
  86. ];
  87. /**
  88. * List of tokens which declares identical operators.
  89. *
  90. * @var array
  91. */
  92. protected $identical = [
  93. T_IS_IDENTICAL,
  94. T_IS_NOT_IDENTICAL,
  95. ];
  96. /**
  97. * @inheritdoc
  98. */
  99. public function register()
  100. {
  101. return [T_IF, T_ELSEIF];
  102. }
  103. /**
  104. * @inheritdoc
  105. */
  106. public function process(File $phpcsFile, $stackPtr)
  107. {
  108. $this->tokens = $phpcsFile->getTokens();
  109. $this->file = $phpcsFile;
  110. $this->leftLimit = $open = $this->tokens[$stackPtr]['parenthesis_opener'];
  111. $this->rightLimit = $close = $this->tokens[$stackPtr]['parenthesis_closer'];
  112. for ($i = ($open + 1); $i < $close; $i++) {
  113. if (($this->tokens[$i]['code'] === T_STRING && in_array($this->tokens[$i]['content'], $this->functions))
  114. && (!$this->findIdentical($i - 1, $this->findFunctionParenthesisCloser($i) + 1))
  115. ) {
  116. $foundFunctionName = $this->tokens[$i]['content'];
  117. $phpcsFile->addError($this->errorMessage, $i, $this->errorCode, [$foundFunctionName]);
  118. }
  119. }
  120. }
  121. /**
  122. * Recursively finds identical operators in current scope.
  123. *
  124. * @param int $leftCurrentPosition
  125. * @param int $rightCurrentPosition
  126. * @return bool
  127. */
  128. protected function findIdentical($leftCurrentPosition, $rightCurrentPosition)
  129. {
  130. $leftBound = $this->file->findPrevious($this->leftRangeTokens, $leftCurrentPosition, $this->leftLimit - 1);
  131. $rightBound = $this->file->findNext($this->rightRangeTokens, $rightCurrentPosition, $this->rightLimit + 1);
  132. $leftToken = $this->tokens[$leftBound];
  133. $rightToken = $this->tokens[$rightBound];
  134. if ($leftToken['code'] === T_OPEN_PARENTHESIS && $rightToken['code'] === T_CLOSE_PARENTHESIS) {
  135. return $this->findIdentical($leftBound - 1, $rightBound + 1);
  136. } else {
  137. return (
  138. in_array($leftToken['code'], $this->identical) || in_array($rightToken['code'], $this->identical)
  139. ) ?: false;
  140. }
  141. }
  142. /**
  143. * Finds the position of close parenthesis of detected function.
  144. *
  145. * @param int $currentPosition
  146. * @return mixed
  147. */
  148. protected function findFunctionParenthesisCloser($currentPosition)
  149. {
  150. $nextOpenParenthesis = $this->file->findNext(T_OPEN_PARENTHESIS, $currentPosition, $this->rightLimit);
  151. return $nextOpenParenthesis ? $this->tokens[$nextOpenParenthesis]['parenthesis_closer'] : false;
  152. }
  153. }