ReservedFunctionNamesSniff.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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\FunctionNameRestrictions;
  11. use Generic_Sniffs_NamingConventions_CamelCapsFunctionNameSniff as PHPCS_CamelCapsFunctionNameSniff;
  12. use PHP_CodeSniffer_File as File;
  13. use PHP_CodeSniffer_Standards_AbstractScopeSniff as PHPCS_AbstractScopeSniff;
  14. use PHP_CodeSniffer_Tokens as Tokens;
  15. /**
  16. * All function and method names starting with double underscore are reserved by PHP.
  17. *
  18. * PHP version All
  19. *
  20. * {@internal Extends an upstream sniff to benefit from the properties contained therein.
  21. * The properties are lists of valid PHP magic function and method names, which
  22. * should be ignored for the purposes of this sniff.
  23. * As this sniff is not PHP version specific, we don't need access to the utility
  24. * methods in the PHPCompatibility\Sniff, so extending the upstream sniff is fine.
  25. * As the upstream sniff checks the same (and more, but we don't need the rest),
  26. * the logic in this sniff is largely the same as used upstream.
  27. * Extending the upstream sniff instead of including it via the ruleset, however,
  28. * prevents hard to debug issues of errors not being reported from the upstream sniff
  29. * if this library is used in combination with other rulesets.}
  30. *
  31. * @link https://www.php.net/manual/en/language.oop5.magic.php
  32. *
  33. * @since 8.2.0 This was previously, since 7.0.3, checked by the upstream sniff.
  34. * @since 9.3.2 The sniff will now ignore functions marked as `@deprecated` by design.
  35. */
  36. class ReservedFunctionNamesSniff extends PHPCS_CamelCapsFunctionNameSniff
  37. {
  38. /**
  39. * Overload the constructor to work round various PHPCS cross-version compatibility issues.
  40. *
  41. * @since 8.2.0
  42. */
  43. public function __construct()
  44. {
  45. $scopeTokens = array(\T_CLASS, \T_INTERFACE, \T_TRAIT);
  46. if (\defined('T_ANON_CLASS')) {
  47. $scopeTokens[] = \T_ANON_CLASS;
  48. }
  49. // Call the grand-parent constructor directly.
  50. PHPCS_AbstractScopeSniff::__construct($scopeTokens, array(\T_FUNCTION), true);
  51. // Make sure debuginfo is included in the array. Upstream only includes it since 2.5.1.
  52. $this->magicMethods['debuginfo'] = true;
  53. }
  54. /**
  55. * Processes the tokens within the scope.
  56. *
  57. * @since 8.2.0
  58. *
  59. * @param \PHP_CodeSniffer_File $phpcsFile The file being processed.
  60. * @param int $stackPtr The position where this token was
  61. * found.
  62. * @param int $currScope The position of the current scope.
  63. *
  64. * @return void
  65. */
  66. protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
  67. {
  68. $tokens = $phpcsFile->getTokens();
  69. /*
  70. * Determine if this is a function which needs to be examined.
  71. * The `processTokenWithinScope()` is called for each valid scope a method is in,
  72. * so for nested classes, we need to make sure we only examine the token for
  73. * the lowest level valid scope.
  74. */
  75. $conditions = $tokens[$stackPtr]['conditions'];
  76. end($conditions);
  77. $deepestScope = key($conditions);
  78. if ($deepestScope !== $currScope) {
  79. return;
  80. }
  81. if ($this->isFunctionDeprecated($phpcsFile, $stackPtr) === true) {
  82. /*
  83. * Deprecated functions don't have to comply with the naming conventions,
  84. * otherwise functions deprecated in favour of a function with a compliant
  85. * name would still trigger an error.
  86. */
  87. return;
  88. }
  89. $methodName = $phpcsFile->getDeclarationName($stackPtr);
  90. if ($methodName === null) {
  91. // Ignore closures.
  92. return;
  93. }
  94. // Is this a magic method. i.e., is prefixed with "__" ?
  95. if (preg_match('|^__[^_]|', $methodName) > 0) {
  96. $magicPart = strtolower(substr($methodName, 2));
  97. if (isset($this->magicMethods[$magicPart]) === false
  98. && isset($this->methodsDoubleUnderscore[$magicPart]) === false
  99. ) {
  100. $className = '[anonymous class]';
  101. $scopeNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($currScope + 1), null, true);
  102. if ($scopeNextNonEmpty !== false && $tokens[$scopeNextNonEmpty]['code'] === \T_STRING) {
  103. $className = $tokens[$scopeNextNonEmpty]['content'];
  104. }
  105. $phpcsFile->addWarning(
  106. 'Method name "%s" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.',
  107. $stackPtr,
  108. 'MethodDoubleUnderscore',
  109. array($className . '::' . $methodName)
  110. );
  111. }
  112. }
  113. }
  114. /**
  115. * Processes the tokens outside the scope.
  116. *
  117. * @since 8.2.0
  118. *
  119. * @param \PHP_CodeSniffer_File $phpcsFile The file being processed.
  120. * @param int $stackPtr The position where this token was
  121. * found.
  122. *
  123. * @return void
  124. */
  125. protected function processTokenOutsideScope(File $phpcsFile, $stackPtr)
  126. {
  127. if ($this->isFunctionDeprecated($phpcsFile, $stackPtr) === true) {
  128. /*
  129. * Deprecated functions don't have to comply with the naming conventions,
  130. * otherwise functions deprecated in favour of a function with a compliant
  131. * name would still trigger an error.
  132. */
  133. return;
  134. }
  135. $functionName = $phpcsFile->getDeclarationName($stackPtr);
  136. if ($functionName === null) {
  137. // Ignore closures.
  138. return;
  139. }
  140. // Is this a magic function. i.e., it is prefixed with "__".
  141. if (preg_match('|^__[^_]|', $functionName) > 0) {
  142. $magicPart = strtolower(substr($functionName, 2));
  143. if (isset($this->magicFunctions[$magicPart]) === false) {
  144. $phpcsFile->addWarning(
  145. 'Function name "%s" is discouraged; PHP has reserved all method names with a double underscore prefix for future use.',
  146. $stackPtr,
  147. 'FunctionDoubleUnderscore',
  148. array($functionName)
  149. );
  150. }
  151. }
  152. }
  153. /**
  154. * Check whether a function has been marked as deprecated via a @deprecated tag
  155. * in the function docblock.
  156. *
  157. * @since 9.3.2
  158. *
  159. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  160. * @param int $stackPtr The position of a T_FUNCTION
  161. * token in the stack.
  162. *
  163. * @return bool
  164. */
  165. private function isFunctionDeprecated(File $phpcsFile, $stackPtr)
  166. {
  167. $tokens = $phpcsFile->getTokens();
  168. $find = Tokens::$methodPrefixes;
  169. $find[] = \T_WHITESPACE;
  170. $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
  171. if ($tokens[$commentEnd]['code'] !== \T_DOC_COMMENT_CLOSE_TAG) {
  172. // Function doesn't have a doc comment or is using the wrong type of comment.
  173. return false;
  174. }
  175. $commentStart = $tokens[$commentEnd]['comment_opener'];
  176. foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
  177. if ($tokens[$tag]['content'] === '@deprecated') {
  178. return true;
  179. }
  180. }
  181. return false;
  182. }
  183. }