Sniff.php 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267
  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;
  11. use PHPCompatibility\PHPCSHelper;
  12. use PHP_CodeSniffer_Exception as PHPCS_Exception;
  13. use PHP_CodeSniffer_File as File;
  14. use PHP_CodeSniffer_Sniff as PHPCS_Sniff;
  15. use PHP_CodeSniffer_Tokens as Tokens;
  16. /**
  17. * Base class from which all PHPCompatibility sniffs extend.
  18. *
  19. * @since 5.6
  20. */
  21. abstract class Sniff implements PHPCS_Sniff
  22. {
  23. /**
  24. * Regex to match variables in a double quoted string.
  25. *
  26. * This matches plain variables, but also more complex variables, such
  27. * as $obj->prop, self::prop and $var[].
  28. *
  29. * @since 7.1.2
  30. *
  31. * @var string
  32. */
  33. const REGEX_COMPLEX_VARS = '`(?:(\{)?(?<!\\\\)\$)?(\{)?(?<!\\\\)\$(\{)?(?P<varname>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:->\$?(?P>varname)|\[[^\]]+\]|::\$?(?P>varname)|\([^\)]*\))*(?(3)\}|)(?(2)\}|)(?(1)\}|)`';
  34. /**
  35. * List of superglobals as an array of strings.
  36. *
  37. * Used by the ForbiddenParameterShadowSuperGlobals and ForbiddenClosureUseVariableNames sniffs.
  38. *
  39. * @since 7.0.0
  40. * @since 7.1.4 Moved from the `ForbiddenParameterShadowSuperGlobals` sniff to the base `Sniff` class.
  41. *
  42. * @var array
  43. */
  44. protected $superglobals = array(
  45. '$GLOBALS' => true,
  46. '$_SERVER' => true,
  47. '$_GET' => true,
  48. '$_POST' => true,
  49. '$_FILES' => true,
  50. '$_COOKIE' => true,
  51. '$_SESSION' => true,
  52. '$_REQUEST' => true,
  53. '$_ENV' => true,
  54. );
  55. /**
  56. * List of functions using hash algorithm as parameter (always the first parameter).
  57. *
  58. * Used by the new/removed hash algorithm sniffs.
  59. * Key is the function name, value is the 1-based parameter position in the function call.
  60. *
  61. * @since 5.5
  62. * @since 7.0.7 Moved from the `RemovedHashAlgorithms` sniff to the base `Sniff` class.
  63. *
  64. * @var array
  65. */
  66. protected $hashAlgoFunctions = array(
  67. 'hash_file' => 1,
  68. 'hash_hmac_file' => 1,
  69. 'hash_hmac' => 1,
  70. 'hash_init' => 1,
  71. 'hash_pbkdf2' => 1,
  72. 'hash' => 1,
  73. );
  74. /**
  75. * List of functions which take an ini directive as parameter (always the first parameter).
  76. *
  77. * Used by the new/removed ini directives sniffs.
  78. * Key is the function name, value is the 1-based parameter position in the function call.
  79. *
  80. * @since 7.1.0
  81. *
  82. * @var array
  83. */
  84. protected $iniFunctions = array(
  85. 'ini_get' => 1,
  86. 'ini_set' => 1,
  87. );
  88. /**
  89. * Get the testVersion configuration variable.
  90. *
  91. * The testVersion configuration variable may be in any of the following formats:
  92. * 1) Omitted/empty, in which case no version is specified. This effectively
  93. * disables all the checks for new PHP features provided by this standard.
  94. * 2) A single PHP version number, e.g. "5.4" in which case the standard checks that
  95. * the code will run on that version of PHP (no deprecated features or newer
  96. * features being used).
  97. * 3) A range, e.g. "5.0-5.5", in which case the standard checks the code will run
  98. * on all PHP versions in that range, and that it doesn't use any features that
  99. * were deprecated by the final version in the list, or which were not available
  100. * for the first version in the list.
  101. * We accept ranges where one of the components is missing, e.g. "-5.6" means
  102. * all versions up to PHP 5.6, and "7.0-" means all versions above PHP 7.0.
  103. * PHP version numbers should always be in Major.Minor format. Both "5", "5.3.2"
  104. * would be treated as invalid, and ignored.
  105. *
  106. * @since 7.0.0
  107. * @since 7.1.3 Now allows for partial ranges such as `5.2-`.
  108. *
  109. * @return array $arrTestVersions will hold an array containing min/max version
  110. * of PHP that we are checking against (see above). If only a
  111. * single version number is specified, then this is used as
  112. * both the min and max.
  113. *
  114. * @throws \PHP_CodeSniffer_Exception If testVersion is invalid.
  115. */
  116. private function getTestVersion()
  117. {
  118. static $arrTestVersions = array();
  119. $default = array(null, null);
  120. $testVersion = trim(PHPCSHelper::getConfigData('testVersion'));
  121. if (empty($testVersion) === false && isset($arrTestVersions[$testVersion]) === false) {
  122. $arrTestVersions[$testVersion] = $default;
  123. if (preg_match('`^\d+\.\d+$`', $testVersion)) {
  124. $arrTestVersions[$testVersion] = array($testVersion, $testVersion);
  125. return $arrTestVersions[$testVersion];
  126. }
  127. if (preg_match('`^(\d+\.\d+)?\s*-\s*(\d+\.\d+)?$`', $testVersion, $matches)) {
  128. if (empty($matches[1]) === false || empty($matches[2]) === false) {
  129. // If no lower-limit is set, we set the min version to 4.0.
  130. // Whilst development focuses on PHP 5 and above, we also accept
  131. // sniffs for PHP 4, so we include that as the minimum.
  132. // (It makes no sense to support PHP 3 as this was effectively a
  133. // different language).
  134. $min = empty($matches[1]) ? '4.0' : $matches[1];
  135. // If no upper-limit is set, we set the max version to 99.9.
  136. $max = empty($matches[2]) ? '99.9' : $matches[2];
  137. if (version_compare($min, $max, '>')) {
  138. trigger_error(
  139. "Invalid range in testVersion setting: '" . $testVersion . "'",
  140. \E_USER_WARNING
  141. );
  142. return $default;
  143. } else {
  144. $arrTestVersions[$testVersion] = array($min, $max);
  145. return $arrTestVersions[$testVersion];
  146. }
  147. }
  148. }
  149. trigger_error(
  150. "Invalid testVersion setting: '" . $testVersion . "'",
  151. \E_USER_WARNING
  152. );
  153. return $default;
  154. }
  155. if (isset($arrTestVersions[$testVersion])) {
  156. return $arrTestVersions[$testVersion];
  157. }
  158. return $default;
  159. }
  160. /**
  161. * Check whether a specific PHP version is equal to or higher than the maximum
  162. * supported PHP version as provided by the user in `testVersion`.
  163. *
  164. * Should be used when sniffing for *old* PHP features (deprecated/removed).
  165. *
  166. * @since 5.6
  167. *
  168. * @param string $phpVersion A PHP version number in 'major.minor' format.
  169. *
  170. * @return bool True if testVersion has not been provided or if the PHP version
  171. * is equal to or higher than the highest supported PHP version
  172. * in testVersion. False otherwise.
  173. */
  174. public function supportsAbove($phpVersion)
  175. {
  176. $testVersion = $this->getTestVersion();
  177. $testVersion = $testVersion[1];
  178. if (\is_null($testVersion)
  179. || version_compare($testVersion, $phpVersion) >= 0
  180. ) {
  181. return true;
  182. } else {
  183. return false;
  184. }
  185. }
  186. /**
  187. * Check whether a specific PHP version is equal to or lower than the minimum
  188. * supported PHP version as provided by the user in `testVersion`.
  189. *
  190. * Should be used when sniffing for *new* PHP features.
  191. *
  192. * @since 5.6
  193. *
  194. * @param string $phpVersion A PHP version number in 'major.minor' format.
  195. *
  196. * @return bool True if the PHP version is equal to or lower than the lowest
  197. * supported PHP version in testVersion.
  198. * False otherwise or if no testVersion is provided.
  199. */
  200. public function supportsBelow($phpVersion)
  201. {
  202. $testVersion = $this->getTestVersion();
  203. $testVersion = $testVersion[0];
  204. if (\is_null($testVersion) === false
  205. && version_compare($testVersion, $phpVersion) <= 0
  206. ) {
  207. return true;
  208. } else {
  209. return false;
  210. }
  211. }
  212. /**
  213. * Add a PHPCS message to the output stack as either a warning or an error.
  214. *
  215. * @since 7.1.0
  216. *
  217. * @param \PHP_CodeSniffer_File $phpcsFile The file the message applies to.
  218. * @param string $message The message.
  219. * @param int $stackPtr The position of the token
  220. * the message relates to.
  221. * @param bool $isError Whether to report the message as an
  222. * 'error' or 'warning'.
  223. * Defaults to true (error).
  224. * @param string $code The error code for the message.
  225. * Defaults to 'Found'.
  226. * @param array $data Optional input for the data replacements.
  227. *
  228. * @return void
  229. */
  230. public function addMessage(File $phpcsFile, $message, $stackPtr, $isError, $code = 'Found', $data = array())
  231. {
  232. if ($isError === true) {
  233. $phpcsFile->addError($message, $stackPtr, $code, $data);
  234. } else {
  235. $phpcsFile->addWarning($message, $stackPtr, $code, $data);
  236. }
  237. }
  238. /**
  239. * Convert an arbitrary string to an alphanumeric string with underscores.
  240. *
  241. * Pre-empt issues with arbitrary strings being used as error codes in XML and PHP.
  242. *
  243. * @since 7.1.0
  244. *
  245. * @param string $baseString Arbitrary string.
  246. *
  247. * @return string
  248. */
  249. public function stringToErrorCode($baseString)
  250. {
  251. return preg_replace('`[^a-z0-9_]`i', '_', strtolower($baseString));
  252. }
  253. /**
  254. * Strip quotes surrounding an arbitrary string.
  255. *
  256. * Intended for use with the contents of a T_CONSTANT_ENCAPSED_STRING / T_DOUBLE_QUOTED_STRING.
  257. *
  258. * @since 7.0.6
  259. *
  260. * @param string $string The raw string.
  261. *
  262. * @return string String without quotes around it.
  263. */
  264. public function stripQuotes($string)
  265. {
  266. return preg_replace('`^([\'"])(.*)\1$`Ds', '$2', $string);
  267. }
  268. /**
  269. * Strip variables from an arbitrary double quoted string.
  270. *
  271. * Intended for use with the contents of a T_DOUBLE_QUOTED_STRING.
  272. *
  273. * @since 7.1.2
  274. *
  275. * @param string $string The raw string.
  276. *
  277. * @return string String without variables in it.
  278. */
  279. public function stripVariables($string)
  280. {
  281. if (strpos($string, '$') === false) {
  282. return $string;
  283. }
  284. return preg_replace(self::REGEX_COMPLEX_VARS, '', $string);
  285. }
  286. /**
  287. * Make all top level array keys in an array lowercase.
  288. *
  289. * @since 7.1.0
  290. *
  291. * @param array $array Initial array.
  292. *
  293. * @return array Same array, but with all lowercase top level keys.
  294. */
  295. public function arrayKeysToLowercase($array)
  296. {
  297. return array_change_key_case($array, \CASE_LOWER);
  298. }
  299. /**
  300. * Checks if a function call has parameters.
  301. *
  302. * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
  303. * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
  304. *
  305. * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer, it
  306. * will detect whether the array has values or is empty.
  307. *
  308. * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/120
  309. * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/152
  310. *
  311. * @since 7.0.3
  312. *
  313. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  314. * @param int $stackPtr The position of the function call token.
  315. *
  316. * @return bool
  317. */
  318. public function doesFunctionCallHaveParameters(File $phpcsFile, $stackPtr)
  319. {
  320. $tokens = $phpcsFile->getTokens();
  321. // Check for the existence of the token.
  322. if (isset($tokens[$stackPtr]) === false) {
  323. return false;
  324. }
  325. // Is this one of the tokens this function handles ?
  326. if (\in_array($tokens[$stackPtr]['code'], array(\T_STRING, \T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_VARIABLE), true) === false) {
  327. return false;
  328. }
  329. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
  330. // Deal with short array syntax.
  331. if ($tokens[$stackPtr]['code'] === \T_OPEN_SHORT_ARRAY) {
  332. if (isset($tokens[$stackPtr]['bracket_closer']) === false) {
  333. return false;
  334. }
  335. if ($nextNonEmpty === $tokens[$stackPtr]['bracket_closer']) {
  336. // No parameters.
  337. return false;
  338. } else {
  339. return true;
  340. }
  341. }
  342. // Deal with function calls & long arrays.
  343. // Next non-empty token should be the open parenthesis.
  344. if ($nextNonEmpty === false && $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) {
  345. return false;
  346. }
  347. if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) {
  348. return false;
  349. }
  350. $closeParenthesis = $tokens[$nextNonEmpty]['parenthesis_closer'];
  351. $nextNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $nextNonEmpty + 1, $closeParenthesis + 1, true);
  352. if ($nextNextNonEmpty === $closeParenthesis) {
  353. // No parameters.
  354. return false;
  355. }
  356. return true;
  357. }
  358. /**
  359. * Count the number of parameters a function call has been passed.
  360. *
  361. * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
  362. * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
  363. *
  364. * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer,
  365. * it will return the number of values in the array.
  366. *
  367. * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/111
  368. * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/114
  369. * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/151
  370. *
  371. * @since 7.0.3
  372. *
  373. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  374. * @param int $stackPtr The position of the function call token.
  375. *
  376. * @return int
  377. */
  378. public function getFunctionCallParameterCount(File $phpcsFile, $stackPtr)
  379. {
  380. if ($this->doesFunctionCallHaveParameters($phpcsFile, $stackPtr) === false) {
  381. return 0;
  382. }
  383. return \count($this->getFunctionCallParameters($phpcsFile, $stackPtr));
  384. }
  385. /**
  386. * Get information on all parameters passed to a function call.
  387. *
  388. * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
  389. * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
  390. *
  391. * Will return an multi-dimentional array with the start token pointer, end token
  392. * pointer and raw parameter value for all parameters. Index will be 1-based.
  393. * If no parameters are found, will return an empty array.
  394. *
  395. * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer,
  396. * it will tokenize the values / key/value pairs contained in the array call.
  397. *
  398. * @since 7.0.5 Split off from the `getFunctionCallParameterCount()` method.
  399. *
  400. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  401. * @param int $stackPtr The position of the function call token.
  402. *
  403. * @return array
  404. */
  405. public function getFunctionCallParameters(File $phpcsFile, $stackPtr)
  406. {
  407. if ($this->doesFunctionCallHaveParameters($phpcsFile, $stackPtr) === false) {
  408. return array();
  409. }
  410. // Ok, we know we have a T_STRING, T_VARIABLE, T_ARRAY or T_OPEN_SHORT_ARRAY with parameters
  411. // and valid open & close brackets/parenthesis.
  412. $tokens = $phpcsFile->getTokens();
  413. // Mark the beginning and end tokens.
  414. if ($tokens[$stackPtr]['code'] === \T_OPEN_SHORT_ARRAY) {
  415. $opener = $stackPtr;
  416. $closer = $tokens[$stackPtr]['bracket_closer'];
  417. $nestedParenthesisCount = 0;
  418. } else {
  419. $opener = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
  420. $closer = $tokens[$opener]['parenthesis_closer'];
  421. $nestedParenthesisCount = 1;
  422. }
  423. // Which nesting level is the one we are interested in ?
  424. if (isset($tokens[$opener]['nested_parenthesis'])) {
  425. $nestedParenthesisCount += \count($tokens[$opener]['nested_parenthesis']);
  426. }
  427. $parameters = array();
  428. $nextComma = $opener;
  429. $paramStart = $opener + 1;
  430. $cnt = 1;
  431. while (($nextComma = $phpcsFile->findNext(array(\T_COMMA, $tokens[$closer]['code'], \T_OPEN_SHORT_ARRAY, \T_CLOSURE), $nextComma + 1, $closer + 1)) !== false) {
  432. // Ignore anything within short array definition brackets.
  433. if ($tokens[$nextComma]['type'] === 'T_OPEN_SHORT_ARRAY'
  434. && (isset($tokens[$nextComma]['bracket_opener'])
  435. && $tokens[$nextComma]['bracket_opener'] === $nextComma)
  436. && isset($tokens[$nextComma]['bracket_closer'])
  437. ) {
  438. // Skip forward to the end of the short array definition.
  439. $nextComma = $tokens[$nextComma]['bracket_closer'];
  440. continue;
  441. }
  442. // Skip past closures passed as function parameters.
  443. if ($tokens[$nextComma]['type'] === 'T_CLOSURE'
  444. && (isset($tokens[$nextComma]['scope_condition'])
  445. && $tokens[$nextComma]['scope_condition'] === $nextComma)
  446. && isset($tokens[$nextComma]['scope_closer'])
  447. ) {
  448. // Skip forward to the end of the closure declaration.
  449. $nextComma = $tokens[$nextComma]['scope_closer'];
  450. continue;
  451. }
  452. // Ignore comma's at a lower nesting level.
  453. if ($tokens[$nextComma]['type'] === 'T_COMMA'
  454. && isset($tokens[$nextComma]['nested_parenthesis'])
  455. && \count($tokens[$nextComma]['nested_parenthesis']) !== $nestedParenthesisCount
  456. ) {
  457. continue;
  458. }
  459. // Ignore closing parenthesis/bracket if not 'ours'.
  460. if ($tokens[$nextComma]['type'] === $tokens[$closer]['type'] && $nextComma !== $closer) {
  461. continue;
  462. }
  463. // Ok, we've reached the end of the parameter.
  464. $parameters[$cnt]['start'] = $paramStart;
  465. $parameters[$cnt]['end'] = $nextComma - 1;
  466. $parameters[$cnt]['raw'] = trim($phpcsFile->getTokensAsString($paramStart, ($nextComma - $paramStart)));
  467. /*
  468. * Check if there are more tokens before the closing parenthesis.
  469. * Prevents code like the following from setting a third parameter:
  470. * `functionCall( $param1, $param2, );`.
  471. */
  472. $hasNextParam = $phpcsFile->findNext(Tokens::$emptyTokens, $nextComma + 1, $closer, true, null, true);
  473. if ($hasNextParam === false) {
  474. break;
  475. }
  476. // Prepare for the next parameter.
  477. $paramStart = $nextComma + 1;
  478. $cnt++;
  479. }
  480. return $parameters;
  481. }
  482. /**
  483. * Get information on a specific parameter passed to a function call.
  484. *
  485. * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
  486. * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
  487. *
  488. * Will return a array with the start token pointer, end token pointer and the raw value
  489. * of the parameter at a specific offset.
  490. * If the specified parameter is not found, will return false.
  491. *
  492. * @since 7.0.5
  493. *
  494. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  495. * @param int $stackPtr The position of the function call token.
  496. * @param int $paramOffset The 1-based index position of the parameter to retrieve.
  497. *
  498. * @return array|false
  499. */
  500. public function getFunctionCallParameter(File $phpcsFile, $stackPtr, $paramOffset)
  501. {
  502. $parameters = $this->getFunctionCallParameters($phpcsFile, $stackPtr);
  503. if (isset($parameters[$paramOffset]) === false) {
  504. return false;
  505. } else {
  506. return $parameters[$paramOffset];
  507. }
  508. }
  509. /**
  510. * Verify whether a token is within a scoped condition.
  511. *
  512. * If the optional $validScopes parameter has been passed, the function
  513. * will check that the token has at least one condition which is of a
  514. * type defined in $validScopes.
  515. *
  516. * @since 7.0.5 Largely split off from the `inClassScope()` method.
  517. *
  518. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  519. * @param int $stackPtr The position of the token.
  520. * @param array|int $validScopes Optional. Array of valid scopes
  521. * or int value of a valid scope.
  522. * Pass the T_.. constant(s) for the
  523. * desired scope to this parameter.
  524. *
  525. * @return bool Without the optional $scopeTypes: True if within a scope, false otherwise.
  526. * If the $scopeTypes are set: True if *one* of the conditions is a
  527. * valid scope, false otherwise.
  528. */
  529. public function tokenHasScope(File $phpcsFile, $stackPtr, $validScopes = null)
  530. {
  531. $tokens = $phpcsFile->getTokens();
  532. // Check for the existence of the token.
  533. if (isset($tokens[$stackPtr]) === false) {
  534. return false;
  535. }
  536. // No conditions = no scope.
  537. if (empty($tokens[$stackPtr]['conditions'])) {
  538. return false;
  539. }
  540. // Ok, there are conditions, do we have to check for specific ones ?
  541. if (isset($validScopes) === false) {
  542. return true;
  543. }
  544. return $phpcsFile->hasCondition($stackPtr, $validScopes);
  545. }
  546. /**
  547. * Verify whether a token is within a class scope.
  548. *
  549. * @since 7.0.3
  550. *
  551. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  552. * @param int $stackPtr The position of the token.
  553. * @param bool $strict Whether to strictly check for the T_CLASS
  554. * scope or also accept interfaces and traits
  555. * as scope.
  556. *
  557. * @return bool True if within class scope, false otherwise.
  558. */
  559. public function inClassScope(File $phpcsFile, $stackPtr, $strict = true)
  560. {
  561. $validScopes = array(\T_CLASS);
  562. if (\defined('T_ANON_CLASS') === true) {
  563. $validScopes[] = \T_ANON_CLASS;
  564. }
  565. if ($strict === false) {
  566. $validScopes[] = \T_INTERFACE;
  567. $validScopes[] = \T_TRAIT;
  568. }
  569. return $phpcsFile->hasCondition($stackPtr, $validScopes);
  570. }
  571. /**
  572. * Returns the fully qualified class name for a new class instantiation.
  573. *
  574. * Returns an empty string if the class name could not be reliably inferred.
  575. *
  576. * @since 7.0.3
  577. *
  578. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  579. * @param int $stackPtr The position of a T_NEW token.
  580. *
  581. * @return string
  582. */
  583. public function getFQClassNameFromNewToken(File $phpcsFile, $stackPtr)
  584. {
  585. $tokens = $phpcsFile->getTokens();
  586. // Check for the existence of the token.
  587. if (isset($tokens[$stackPtr]) === false) {
  588. return '';
  589. }
  590. if ($tokens[$stackPtr]['code'] !== \T_NEW) {
  591. return '';
  592. }
  593. $start = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
  594. if ($start === false) {
  595. return '';
  596. }
  597. // Bow out if the next token is a variable as we don't know where it was defined.
  598. if ($tokens[$start]['code'] === \T_VARIABLE) {
  599. return '';
  600. }
  601. // Bow out if the next token is the class keyword.
  602. if ($tokens[$start]['type'] === 'T_ANON_CLASS' || $tokens[$start]['code'] === \T_CLASS) {
  603. return '';
  604. }
  605. $find = array(
  606. \T_NS_SEPARATOR,
  607. \T_STRING,
  608. \T_NAMESPACE,
  609. \T_WHITESPACE,
  610. );
  611. $end = $phpcsFile->findNext($find, ($start + 1), null, true, null, true);
  612. $className = $phpcsFile->getTokensAsString($start, ($end - $start));
  613. $className = trim($className);
  614. return $this->getFQName($phpcsFile, $stackPtr, $className);
  615. }
  616. /**
  617. * Returns the fully qualified name of the class that the specified class extends.
  618. *
  619. * Returns an empty string if the class does not extend another class or if
  620. * the class name could not be reliably inferred.
  621. *
  622. * @since 7.0.3
  623. *
  624. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  625. * @param int $stackPtr The position of a T_CLASS token.
  626. *
  627. * @return string
  628. */
  629. public function getFQExtendedClassName(File $phpcsFile, $stackPtr)
  630. {
  631. $tokens = $phpcsFile->getTokens();
  632. // Check for the existence of the token.
  633. if (isset($tokens[$stackPtr]) === false) {
  634. return '';
  635. }
  636. if ($tokens[$stackPtr]['code'] !== \T_CLASS
  637. && $tokens[$stackPtr]['type'] !== 'T_ANON_CLASS'
  638. && $tokens[$stackPtr]['type'] !== 'T_INTERFACE'
  639. ) {
  640. return '';
  641. }
  642. $extends = PHPCSHelper::findExtendedClassName($phpcsFile, $stackPtr);
  643. if (empty($extends) || \is_string($extends) === false) {
  644. return '';
  645. }
  646. return $this->getFQName($phpcsFile, $stackPtr, $extends);
  647. }
  648. /**
  649. * Returns the class name for the static usage of a class.
  650. * This can be a call to a method, the use of a property or constant.
  651. *
  652. * Returns an empty string if the class name could not be reliably inferred.
  653. *
  654. * @since 7.0.3
  655. *
  656. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  657. * @param int $stackPtr The position of a T_NEW token.
  658. *
  659. * @return string
  660. */
  661. public function getFQClassNameFromDoubleColonToken(File $phpcsFile, $stackPtr)
  662. {
  663. $tokens = $phpcsFile->getTokens();
  664. // Check for the existence of the token.
  665. if (isset($tokens[$stackPtr]) === false) {
  666. return '';
  667. }
  668. if ($tokens[$stackPtr]['code'] !== \T_DOUBLE_COLON) {
  669. return '';
  670. }
  671. // Nothing to do if previous token is a variable as we don't know where it was defined.
  672. if ($tokens[$stackPtr - 1]['code'] === \T_VARIABLE) {
  673. return '';
  674. }
  675. // Nothing to do if 'parent' or 'static' as we don't know how far the class tree extends.
  676. if (\in_array($tokens[$stackPtr - 1]['code'], array(\T_PARENT, \T_STATIC), true)) {
  677. return '';
  678. }
  679. // Get the classname from the class declaration if self is used.
  680. if ($tokens[$stackPtr - 1]['code'] === \T_SELF) {
  681. $classDeclarationPtr = $phpcsFile->findPrevious(\T_CLASS, $stackPtr - 1);
  682. if ($classDeclarationPtr === false) {
  683. return '';
  684. }
  685. $className = $phpcsFile->getDeclarationName($classDeclarationPtr);
  686. return $this->getFQName($phpcsFile, $classDeclarationPtr, $className);
  687. }
  688. $find = array(
  689. \T_NS_SEPARATOR,
  690. \T_STRING,
  691. \T_NAMESPACE,
  692. \T_WHITESPACE,
  693. );
  694. $start = $phpcsFile->findPrevious($find, $stackPtr - 1, null, true, null, true);
  695. if ($start === false || isset($tokens[($start + 1)]) === false) {
  696. return '';
  697. }
  698. $start = ($start + 1);
  699. $className = $phpcsFile->getTokensAsString($start, ($stackPtr - $start));
  700. $className = trim($className);
  701. return $this->getFQName($phpcsFile, $stackPtr, $className);
  702. }
  703. /**
  704. * Get the Fully Qualified name for a class/function/constant etc.
  705. *
  706. * Checks if a class/function/constant name is already fully qualified and
  707. * if not, enrich it with the relevant namespace information.
  708. *
  709. * @since 7.0.3
  710. *
  711. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  712. * @param int $stackPtr The position of the token.
  713. * @param string $name The class / function / constant name.
  714. *
  715. * @return string
  716. */
  717. public function getFQName(File $phpcsFile, $stackPtr, $name)
  718. {
  719. if (strpos($name, '\\') === 0) {
  720. // Already fully qualified.
  721. return $name;
  722. }
  723. // Remove the namespace keyword if used.
  724. if (strpos($name, 'namespace\\') === 0) {
  725. $name = substr($name, 10);
  726. }
  727. $namespace = $this->determineNamespace($phpcsFile, $stackPtr);
  728. if ($namespace === '') {
  729. return '\\' . $name;
  730. } else {
  731. return '\\' . $namespace . '\\' . $name;
  732. }
  733. }
  734. /**
  735. * Is the class/function/constant name namespaced or global ?
  736. *
  737. * @since 7.0.3
  738. *
  739. * @param string $FQName Fully Qualified name of a class, function etc.
  740. * I.e. should always start with a `\`.
  741. *
  742. * @return bool True if namespaced, false if global.
  743. *
  744. * @throws \PHP_CodeSniffer_Exception If the name in the passed parameter
  745. * is not fully qualified.
  746. */
  747. public function isNamespaced($FQName)
  748. {
  749. if (strpos($FQName, '\\') !== 0) {
  750. throw new PHPCS_Exception('$FQName must be a fully qualified name');
  751. }
  752. return (strpos(substr($FQName, 1), '\\') !== false);
  753. }
  754. /**
  755. * Determine the namespace name an arbitrary token lives in.
  756. *
  757. * @since 7.0.3
  758. *
  759. * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
  760. * @param int $stackPtr The token position for which to determine the namespace.
  761. *
  762. * @return string Namespace name or empty string if it couldn't be determined or no namespace applies.
  763. */
  764. public function determineNamespace(File $phpcsFile, $stackPtr)
  765. {
  766. $tokens = $phpcsFile->getTokens();
  767. // Check for the existence of the token.
  768. if (isset($tokens[$stackPtr]) === false) {
  769. return '';
  770. }
  771. // Check for scoped namespace {}.
  772. if (empty($tokens[$stackPtr]['conditions']) === false) {
  773. $namespacePtr = $phpcsFile->getCondition($stackPtr, \T_NAMESPACE);
  774. if ($namespacePtr !== false) {
  775. $namespace = $this->getDeclaredNamespaceName($phpcsFile, $namespacePtr);
  776. if ($namespace !== false) {
  777. return $namespace;
  778. }
  779. // We are in a scoped namespace, but couldn't determine the name. Searching for a global namespace is futile.
  780. return '';
  781. }
  782. }
  783. /*
  784. * Not in a scoped namespace, so let's see if we can find a non-scoped namespace instead.
  785. * Keeping in mind that:
  786. * - there can be multiple non-scoped namespaces in a file (bad practice, but it happens).
  787. * - the namespace keyword can also be used as part of a function/method call and such.
  788. * - that a non-named namespace resolves to the global namespace.
  789. */
  790. $previousNSToken = $stackPtr;
  791. $namespace = false;
  792. do {
  793. $previousNSToken = $phpcsFile->findPrevious(\T_NAMESPACE, ($previousNSToken - 1));
  794. // Stop if we encounter a scoped namespace declaration as we already know we're not in one.
  795. if (empty($tokens[$previousNSToken]['scope_condition']) === false && $tokens[$previousNSToken]['scope_condition'] === $previousNSToken) {
  796. break;
  797. }
  798. $namespace = $this->getDeclaredNamespaceName($phpcsFile, $previousNSToken);
  799. } while ($namespace === false && $previousNSToken !== false);
  800. // If we still haven't got a namespace, return an empty string.
  801. if ($namespace === false) {
  802. return '';
  803. } else {
  804. return $namespace;
  805. }
  806. }
  807. /**
  808. * Get the complete namespace name for a namespace declaration.
  809. *
  810. * For hierarchical namespaces, the name will be composed of several tokens,
  811. * i.e. MyProject\Sub\Level which will be returned together as one string.
  812. *
  813. * @since 7.0.3
  814. *
  815. * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
  816. * @param int|bool $stackPtr The position of a T_NAMESPACE token.
  817. *
  818. * @return string|false Namespace name or false if not a namespace declaration.
  819. * Namespace name can be an empty string for global namespace declaration.
  820. */
  821. public function getDeclaredNamespaceName(File $phpcsFile, $stackPtr)
  822. {
  823. $tokens = $phpcsFile->getTokens();
  824. // Check for the existence of the token.
  825. if ($stackPtr === false || isset($tokens[$stackPtr]) === false) {
  826. return false;
  827. }
  828. if ($tokens[$stackPtr]['code'] !== \T_NAMESPACE) {
  829. return false;
  830. }
  831. if ($tokens[($stackPtr + 1)]['code'] === \T_NS_SEPARATOR) {
  832. // Not a namespace declaration, but use of, i.e. `namespace\someFunction();`.
  833. return false;
  834. }
  835. $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
  836. if ($tokens[$nextToken]['code'] === \T_OPEN_CURLY_BRACKET) {
  837. /*
  838. * Declaration for global namespace when using multiple namespaces in a file.
  839. * I.e.: `namespace {}`.
  840. */
  841. return '';
  842. }
  843. // Ok, this should be a namespace declaration, so get all the parts together.
  844. $validTokens = array(
  845. \T_STRING => true,
  846. \T_NS_SEPARATOR => true,
  847. \T_WHITESPACE => true,
  848. );
  849. $namespaceName = '';
  850. while (isset($validTokens[$tokens[$nextToken]['code']]) === true) {
  851. $namespaceName .= trim($tokens[$nextToken]['content']);
  852. $nextToken++;
  853. }
  854. return $namespaceName;
  855. }
  856. /**
  857. * Get the stack pointer for a return type token for a given function.
  858. *
  859. * Compatible layer for older PHPCS versions which don't recognize
  860. * return type hints correctly.
  861. *
  862. * Expects to be passed T_RETURN_TYPE, T_FUNCTION or T_CLOSURE token.
  863. *
  864. * @since 7.1.2
  865. *
  866. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  867. * @param int $stackPtr The position of the token.
  868. *
  869. * @return int|false Stack pointer to the return type token or false if
  870. * no return type was found or the passed token was
  871. * not of the correct type.
  872. */
  873. public function getReturnTypeHintToken(File $phpcsFile, $stackPtr)
  874. {
  875. $tokens = $phpcsFile->getTokens();
  876. if (\defined('T_RETURN_TYPE') && $tokens[$stackPtr]['code'] === \T_RETURN_TYPE) {
  877. return $stackPtr;
  878. }
  879. if ($tokens[$stackPtr]['code'] !== \T_FUNCTION && $tokens[$stackPtr]['code'] !== \T_CLOSURE) {
  880. return false;
  881. }
  882. if (isset($tokens[$stackPtr]['parenthesis_closer']) === false) {
  883. return false;
  884. }
  885. // Allow for interface and abstract method declarations.
  886. $endOfFunctionDeclaration = null;
  887. if (isset($tokens[$stackPtr]['scope_opener'])) {
  888. $endOfFunctionDeclaration = $tokens[$stackPtr]['scope_opener'];
  889. } else {
  890. $nextSemiColon = $phpcsFile->findNext(\T_SEMICOLON, ($tokens[$stackPtr]['parenthesis_closer'] + 1), null, false, null, true);
  891. if ($nextSemiColon !== false) {
  892. $endOfFunctionDeclaration = $nextSemiColon;
  893. }
  894. }
  895. if (isset($endOfFunctionDeclaration) === false) {
  896. return false;
  897. }
  898. $hasColon = $phpcsFile->findNext(
  899. array(\T_COLON, \T_INLINE_ELSE),
  900. ($tokens[$stackPtr]['parenthesis_closer'] + 1),
  901. $endOfFunctionDeclaration
  902. );
  903. if ($hasColon === false) {
  904. return false;
  905. }
  906. /*
  907. * - `self`, `parent` and `callable` are not being recognized as return types in PHPCS < 2.6.0.
  908. * - Return types are not recognized at all in PHPCS < 2.4.0.
  909. * - The T_RETURN_TYPE token is defined, but no longer in use since PHPCS 3.3.0+.
  910. * The token will now be tokenized as T_STRING.
  911. * - An `array` (return) type declaration was tokenized as `T_ARRAY_HINT` in PHPCS 2.3.3 - 3.2.3
  912. * to prevent confusing sniffs looking for array declarations.
  913. * As of PHPCS 3.3.0 `array` as a type declaration will be tokenized as `T_STRING`.
  914. */
  915. $unrecognizedTypes = array(
  916. \T_CALLABLE,
  917. \T_SELF,
  918. \T_PARENT,
  919. \T_ARRAY, // PHPCS < 2.4.0.
  920. \T_STRING,
  921. );
  922. return $phpcsFile->findPrevious($unrecognizedTypes, ($endOfFunctionDeclaration - 1), $hasColon);
  923. }
  924. /**
  925. * Get the complete return type declaration for a given function.
  926. *
  927. * Cross-version compatible way to retrieve the complete return type declaration.
  928. *
  929. * For a classname-based return type, PHPCS, as well as the Sniff::getReturnTypeHintToken()
  930. * method will mark the classname as the return type token.
  931. * This method will find preceeding namespaces and namespace separators and will return a
  932. * string containing the qualified return type declaration.
  933. *
  934. * Expects to be passed a T_RETURN_TYPE token or the return value from a call to
  935. * the Sniff::getReturnTypeHintToken() method.
  936. *
  937. * @since 8.2.0
  938. *
  939. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  940. * @param int $stackPtr The position of the return type token.
  941. *
  942. * @return string The name of the return type token.
  943. */
  944. public function getReturnTypeHintName(File $phpcsFile, $stackPtr)
  945. {
  946. $tokens = $phpcsFile->getTokens();
  947. // In older PHPCS versions, the nullable indicator will turn a return type colon into a T_INLINE_ELSE.
  948. $colon = $phpcsFile->findPrevious(array(\T_COLON, \T_INLINE_ELSE, \T_FUNCTION, \T_CLOSE_PARENTHESIS), ($stackPtr - 1));
  949. if ($colon === false
  950. || ($tokens[$colon]['code'] !== \T_COLON && $tokens[$colon]['code'] !== \T_INLINE_ELSE)
  951. ) {
  952. // Shouldn't happen, just in case.
  953. return '';
  954. }
  955. $returnTypeHint = '';
  956. for ($i = ($colon + 1); $i <= $stackPtr; $i++) {
  957. // As of PHPCS 3.3.0+, all tokens are tokenized as "normal", so T_CALLABLE, T_SELF etc are
  958. // all possible, just exclude anything that's regarded as empty and the nullable indicator.
  959. if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
  960. continue;
  961. }
  962. if ($tokens[$i]['type'] === 'T_NULLABLE') {
  963. continue;
  964. }
  965. if (\defined('T_NULLABLE') === false && $tokens[$i]['code'] === \T_INLINE_THEN) {
  966. // Old PHPCS.
  967. continue;
  968. }
  969. $returnTypeHint .= $tokens[$i]['content'];
  970. }
  971. return $returnTypeHint;
  972. }
  973. /**
  974. * Check whether a T_VARIABLE token is a class property declaration.
  975. *
  976. * Compatibility layer for PHPCS cross-version compatibility
  977. * as PHPCS 2.4.0 - 2.7.1 does not have good enough support for
  978. * anonymous classes. Along the same lines, the`getMemberProperties()`
  979. * method does not support the `var` prefix.
  980. *
  981. * @since 7.1.4
  982. *
  983. * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
  984. * @param int $stackPtr The position in the stack of the
  985. * T_VARIABLE token to verify.
  986. *
  987. * @return bool
  988. */
  989. public function isClassProperty(File $phpcsFile, $stackPtr)
  990. {
  991. $tokens = $phpcsFile->getTokens();
  992. if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_VARIABLE) {
  993. return false;
  994. }
  995. // Note: interfaces can not declare properties.
  996. $validScopes = array(
  997. 'T_CLASS' => true,
  998. 'T_ANON_CLASS' => true,
  999. 'T_TRAIT' => true,
  1000. );
  1001. $scopePtr = $this->validDirectScope($phpcsFile, $stackPtr, $validScopes);
  1002. if ($scopePtr !== false) {
  1003. // Make sure it's not a method parameter.
  1004. if (empty($tokens[$stackPtr]['nested_parenthesis']) === true) {
  1005. return true;
  1006. } else {
  1007. $parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']);
  1008. $deepestOpen = array_pop($parenthesis);
  1009. if ($deepestOpen < $scopePtr
  1010. || isset($tokens[$deepestOpen]['parenthesis_owner']) === false
  1011. || $tokens[$tokens[$deepestOpen]['parenthesis_owner']]['code'] !== \T_FUNCTION
  1012. ) {
  1013. return true;
  1014. }
  1015. }
  1016. }
  1017. return false;
  1018. }
  1019. /**
  1020. * Check whether a T_CONST token is a class constant declaration.
  1021. *
  1022. * @since 7.1.4
  1023. *
  1024. * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
  1025. * @param int $stackPtr The position in the stack of the
  1026. * T_CONST token to verify.
  1027. *
  1028. * @return bool
  1029. */
  1030. public function isClassConstant(File $phpcsFile, $stackPtr)
  1031. {
  1032. $tokens = $phpcsFile->getTokens();
  1033. if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CONST) {
  1034. return false;
  1035. }
  1036. // Note: traits can not declare constants.
  1037. $validScopes = array(
  1038. 'T_CLASS' => true,
  1039. 'T_ANON_CLASS' => true,
  1040. 'T_INTERFACE' => true,
  1041. );
  1042. if ($this->validDirectScope($phpcsFile, $stackPtr, $validScopes) !== false) {
  1043. return true;
  1044. }
  1045. return false;
  1046. }
  1047. /**
  1048. * Check whether the direct wrapping scope of a token is within a limited set of
  1049. * acceptable tokens.
  1050. *
  1051. * Used to check, for instance, if a T_CONST is a class constant.
  1052. *
  1053. * @since 7.1.4
  1054. *
  1055. * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
  1056. * @param int $stackPtr The position in the stack of the
  1057. * token to verify.
  1058. * @param array $validScopes Array of token types.
  1059. * Keys should be the token types in string
  1060. * format to allow for newer token types.
  1061. * Value is irrelevant.
  1062. *
  1063. * @return int|bool StackPtr to the scope if valid, false otherwise.
  1064. */
  1065. protected function validDirectScope(File $phpcsFile, $stackPtr, $validScopes)
  1066. {
  1067. $tokens = $phpcsFile->getTokens();
  1068. if (empty($tokens[$stackPtr]['conditions']) === true) {
  1069. return false;
  1070. }
  1071. /*
  1072. * Check only the direct wrapping scope of the token.
  1073. */
  1074. $conditions = array_keys($tokens[$stackPtr]['conditions']);
  1075. $ptr = array_pop($conditions);
  1076. if (isset($tokens[$ptr]) === false) {
  1077. return false;
  1078. }
  1079. if (isset($validScopes[$tokens[$ptr]['type']]) === true) {
  1080. return $ptr;
  1081. }
  1082. return false;
  1083. }
  1084. /**
  1085. * Get an array of just the type hints from a function declaration.
  1086. *
  1087. * Expects to be passed T_FUNCTION or T_CLOSURE token.
  1088. *
  1089. * Strips potential nullable indicator and potential global namespace
  1090. * indicator from the type hints before returning them.
  1091. *
  1092. * @since 7.1.4
  1093. *
  1094. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1095. * @param int $stackPtr The position of the token.
  1096. *
  1097. * @return array Array with type hints or an empty array if
  1098. * - the function does not have any parameters
  1099. * - no type hints were found
  1100. * - or the passed token was not of the correct type.
  1101. */
  1102. public function getTypeHintsFromFunctionDeclaration(File $phpcsFile, $stackPtr)
  1103. {
  1104. $tokens = $phpcsFile->getTokens();
  1105. if ($tokens[$stackPtr]['code'] !== \T_FUNCTION && $tokens[$stackPtr]['code'] !== \T_CLOSURE) {
  1106. return array();
  1107. }
  1108. $parameters = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
  1109. if (empty($parameters) || \is_array($parameters) === false) {
  1110. return array();
  1111. }
  1112. $typeHints = array();
  1113. foreach ($parameters as $param) {
  1114. if ($param['type_hint'] === '') {
  1115. continue;
  1116. }
  1117. // Strip off potential nullable indication.
  1118. $typeHint = ltrim($param['type_hint'], '?');
  1119. // Strip off potential (global) namespace indication.
  1120. $typeHint = ltrim($typeHint, '\\');
  1121. if ($typeHint !== '') {
  1122. $typeHints[] = $typeHint;
  1123. }
  1124. }
  1125. return $typeHints;
  1126. }
  1127. /**
  1128. * Get the hash algorithm name from the parameter in a hash function call.
  1129. *
  1130. * @since 7.0.7 Logic was originally contained in the `RemovedHashAlgorithms` sniff.
  1131. *
  1132. * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
  1133. * @param int $stackPtr The position of the T_STRING function token.
  1134. *
  1135. * @return string|false The algorithm name without quotes if this was a relevant hash
  1136. * function call or false if it was not.
  1137. */
  1138. public function getHashAlgorithmParameter(File $phpcsFile, $stackPtr)
  1139. {
  1140. $tokens = $phpcsFile->getTokens();
  1141. // Check for the existence of the token.
  1142. if (isset($tokens[$stackPtr]) === false) {
  1143. return false;
  1144. }
  1145. if ($tokens[$stackPtr]['code'] !== \T_STRING) {
  1146. return false;
  1147. }
  1148. $functionName = $tokens[$stackPtr]['content'];
  1149. $functionNameLc = strtolower($functionName);
  1150. // Bow out if not one of the functions we're targetting.
  1151. if (isset($this->hashAlgoFunctions[$functionNameLc]) === false) {
  1152. return false;
  1153. }
  1154. // Get the parameter from the function call which should contain the algorithm name.
  1155. $algoParam = $this->getFunctionCallParameter($phpcsFile, $stackPtr, $this->hashAlgoFunctions[$functionNameLc]);
  1156. if ($algoParam === false) {
  1157. return false;
  1158. }
  1159. // Algorithm is a text string, so we need to remove the quotes.
  1160. $algo = strtolower(trim($algoParam['raw']));
  1161. $algo = $this->stripQuotes($algo);
  1162. return $algo;
  1163. }
  1164. /**
  1165. * Determine whether an arbitrary T_STRING token is the use of a global constant.
  1166. *
  1167. * @since 8.1.0
  1168. *
  1169. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1170. * @param int $stackPtr The position of the T_STRING token.
  1171. *
  1172. * @return bool
  1173. */
  1174. public function isUseOfGlobalConstant(File $phpcsFile, $stackPtr)
  1175. {
  1176. static $isLowPHPCS, $isLowPHP;
  1177. $tokens = $phpcsFile->getTokens();
  1178. // Check for the existence of the token.
  1179. if (isset($tokens[$stackPtr]) === false) {
  1180. return false;
  1181. }
  1182. // Is this one of the tokens this function handles ?
  1183. if ($tokens[$stackPtr]['code'] !== \T_STRING) {
  1184. return false;
  1185. }
  1186. // Check for older PHP, PHPCS version so we can compensate for misidentified tokens.
  1187. if (isset($isLowPHPCS, $isLowPHP) === false) {
  1188. $isLowPHP = false;
  1189. $isLowPHPCS = false;
  1190. if (version_compare(\PHP_VERSION_ID, '50400', '<')) {
  1191. $isLowPHP = true;
  1192. $isLowPHPCS = version_compare(PHPCSHelper::getVersion(), '2.4.0', '<');
  1193. }
  1194. }
  1195. $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
  1196. if ($next !== false
  1197. && ($tokens[$next]['code'] === \T_OPEN_PARENTHESIS
  1198. || $tokens[$next]['code'] === \T_DOUBLE_COLON)
  1199. ) {
  1200. // Function call or declaration.
  1201. return false;
  1202. }
  1203. // Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a global constant.
  1204. $tokensToIgnore = array(
  1205. 'T_NAMESPACE' => true,
  1206. 'T_USE' => true,
  1207. 'T_CLASS' => true,
  1208. 'T_TRAIT' => true,
  1209. 'T_INTERFACE' => true,
  1210. 'T_EXTENDS' => true,
  1211. 'T_IMPLEMENTS' => true,
  1212. 'T_NEW' => true,
  1213. 'T_FUNCTION' => true,
  1214. 'T_DOUBLE_COLON' => true,
  1215. 'T_OBJECT_OPERATOR' => true,
  1216. 'T_INSTANCEOF' => true,
  1217. 'T_INSTEADOF' => true,
  1218. 'T_GOTO' => true,
  1219. 'T_AS' => true,
  1220. 'T_PUBLIC' => true,
  1221. 'T_PROTECTED' => true,
  1222. 'T_PRIVATE' => true,
  1223. );
  1224. $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
  1225. if ($prev !== false
  1226. && (isset($tokensToIgnore[$tokens[$prev]['type']]) === true
  1227. || ($tokens[$prev]['code'] === \T_STRING
  1228. && (($isLowPHPCS === true
  1229. && $tokens[$prev]['content'] === 'trait')
  1230. || ($isLowPHP === true
  1231. && $tokens[$prev]['content'] === 'insteadof'))))
  1232. ) {
  1233. // Not the use of a constant.
  1234. return false;
  1235. }
  1236. if ($prev !== false
  1237. && $tokens[$prev]['code'] === \T_NS_SEPARATOR
  1238. && $tokens[($prev - 1)]['code'] === \T_STRING
  1239. ) {
  1240. // Namespaced constant of the same name.
  1241. return false;
  1242. }
  1243. if ($prev !== false
  1244. && $tokens[$prev]['code'] === \T_CONST
  1245. && $this->isClassConstant($phpcsFile, $prev) === true
  1246. ) {
  1247. // Class constant declaration of the same name.
  1248. return false;
  1249. }
  1250. /*
  1251. * Deal with a number of variations of use statements.
  1252. */
  1253. for ($i = $stackPtr; $i > 0; $i--) {
  1254. if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) {
  1255. break;
  1256. }
  1257. }
  1258. $firstOnLine = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
  1259. if ($firstOnLine !== false && $tokens[$firstOnLine]['code'] === \T_USE) {
  1260. $nextOnLine = $phpcsFile->findNext(Tokens::$emptyTokens, ($firstOnLine + 1), null, true);
  1261. if ($nextOnLine !== false) {
  1262. if (($tokens[$nextOnLine]['code'] === \T_STRING && $tokens[$nextOnLine]['content'] === 'const')
  1263. || $tokens[$nextOnLine]['code'] === \T_CONST // Happens in some PHPCS versions.
  1264. ) {
  1265. $hasNsSep = $phpcsFile->findNext(\T_NS_SEPARATOR, ($nextOnLine + 1), $stackPtr);
  1266. if ($hasNsSep !== false) {
  1267. // Namespaced const (group) use statement.
  1268. return false;
  1269. }
  1270. } else {
  1271. // Not a const use statement.
  1272. return false;
  1273. }
  1274. }
  1275. }
  1276. return true;
  1277. }
  1278. /**
  1279. * Determine whether the tokens between $start and $end together form a positive number
  1280. * as recognized by PHP.
  1281. *
  1282. * The outcome of this function is reliable for `true`, `false` should be regarded as
  1283. * "undetermined".
  1284. *
  1285. * Note: Zero is *not* regarded as a positive number.
  1286. *
  1287. * @since 8.2.0
  1288. *
  1289. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1290. * @param int $start Start of the snippet (inclusive), i.e. this
  1291. * token will be examined as part of the snippet.
  1292. * @param int $end End of the snippet (inclusive), i.e. this
  1293. * token will be examined as part of the snippet.
  1294. * @param bool $allowFloats Whether to only consider integers, or also floats.
  1295. *
  1296. * @return bool True if PHP would evaluate the snippet as a positive number.
  1297. * False if not or if it could not be reliably determined
  1298. * (variable or calculations and such).
  1299. */
  1300. public function isPositiveNumber(File $phpcsFile, $start, $end, $allowFloats = false)
  1301. {
  1302. $number = $this->isNumber($phpcsFile, $start, $end, $allowFloats);
  1303. if ($number === false) {
  1304. return false;
  1305. }
  1306. return ($number > 0);
  1307. }
  1308. /**
  1309. * Determine whether the tokens between $start and $end together form a negative number
  1310. * as recognized by PHP.
  1311. *
  1312. * The outcome of this function is reliable for `true`, `false` should be regarded as
  1313. * "undetermined".
  1314. *
  1315. * Note: Zero is *not* regarded as a negative number.
  1316. *
  1317. * @since 8.2.0
  1318. *
  1319. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1320. * @param int $start Start of the snippet (inclusive), i.e. this
  1321. * token will be examined as part of the snippet.
  1322. * @param int $end End of the snippet (inclusive), i.e. this
  1323. * token will be examined as part of the snippet.
  1324. * @param bool $allowFloats Whether to only consider integers, or also floats.
  1325. *
  1326. * @return bool True if PHP would evaluate the snippet as a negative number.
  1327. * False if not or if it could not be reliably determined
  1328. * (variable or calculations and such).
  1329. */
  1330. public function isNegativeNumber(File $phpcsFile, $start, $end, $allowFloats = false)
  1331. {
  1332. $number = $this->isNumber($phpcsFile, $start, $end, $allowFloats);
  1333. if ($number === false) {
  1334. return false;
  1335. }
  1336. return ($number < 0);
  1337. }
  1338. /**
  1339. * Determine whether the tokens between $start and $end together form a number
  1340. * as recognized by PHP.
  1341. *
  1342. * The outcome of this function is reliable for "true-ish" values, `false` should
  1343. * be regarded as "undetermined".
  1344. *
  1345. * @link https://3v4l.org/npTeM
  1346. *
  1347. * Mainly intended for examining variable assignments, function call parameters, array values
  1348. * where the start and end of the snippet to examine is very clear.
  1349. *
  1350. * @since 8.2.0
  1351. *
  1352. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1353. * @param int $start Start of the snippet (inclusive), i.e. this
  1354. * token will be examined as part of the snippet.
  1355. * @param int $end End of the snippet (inclusive), i.e. this
  1356. * token will be examined as part of the snippet.
  1357. * @param bool $allowFloats Whether to only consider integers, or also floats.
  1358. *
  1359. * @return int|float|bool The number found if PHP would evaluate the snippet as a number.
  1360. * The return type will be int if $allowFloats is false, if
  1361. * $allowFloats is true, the return type will be float.
  1362. * False will be returned when the snippet does not evaluate to a
  1363. * number or if it could not be reliably determined
  1364. * (variable or calculations and such).
  1365. */
  1366. protected function isNumber(File $phpcsFile, $start, $end, $allowFloats = false)
  1367. {
  1368. $stringTokens = Tokens::$heredocTokens + Tokens::$stringTokens;
  1369. $validTokens = array();
  1370. $validTokens[\T_LNUMBER] = true;
  1371. $validTokens[\T_TRUE] = true; // Evaluates to int 1.
  1372. $validTokens[\T_FALSE] = true; // Evaluates to int 0.
  1373. $validTokens[\T_NULL] = true; // Evaluates to int 0.
  1374. if ($allowFloats === true) {
  1375. $validTokens[\T_DNUMBER] = true;
  1376. }
  1377. $maybeValidTokens = $stringTokens + $validTokens;
  1378. $tokens = $phpcsFile->getTokens();
  1379. $searchEnd = ($end + 1);
  1380. $negativeNumber = false;
  1381. if (isset($tokens[$start], $tokens[$searchEnd]) === false) {
  1382. return false;
  1383. }
  1384. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $searchEnd, true);
  1385. while ($nextNonEmpty !== false
  1386. && ($tokens[$nextNonEmpty]['code'] === \T_PLUS
  1387. || $tokens[$nextNonEmpty]['code'] === \T_MINUS)
  1388. ) {
  1389. if ($tokens[$nextNonEmpty]['code'] === \T_MINUS) {
  1390. $negativeNumber = ($negativeNumber === false) ? true : false;
  1391. }
  1392. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $searchEnd, true);
  1393. }
  1394. if ($nextNonEmpty === false || isset($maybeValidTokens[$tokens[$nextNonEmpty]['code']]) === false) {
  1395. return false;
  1396. }
  1397. $content = false;
  1398. if ($tokens[$nextNonEmpty]['code'] === \T_LNUMBER
  1399. || $tokens[$nextNonEmpty]['code'] === \T_DNUMBER
  1400. ) {
  1401. $content = (float) $tokens[$nextNonEmpty]['content'];
  1402. } elseif ($tokens[$nextNonEmpty]['code'] === \T_TRUE) {
  1403. $content = 1.0;
  1404. } elseif ($tokens[$nextNonEmpty]['code'] === \T_FALSE
  1405. || $tokens[$nextNonEmpty]['code'] === \T_NULL
  1406. ) {
  1407. $content = 0.0;
  1408. } elseif (isset($stringTokens[$tokens[$nextNonEmpty]['code']]) === true) {
  1409. if ($tokens[$nextNonEmpty]['code'] === \T_START_HEREDOC
  1410. || $tokens[$nextNonEmpty]['code'] === \T_START_NOWDOC
  1411. ) {
  1412. // Skip past heredoc/nowdoc opener to the first content.
  1413. $firstDocToken = $phpcsFile->findNext(array(\T_HEREDOC, \T_NOWDOC), ($nextNonEmpty + 1), $searchEnd);
  1414. if ($firstDocToken === false) {
  1415. // Live coding or parse error.
  1416. return false;
  1417. }
  1418. $stringContent = $content = $tokens[$firstDocToken]['content'];
  1419. // Skip forward to the end in preparation for the next part of the examination.
  1420. $nextNonEmpty = $phpcsFile->findNext(array(\T_END_HEREDOC, \T_END_NOWDOC), ($nextNonEmpty + 1), $searchEnd);
  1421. if ($nextNonEmpty === false) {
  1422. // Live coding or parse error.
  1423. return false;
  1424. }
  1425. } else {
  1426. // Gather subsequent lines for a multi-line string.
  1427. for ($i = $nextNonEmpty; $i < $searchEnd; $i++) {
  1428. if ($tokens[$i]['code'] !== $tokens[$nextNonEmpty]['code']) {
  1429. break;
  1430. }
  1431. $content .= $tokens[$i]['content'];
  1432. }
  1433. $nextNonEmpty = --$i;
  1434. $content = $this->stripQuotes($content);
  1435. $stringContent = $content;
  1436. }
  1437. /*
  1438. * Regexes based on the formats outlined in the manual, created by JRF.
  1439. * @link https://www.php.net/manual/en/language.types.float.php
  1440. */
  1441. $regexInt = '`^\s*[0-9]+`';
  1442. $regexFloat = '`^\s*(?:[+-]?(?:(?:(?P<LNUM>[0-9]+)|(?P<DNUM>([0-9]*\.(?P>LNUM)|(?P>LNUM)\.[0-9]*)))[eE][+-]?(?P>LNUM))|(?P>DNUM))`';
  1443. $intString = preg_match($regexInt, $content, $intMatch);
  1444. $floatString = preg_match($regexFloat, $content, $floatMatch);
  1445. // Does the text string start with a number ? If so, PHP would juggle it and use it as a number.
  1446. if ($allowFloats === false) {
  1447. if ($intString !== 1 || $floatString === 1) {
  1448. if ($floatString === 1) {
  1449. // Found float. Only integers targetted.
  1450. return false;
  1451. }
  1452. $content = 0.0;
  1453. } else {
  1454. $content = (float) trim($intMatch[0]);
  1455. }
  1456. } else {
  1457. if ($intString !== 1 && $floatString !== 1) {
  1458. $content = 0.0;
  1459. } else {
  1460. $content = ($floatString === 1) ? (float) trim($floatMatch[0]) : (float) trim($intMatch[0]);
  1461. }
  1462. }
  1463. // Allow for different behaviour for hex numeric strings between PHP 5 vs PHP 7.
  1464. if ($intString === 1 && trim($intMatch[0]) === '0'
  1465. && preg_match('`^\s*(0x[A-Fa-f0-9]+)`', $stringContent, $hexNumberString) === 1
  1466. && $this->supportsBelow('5.6') === true
  1467. ) {
  1468. // The filter extension still allows for hex numeric strings in PHP 7, so
  1469. // use that to get the numeric value if possible.
  1470. // If the filter extension is not available, the value will be zero, but so be it.
  1471. if (function_exists('filter_var')) {
  1472. $filtered = filter_var($hexNumberString[1], \FILTER_VALIDATE_INT, \FILTER_FLAG_ALLOW_HEX);
  1473. if ($filtered !== false) {
  1474. $content = $filtered;
  1475. }
  1476. }
  1477. }
  1478. }
  1479. // OK, so we have a number, now is there still more code after it ?
  1480. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $searchEnd, true);
  1481. if ($nextNonEmpty !== false) {
  1482. return false;
  1483. }
  1484. if ($negativeNumber === true) {
  1485. $content = -$content;
  1486. }
  1487. if ($allowFloats === false) {
  1488. return (int) $content;
  1489. }
  1490. return $content;
  1491. }
  1492. /**
  1493. * Determine whether the tokens between $start and $end together form a numberic calculation
  1494. * as recognized by PHP.
  1495. *
  1496. * The outcome of this function is reliable for `true`, `false` should be regarded as "undetermined".
  1497. *
  1498. * Mainly intended for examining variable assignments, function call parameters, array values
  1499. * where the start and end of the snippet to examine is very clear.
  1500. *
  1501. * @since 9.0.0
  1502. *
  1503. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1504. * @param int $start Start of the snippet (inclusive), i.e. this
  1505. * token will be examined as part of the snippet.
  1506. * @param int $end End of the snippet (inclusive), i.e. this
  1507. * token will be examined as part of the snippet.
  1508. *
  1509. * @return bool
  1510. */
  1511. protected function isNumericCalculation(File $phpcsFile, $start, $end)
  1512. {
  1513. $arithmeticTokens = Tokens::$arithmeticTokens;
  1514. // phpcs:disable PHPCompatibility.Constants.NewConstants.t_powFound
  1515. if (\defined('T_POW') && isset($arithmeticTokens[\T_POW]) === false) {
  1516. // T_POW was not added to the arithmetic array until PHPCS 2.9.0.
  1517. $arithmeticTokens[\T_POW] = \T_POW;
  1518. }
  1519. // phpcs:enable
  1520. $skipTokens = Tokens::$emptyTokens;
  1521. $skipTokens[] = \T_MINUS;
  1522. $skipTokens[] = \T_PLUS;
  1523. // Find the first arithmetic operator, but skip past +/- signs before numbers.
  1524. $nextNonEmpty = ($start - 1);
  1525. do {
  1526. $nextNonEmpty = $phpcsFile->findNext($skipTokens, ($nextNonEmpty + 1), ($end + 1), true);
  1527. $arithmeticOperator = $phpcsFile->findNext($arithmeticTokens, ($nextNonEmpty + 1), ($end + 1));
  1528. } while ($nextNonEmpty !== false && $arithmeticOperator !== false && $nextNonEmpty === $arithmeticOperator);
  1529. if ($arithmeticOperator === false) {
  1530. return false;
  1531. }
  1532. $tokens = $phpcsFile->getTokens();
  1533. $subsetStart = $start;
  1534. $subsetEnd = ($arithmeticOperator - 1);
  1535. while ($this->isNumber($phpcsFile, $subsetStart, $subsetEnd, true) !== false
  1536. && isset($tokens[($arithmeticOperator + 1)]) === true
  1537. ) {
  1538. // Recognize T_POW for PHPCS < 2.4.0 on low PHP versions.
  1539. if (\defined('T_POW') === false
  1540. && $tokens[$arithmeticOperator]['code'] === \T_MULTIPLY
  1541. && $tokens[($arithmeticOperator + 1)]['code'] === \T_MULTIPLY
  1542. && isset($tokens[$arithmeticOperator + 2]) === true
  1543. ) {
  1544. // Move operator one forward to the second * in T_POW.
  1545. ++$arithmeticOperator;
  1546. }
  1547. $subsetStart = ($arithmeticOperator + 1);
  1548. $nextNonEmpty = $arithmeticOperator;
  1549. do {
  1550. $nextNonEmpty = $phpcsFile->findNext($skipTokens, ($nextNonEmpty + 1), ($end + 1), true);
  1551. $arithmeticOperator = $phpcsFile->findNext($arithmeticTokens, ($nextNonEmpty + 1), ($end + 1));
  1552. } while ($nextNonEmpty !== false && $arithmeticOperator !== false && $nextNonEmpty === $arithmeticOperator);
  1553. if ($arithmeticOperator === false) {
  1554. // Last calculation operator already reached.
  1555. if ($this->isNumber($phpcsFile, $subsetStart, $end, true) !== false) {
  1556. return true;
  1557. }
  1558. return false;
  1559. }
  1560. $subsetEnd = ($arithmeticOperator - 1);
  1561. }
  1562. return false;
  1563. }
  1564. /**
  1565. * Determine whether a ternary is a short ternary, i.e. without "middle".
  1566. *
  1567. * N.B.: This is a back-fill for a new method which is expected to go into
  1568. * PHP_CodeSniffer 3.5.0.
  1569. * Once that method has been merged into PHPCS, this one should be moved
  1570. * to the PHPCSHelper.php file.
  1571. *
  1572. * @since 9.2.0
  1573. *
  1574. * @codeCoverageIgnore Method as pulled upstream is accompanied by unit tests.
  1575. *
  1576. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1577. * @param int $stackPtr The position of the ternary operator
  1578. * in the stack.
  1579. *
  1580. * @return bool True if short ternary, or false otherwise.
  1581. */
  1582. public function isShortTernary(File $phpcsFile, $stackPtr)
  1583. {
  1584. $tokens = $phpcsFile->getTokens();
  1585. if (isset($tokens[$stackPtr]) === false
  1586. || $tokens[$stackPtr]['code'] !== \T_INLINE_THEN
  1587. ) {
  1588. return false;
  1589. }
  1590. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
  1591. if ($nextNonEmpty === false) {
  1592. // Live coding or parse error.
  1593. return false;
  1594. }
  1595. if ($tokens[$nextNonEmpty]['code'] === \T_INLINE_ELSE) {
  1596. return true;
  1597. }
  1598. return false;
  1599. }
  1600. /**
  1601. * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a list() construct.
  1602. *
  1603. * Note: A variety of PHPCS versions have bugs in the tokenizing of short arrays.
  1604. * In that case, the tokens are identified as T_OPEN/CLOSE_SQUARE_BRACKET.
  1605. *
  1606. * @since 8.2.0
  1607. *
  1608. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1609. * @param int $stackPtr The position of the function call token.
  1610. *
  1611. * @return bool
  1612. */
  1613. public function isShortList(File $phpcsFile, $stackPtr)
  1614. {
  1615. $tokens = $phpcsFile->getTokens();
  1616. // Check for the existence of the token.
  1617. if (isset($tokens[$stackPtr]) === false) {
  1618. return false;
  1619. }
  1620. // Is this one of the tokens this function handles ?
  1621. if ($tokens[$stackPtr]['code'] !== \T_OPEN_SHORT_ARRAY
  1622. && $tokens[$stackPtr]['code'] !== \T_CLOSE_SHORT_ARRAY
  1623. ) {
  1624. return false;
  1625. }
  1626. switch ($tokens[$stackPtr]['code']) {
  1627. case \T_OPEN_SHORT_ARRAY:
  1628. if (isset($tokens[$stackPtr]['bracket_closer']) === true) {
  1629. $opener = $stackPtr;
  1630. $closer = $tokens[$stackPtr]['bracket_closer'];
  1631. }
  1632. break;
  1633. case \T_CLOSE_SHORT_ARRAY:
  1634. if (isset($tokens[$stackPtr]['bracket_opener']) === true) {
  1635. $opener = $tokens[$stackPtr]['bracket_opener'];
  1636. $closer = $stackPtr;
  1637. }
  1638. break;
  1639. }
  1640. if (isset($opener, $closer) === false) {
  1641. // Parse error, live coding or real square bracket.
  1642. return false;
  1643. }
  1644. /*
  1645. * PHPCS cross-version compatibility: work around for square brackets misidentified
  1646. * as short array when preceded by a variable variable in older PHPCS versions.
  1647. */
  1648. $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true, null, true);
  1649. if ($prevNonEmpty !== false
  1650. && $tokens[$prevNonEmpty]['code'] === \T_CLOSE_CURLY_BRACKET
  1651. && isset($tokens[$prevNonEmpty]['bracket_opener']) === true
  1652. ) {
  1653. $maybeVariableVariable = $phpcsFile->findPrevious(
  1654. Tokens::$emptyTokens,
  1655. ($tokens[$prevNonEmpty]['bracket_opener'] - 1),
  1656. null,
  1657. true,
  1658. null,
  1659. true
  1660. );
  1661. if ($tokens[$maybeVariableVariable]['code'] === \T_VARIABLE
  1662. || $tokens[$maybeVariableVariable]['code'] === \T_DOLLAR
  1663. ) {
  1664. return false;
  1665. }
  1666. }
  1667. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true, null, true);
  1668. if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_EQUAL) {
  1669. return true;
  1670. }
  1671. if ($prevNonEmpty !== false
  1672. && $tokens[$prevNonEmpty]['code'] === \T_AS
  1673. && isset($tokens[$prevNonEmpty]['nested_parenthesis']) === true
  1674. ) {
  1675. $parentheses = array_reverse($tokens[$prevNonEmpty]['nested_parenthesis'], true);
  1676. foreach ($parentheses as $open => $close) {
  1677. if (isset($tokens[$open]['parenthesis_owner'])
  1678. && $tokens[$tokens[$open]['parenthesis_owner']]['code'] === \T_FOREACH
  1679. ) {
  1680. return true;
  1681. }
  1682. }
  1683. }
  1684. // Maybe this is a short list syntax nested inside another short list syntax ?
  1685. $parentOpener = $opener;
  1686. do {
  1687. $parentOpener = $phpcsFile->findPrevious(
  1688. array(\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET),
  1689. ($parentOpener - 1),
  1690. null,
  1691. false,
  1692. null,
  1693. true
  1694. );
  1695. if ($parentOpener === false) {
  1696. return false;
  1697. }
  1698. } while (isset($tokens[$parentOpener]['bracket_closer']) === true
  1699. && $tokens[$parentOpener]['bracket_closer'] < $opener
  1700. );
  1701. if (isset($tokens[$parentOpener]['bracket_closer']) === true
  1702. && $tokens[$parentOpener]['bracket_closer'] > $closer
  1703. ) {
  1704. // Work around tokenizer issue in PHPCS 2.0 - 2.7.
  1705. $phpcsVersion = PHPCSHelper::getVersion();
  1706. if ((version_compare($phpcsVersion, '2.0', '>') === true
  1707. && version_compare($phpcsVersion, '2.8', '<') === true)
  1708. && $tokens[$parentOpener]['code'] === \T_OPEN_SQUARE_BRACKET
  1709. ) {
  1710. $nextNonEmpty = $phpcsFile->findNext(
  1711. Tokens::$emptyTokens,
  1712. ($tokens[$parentOpener]['bracket_closer'] + 1),
  1713. null,
  1714. true,
  1715. null,
  1716. true
  1717. );
  1718. if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_EQUAL) {
  1719. return true;
  1720. }
  1721. return false;
  1722. }
  1723. return $this->isShortList($phpcsFile, $parentOpener);
  1724. }
  1725. return false;
  1726. }
  1727. /**
  1728. * Determine whether the tokens between $start and $end could together represent a variable.
  1729. *
  1730. * @since 9.0.0
  1731. *
  1732. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1733. * @param int $start Starting point stack pointer. Inclusive.
  1734. * I.e. this token should be taken into account.
  1735. * @param int $end End point stack pointer. Exclusive.
  1736. * I.e. this token should not be taken into account.
  1737. * @param int $targetNestingLevel The nesting level the variable should be at.
  1738. *
  1739. * @return bool
  1740. */
  1741. public function isVariable(File $phpcsFile, $start, $end, $targetNestingLevel)
  1742. {
  1743. static $tokenBlackList, $bracketTokens;
  1744. // Create the token arrays only once.
  1745. if (isset($tokenBlackList, $bracketTokens) === false) {
  1746. $tokenBlackList = array(
  1747. \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
  1748. \T_STRING_CONCAT => \T_STRING_CONCAT,
  1749. );
  1750. $tokenBlackList += Tokens::$assignmentTokens;
  1751. $tokenBlackList += Tokens::$equalityTokens;
  1752. $tokenBlackList += Tokens::$comparisonTokens;
  1753. $tokenBlackList += Tokens::$operators;
  1754. $tokenBlackList += Tokens::$booleanOperators;
  1755. $tokenBlackList += Tokens::$castTokens;
  1756. /*
  1757. * List of brackets which can be part of a variable variable.
  1758. *
  1759. * Key is the open bracket token, value the close bracket token.
  1760. */
  1761. $bracketTokens = array(
  1762. \T_OPEN_CURLY_BRACKET => \T_CLOSE_CURLY_BRACKET,
  1763. \T_OPEN_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET,
  1764. );
  1765. }
  1766. $tokens = $phpcsFile->getTokens();
  1767. // If no variable at all was found, then it's definitely a no-no.
  1768. $hasVariable = $phpcsFile->findNext(\T_VARIABLE, $start, $end);
  1769. if ($hasVariable === false) {
  1770. return false;
  1771. }
  1772. // Check if the variable found is at the right level. Deeper levels are always an error.
  1773. if (isset($tokens[$hasVariable]['nested_parenthesis'])
  1774. && \count($tokens[$hasVariable]['nested_parenthesis']) !== $targetNestingLevel
  1775. ) {
  1776. return false;
  1777. }
  1778. // Ok, so the first variable is at the right level, now are there any
  1779. // blacklisted tokens within the empty() ?
  1780. $hasBadToken = $phpcsFile->findNext($tokenBlackList, $start, $end);
  1781. if ($hasBadToken === false) {
  1782. return true;
  1783. }
  1784. // If there are also bracket tokens, the blacklisted token might be part of a variable
  1785. // variable, but if there are no bracket tokens, we know we have an error.
  1786. $hasBrackets = $phpcsFile->findNext($bracketTokens, $start, $end);
  1787. if ($hasBrackets === false) {
  1788. return false;
  1789. }
  1790. // Ok, we have both a blacklisted token as well as brackets, so we need to walk
  1791. // the tokens of the variable variable.
  1792. for ($i = $start; $i < $end; $i++) {
  1793. // If this is a bracket token, skip to the end of the bracketed expression.
  1794. if (isset($bracketTokens[$tokens[$i]['code']], $tokens[$i]['bracket_closer'])) {
  1795. $i = $tokens[$i]['bracket_closer'];
  1796. continue;
  1797. }
  1798. // If it's a blacklisted token, not within brackets, we have an error.
  1799. if (isset($tokenBlackList[$tokens[$i]['code']])) {
  1800. return false;
  1801. }
  1802. }
  1803. return true;
  1804. }
  1805. /**
  1806. * Determine whether a T_MINUS/T_PLUS token is a unary operator.
  1807. *
  1808. * N.B.: This is a back-fill for a new method which is expected to go into
  1809. * PHP_CodeSniffer 3.5.0.
  1810. * Once that method has been merged into PHPCS, this one should be moved
  1811. * to the PHPCSHelper.php file.
  1812. *
  1813. * @since 9.2.0
  1814. *
  1815. * @codeCoverageIgnore Method as pulled upstream is accompanied by unit tests.
  1816. *
  1817. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1818. * @param int $stackPtr The position of the plus/minus token.
  1819. *
  1820. * @return bool True if the token passed is a unary operator.
  1821. * False otherwise or if the token is not a T_PLUS/T_MINUS token.
  1822. */
  1823. public static function isUnaryPlusMinus(File $phpcsFile, $stackPtr)
  1824. {
  1825. $tokens = $phpcsFile->getTokens();
  1826. if (isset($tokens[$stackPtr]) === false
  1827. || ($tokens[$stackPtr]['code'] !== \T_PLUS
  1828. && $tokens[$stackPtr]['code'] !== \T_MINUS)
  1829. ) {
  1830. return false;
  1831. }
  1832. $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
  1833. if ($next === false) {
  1834. // Live coding or parse error.
  1835. return false;
  1836. }
  1837. if (isset(Tokens::$operators[$tokens[$next]['code']]) === true) {
  1838. // Next token is an operator, so this is not a unary.
  1839. return false;
  1840. }
  1841. $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
  1842. if ($tokens[$prev]['code'] === \T_RETURN) {
  1843. // Just returning a positive/negative value; eg. (return -1).
  1844. return true;
  1845. }
  1846. if (isset(Tokens::$operators[$tokens[$prev]['code']]) === true) {
  1847. // Just trying to operate on a positive/negative value; eg. ($var * -1).
  1848. return true;
  1849. }
  1850. if (isset(Tokens::$comparisonTokens[$tokens[$prev]['code']]) === true) {
  1851. // Just trying to compare a positive/negative value; eg. ($var === -1).
  1852. return true;
  1853. }
  1854. if (isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true) {
  1855. // Just trying to compare a positive/negative value; eg. ($var || -1 === $b).
  1856. return true;
  1857. }
  1858. if (isset(Tokens::$assignmentTokens[$tokens[$prev]['code']]) === true) {
  1859. // Just trying to assign a positive/negative value; eg. ($var = -1).
  1860. return true;
  1861. }
  1862. if (isset(Tokens::$castTokens[$tokens[$prev]['code']]) === true) {
  1863. // Just casting a positive/negative value; eg. (string) -$var.
  1864. return true;
  1865. }
  1866. // Other indicators that a plus/minus sign is a unary operator.
  1867. $invalidTokens = array(
  1868. \T_COMMA => true,
  1869. \T_OPEN_PARENTHESIS => true,
  1870. \T_OPEN_SQUARE_BRACKET => true,
  1871. \T_OPEN_SHORT_ARRAY => true,
  1872. \T_COLON => true,
  1873. \T_INLINE_THEN => true,
  1874. \T_INLINE_ELSE => true,
  1875. \T_CASE => true,
  1876. \T_OPEN_CURLY_BRACKET => true,
  1877. \T_STRING_CONCAT => true,
  1878. );
  1879. if (isset($invalidTokens[$tokens[$prev]['code']]) === true) {
  1880. // Just trying to use a positive/negative value; eg. myFunction($var, -2).
  1881. return true;
  1882. }
  1883. return false;
  1884. }
  1885. /**
  1886. * Get the complete contents of a multi-line text string.
  1887. *
  1888. * N.B.: This is a back-fill for a new method which is expected to go into
  1889. * PHP_CodeSniffer 3.5.0.
  1890. * Once that method has been merged into PHPCS, this one should be moved
  1891. * to the PHPCSHelper.php file.
  1892. *
  1893. * @since 9.3.0
  1894. *
  1895. * @codeCoverageIgnore Method as pulled upstream is accompanied by unit tests.
  1896. *
  1897. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  1898. * @param int $stackPtr Pointer to the first text string token
  1899. * of a multi-line text string or to a
  1900. * Nowdoc/Heredoc opener.
  1901. * @param bool $stripQuotes Optional. Whether to strip text delimiter
  1902. * quotes off the resulting text string.
  1903. * Defaults to true.
  1904. *
  1905. * @return string
  1906. *
  1907. * @throws \PHP_CodeSniffer_Exception If the specified position is not a
  1908. * valid text string token or if the
  1909. * token is not the first text string token.
  1910. */
  1911. public function getCompleteTextString(File $phpcsFile, $stackPtr, $stripQuotes = true)
  1912. {
  1913. $tokens = $phpcsFile->getTokens();
  1914. // Must be the start of a text string token.
  1915. if ($tokens[$stackPtr]['code'] !== \T_START_HEREDOC
  1916. && $tokens[$stackPtr]['code'] !== \T_START_NOWDOC
  1917. && $tokens[$stackPtr]['code'] !== \T_CONSTANT_ENCAPSED_STRING
  1918. && $tokens[$stackPtr]['code'] !== \T_DOUBLE_QUOTED_STRING
  1919. ) {
  1920. throw new PHPCS_Exception('$stackPtr must be of type T_START_HEREDOC, T_START_NOWDOC, T_CONSTANT_ENCAPSED_STRING or T_DOUBLE_QUOTED_STRING');
  1921. }
  1922. if ($tokens[$stackPtr]['code'] === \T_CONSTANT_ENCAPSED_STRING
  1923. || $tokens[$stackPtr]['code'] === \T_DOUBLE_QUOTED_STRING
  1924. ) {
  1925. $prev = $phpcsFile->findPrevious(\T_WHITESPACE, ($stackPtr - 1), null, true);
  1926. if ($tokens[$stackPtr]['code'] === $tokens[$prev]['code']) {
  1927. throw new PHPCS_Exception('$stackPtr must be the start of the text string');
  1928. }
  1929. }
  1930. switch ($tokens[$stackPtr]['code']) {
  1931. case \T_START_HEREDOC:
  1932. $stripQuotes = false;
  1933. $targetType = \T_HEREDOC;
  1934. $current = ($stackPtr + 1);
  1935. break;
  1936. case \T_START_NOWDOC:
  1937. $stripQuotes = false;
  1938. $targetType = \T_NOWDOC;
  1939. $current = ($stackPtr + 1);
  1940. break;
  1941. default:
  1942. $targetType = $tokens[$stackPtr]['code'];
  1943. $current = $stackPtr;
  1944. break;
  1945. }
  1946. $string = '';
  1947. do {
  1948. $string .= $tokens[$current]['content'];
  1949. ++$current;
  1950. } while ($tokens[$current]['code'] === $targetType);
  1951. if ($stripQuotes === true) {
  1952. return $this->stripQuotes($string);
  1953. }
  1954. return $string;
  1955. }
  1956. }