NewConstantScalarExpressionsSniff.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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\InitialValue;
  11. use PHPCompatibility\Sniff;
  12. use PHPCompatibility\PHPCSHelper;
  13. use PHP_CodeSniffer_File as File;
  14. use PHP_CodeSniffer_Tokens as Tokens;
  15. /**
  16. * Detect constant scalar expressions being used to set an initial value.
  17. *
  18. * Since PHP 5.6, it is now possible to provide a scalar expression involving
  19. * numeric and string literals and/or constants in contexts where PHP previously
  20. * expected a static value, such as constant and property declarations and
  21. * default values for function parameters.
  22. *
  23. * PHP version 5.6
  24. *
  25. * @link https://www.php.net/manual/en/migration56.new-features.php#migration56.new-features.const-scalar-exprs
  26. * @link https://wiki.php.net/rfc/const_scalar_exprs
  27. *
  28. * @since 8.2.0
  29. */
  30. class NewConstantScalarExpressionsSniff extends Sniff
  31. {
  32. /**
  33. * Error message.
  34. *
  35. * @since 8.2.0
  36. *
  37. * @var string
  38. */
  39. const ERROR_PHRASE = 'Constant scalar expressions are not allowed %s in PHP 5.5 or earlier.';
  40. /**
  41. * Partial error phrases to be used in combination with the error message constant.
  42. *
  43. * @since 8.2.0
  44. *
  45. * @var array
  46. */
  47. protected $errorPhrases = array(
  48. 'const' => 'when defining constants using the const keyword',
  49. 'property' => 'in property declarations',
  50. 'staticvar' => 'in static variable declarations',
  51. 'default' => 'in default function arguments',
  52. );
  53. /**
  54. * Tokens which were allowed to be used in these declarations prior to PHP 5.6.
  55. *
  56. * This list will be enriched in the setProperties() method.
  57. *
  58. * @since 8.2.0
  59. *
  60. * @var array
  61. */
  62. protected $safeOperands = array(
  63. \T_LNUMBER => \T_LNUMBER,
  64. \T_DNUMBER => \T_DNUMBER,
  65. \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
  66. \T_TRUE => \T_TRUE,
  67. \T_FALSE => \T_FALSE,
  68. \T_NULL => \T_NULL,
  69. \T_LINE => \T_LINE,
  70. \T_FILE => \T_FILE,
  71. \T_DIR => \T_DIR,
  72. \T_FUNC_C => \T_FUNC_C,
  73. \T_CLASS_C => \T_CLASS_C,
  74. \T_TRAIT_C => \T_TRAIT_C,
  75. \T_METHOD_C => \T_METHOD_C,
  76. \T_NS_C => \T_NS_C,
  77. // Special cases:
  78. \T_NS_SEPARATOR => \T_NS_SEPARATOR,
  79. /*
  80. * This can be neigh anything, but for any usage except constants,
  81. * the T_STRING will be combined with non-allowed tokens, so we should be good.
  82. */
  83. \T_STRING => \T_STRING,
  84. );
  85. /**
  86. * Returns an array of tokens this test wants to listen for.
  87. *
  88. * @since 8.2.0
  89. *
  90. * @return array
  91. */
  92. public function register()
  93. {
  94. // Set the properties up only once.
  95. $this->setProperties();
  96. return array(
  97. \T_CONST,
  98. \T_VARIABLE,
  99. \T_FUNCTION,
  100. \T_CLOSURE,
  101. \T_STATIC,
  102. );
  103. }
  104. /**
  105. * Make some adjustments to the $safeOperands property.
  106. *
  107. * @since 8.2.0
  108. *
  109. * @return void
  110. */
  111. public function setProperties()
  112. {
  113. $this->safeOperands += Tokens::$heredocTokens;
  114. $this->safeOperands += Tokens::$emptyTokens;
  115. }
  116. /**
  117. * Do a version check to determine if this sniff needs to run at all.
  118. *
  119. * @since 8.2.0
  120. *
  121. * @return bool
  122. */
  123. protected function bowOutEarly()
  124. {
  125. return ($this->supportsBelow('5.5') !== true);
  126. }
  127. /**
  128. * Processes this test, when one of its tokens is encountered.
  129. *
  130. * @since 8.2.0
  131. *
  132. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  133. * @param int $stackPtr The position of the current token in the
  134. * stack passed in $tokens.
  135. *
  136. * @return void|int Null or integer stack pointer to skip forward.
  137. */
  138. public function process(File $phpcsFile, $stackPtr)
  139. {
  140. if ($this->bowOutEarly() === true) {
  141. return;
  142. }
  143. $tokens = $phpcsFile->getTokens();
  144. switch ($tokens[$stackPtr]['type']) {
  145. case 'T_FUNCTION':
  146. case 'T_CLOSURE':
  147. $params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
  148. if (empty($params)) {
  149. // No parameters.
  150. return;
  151. }
  152. $funcToken = $tokens[$stackPtr];
  153. if (isset($funcToken['parenthesis_owner'], $funcToken['parenthesis_opener'], $funcToken['parenthesis_closer']) === false
  154. || $funcToken['parenthesis_owner'] !== $stackPtr
  155. || isset($tokens[$funcToken['parenthesis_opener']], $tokens[$funcToken['parenthesis_closer']]) === false
  156. ) {
  157. // Hmm.. something is going wrong as these should all be available & valid.
  158. return;
  159. }
  160. $opener = $funcToken['parenthesis_opener'];
  161. $closer = $funcToken['parenthesis_closer'];
  162. // Which nesting level is the one we are interested in ?
  163. $nestedParenthesisCount = 1;
  164. if (isset($tokens[$opener]['nested_parenthesis'])) {
  165. $nestedParenthesisCount += \count($tokens[$opener]['nested_parenthesis']);
  166. }
  167. foreach ($params as $param) {
  168. if (isset($param['default']) === false) {
  169. continue;
  170. }
  171. $end = $param['token'];
  172. while (($end = $phpcsFile->findNext(array(\T_COMMA, \T_CLOSE_PARENTHESIS), ($end + 1), ($closer + 1))) !== false) {
  173. $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $nestedParenthesisCount);
  174. if ($maybeSkipTo !== true) {
  175. $end = $maybeSkipTo;
  176. continue;
  177. }
  178. // Ignore closing parenthesis/bracket if not 'ours'.
  179. if ($tokens[$end]['code'] === \T_CLOSE_PARENTHESIS && $end !== $closer) {
  180. continue;
  181. }
  182. // Ok, we've found the end of the param default value declaration.
  183. break;
  184. }
  185. if ($this->isValidAssignment($phpcsFile, $param['token'], $end) === false) {
  186. $this->throwError($phpcsFile, $param['token'], 'default', $param['content']);
  187. }
  188. }
  189. /*
  190. * No need for the sniff to be triggered by the T_VARIABLEs in the function
  191. * definition as we've already examined them above, so let's skip over them.
  192. */
  193. return $closer;
  194. case 'T_VARIABLE':
  195. case 'T_STATIC':
  196. case 'T_CONST':
  197. $type = 'const';
  198. // Filter out non-property declarations.
  199. if ($tokens[$stackPtr]['code'] === \T_VARIABLE) {
  200. if ($this->isClassProperty($phpcsFile, $stackPtr) === false) {
  201. return;
  202. }
  203. $type = 'property';
  204. // Move back one token to have the same starting point as the others.
  205. $stackPtr = ($stackPtr - 1);
  206. }
  207. // Filter out late static binding and class properties.
  208. if ($tokens[$stackPtr]['code'] === \T_STATIC) {
  209. $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
  210. if ($next === false || $tokens[$next]['code'] !== \T_VARIABLE) {
  211. // Late static binding.
  212. return;
  213. }
  214. if ($this->isClassProperty($phpcsFile, $next) === true) {
  215. // Class properties are examined based on the T_VARIABLE token.
  216. return;
  217. }
  218. unset($next);
  219. $type = 'staticvar';
  220. }
  221. $endOfStatement = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($stackPtr + 1));
  222. if ($endOfStatement === false) {
  223. // No semi-colon - live coding.
  224. return;
  225. }
  226. $targetNestingLevel = 0;
  227. if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
  228. $targetNestingLevel = \count($tokens[$stackPtr]['nested_parenthesis']);
  229. }
  230. // Examine each variable/constant in multi-declarations.
  231. $start = $stackPtr;
  232. $end = $stackPtr;
  233. while (($end = $phpcsFile->findNext(array(\T_COMMA, \T_SEMICOLON, \T_OPEN_SHORT_ARRAY, \T_CLOSE_TAG), ($end + 1), ($endOfStatement + 1))) !== false) {
  234. $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $targetNestingLevel);
  235. if ($maybeSkipTo !== true) {
  236. $end = $maybeSkipTo;
  237. continue;
  238. }
  239. $start = $phpcsFile->findNext(Tokens::$emptyTokens, ($start + 1), $end, true);
  240. if ($start === false
  241. || ($tokens[$stackPtr]['code'] === \T_CONST && $tokens[$start]['code'] !== \T_STRING)
  242. || ($tokens[$stackPtr]['code'] !== \T_CONST && $tokens[$start]['code'] !== \T_VARIABLE)
  243. ) {
  244. // Shouldn't be possible.
  245. continue;
  246. }
  247. if ($this->isValidAssignment($phpcsFile, $start, $end) === false) {
  248. // Create the "found" snippet.
  249. $content = '';
  250. $tokenCount = ($end - $start);
  251. if ($tokenCount < 20) {
  252. // Prevent large arrays from being added to the error message.
  253. $content = $phpcsFile->getTokensAsString($start, ($tokenCount + 1));
  254. }
  255. $this->throwError($phpcsFile, $start, $type, $content);
  256. }
  257. $start = $end;
  258. }
  259. // Skip to the end of the statement to prevent duplicate messages for multi-declarations.
  260. return $endOfStatement;
  261. }
  262. }
  263. /**
  264. * Is a value declared and is the value declared valid pre-PHP 5.6 ?
  265. *
  266. * @since 8.2.0
  267. *
  268. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  269. * @param int $stackPtr The position of the current token in the
  270. * stack passed in $tokens.
  271. * @param int $end The end of the value definition.
  272. * This will normally be a comma or semi-colon.
  273. *
  274. * @return bool
  275. */
  276. protected function isValidAssignment(File $phpcsFile, $stackPtr, $end)
  277. {
  278. $tokens = $phpcsFile->getTokens();
  279. $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $end, true);
  280. if ($next === false || $tokens[$next]['code'] !== \T_EQUAL) {
  281. // No value assigned.
  282. return true;
  283. }
  284. return $this->isStaticValue($phpcsFile, $tokens, ($next + 1), ($end - 1));
  285. }
  286. /**
  287. * Is a value declared and is the value declared constant as accepted in PHP 5.5 and lower ?
  288. *
  289. * @since 8.2.0
  290. *
  291. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  292. * @param array $tokens The token stack of the current file.
  293. * @param int $start The stackPtr from which to start examining.
  294. * @param int $end The end of the value definition (inclusive),
  295. * i.e. this token will be examined as part of
  296. * the snippet.
  297. * @param int $nestedArrays Optional. Array nesting level when examining
  298. * the content of an array.
  299. *
  300. * @return bool
  301. */
  302. protected function isStaticValue(File $phpcsFile, $tokens, $start, $end, $nestedArrays = 0)
  303. {
  304. $nextNonSimple = $phpcsFile->findNext($this->safeOperands, $start, ($end + 1), true);
  305. if ($nextNonSimple === false) {
  306. return true;
  307. }
  308. /*
  309. * OK, so we have at least one token which needs extra examination.
  310. */
  311. switch ($tokens[$nextNonSimple]['code']) {
  312. case \T_MINUS:
  313. case \T_PLUS:
  314. if ($this->isNumber($phpcsFile, $start, $end, true) !== false) {
  315. // Int or float with sign.
  316. return true;
  317. }
  318. return false;
  319. case \T_NAMESPACE:
  320. case \T_PARENT:
  321. case \T_SELF:
  322. case \T_DOUBLE_COLON:
  323. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
  324. if ($tokens[$nextNonSimple]['code'] === \T_NAMESPACE) {
  325. // Allow only `namespace\...`.
  326. if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_NS_SEPARATOR) {
  327. return false;
  328. }
  329. } elseif ($tokens[$nextNonSimple]['code'] === \T_PARENT
  330. || $tokens[$nextNonSimple]['code'] === \T_SELF
  331. ) {
  332. // Allow only `parent::` and `self::`.
  333. if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_DOUBLE_COLON) {
  334. return false;
  335. }
  336. } elseif ($tokens[$nextNonSimple]['code'] === \T_DOUBLE_COLON) {
  337. // Allow only `T_STRING::T_STRING`.
  338. if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_STRING) {
  339. return false;
  340. }
  341. $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($nextNonSimple - 1), null, true);
  342. // No need to worry about parent/self, that's handled above and
  343. // the double colon is skipped over in that case.
  344. if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== \T_STRING) {
  345. return false;
  346. }
  347. }
  348. // Examine what comes after the namespace/parent/self/double colon, if anything.
  349. return $this->isStaticValue($phpcsFile, $tokens, ($nextNonEmpty + 1), $end, $nestedArrays);
  350. case \T_ARRAY:
  351. case \T_OPEN_SHORT_ARRAY:
  352. ++$nestedArrays;
  353. $arrayItems = $this->getFunctionCallParameters($phpcsFile, $nextNonSimple);
  354. if (empty($arrayItems) === false) {
  355. foreach ($arrayItems as $item) {
  356. // Check for a double arrow, but only if it's for this array item, not for a nested array.
  357. $doubleArrow = false;
  358. $maybeDoubleArrow = $phpcsFile->findNext(
  359. array(\T_DOUBLE_ARROW, \T_ARRAY, \T_OPEN_SHORT_ARRAY),
  360. $item['start'],
  361. ($item['end'] + 1)
  362. );
  363. if ($maybeDoubleArrow !== false && $tokens[$maybeDoubleArrow]['code'] === \T_DOUBLE_ARROW) {
  364. // Double arrow is for this nesting level.
  365. $doubleArrow = $maybeDoubleArrow;
  366. }
  367. if ($doubleArrow === false) {
  368. if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], $item['end'], $nestedArrays) === false) {
  369. return false;
  370. }
  371. } else {
  372. // Examine array key.
  373. if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], ($doubleArrow - 1), $nestedArrays) === false) {
  374. return false;
  375. }
  376. // Examine array value.
  377. if ($this->isStaticValue($phpcsFile, $tokens, ($doubleArrow + 1), $item['end'], $nestedArrays) === false) {
  378. return false;
  379. }
  380. }
  381. }
  382. }
  383. --$nestedArrays;
  384. /*
  385. * Find the end of the array.
  386. * We already know we will have a valid closer as otherwise we wouldn't have been
  387. * able to get the array items.
  388. */
  389. $closer = ($nextNonSimple + 1);
  390. if ($tokens[$nextNonSimple]['code'] === \T_OPEN_SHORT_ARRAY
  391. && isset($tokens[$nextNonSimple]['bracket_closer']) === true
  392. ) {
  393. $closer = $tokens[$nextNonSimple]['bracket_closer'];
  394. } else {
  395. $maybeOpener = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
  396. if ($tokens[$maybeOpener]['code'] === \T_OPEN_PARENTHESIS) {
  397. $opener = $maybeOpener;
  398. if (isset($tokens[$opener]['parenthesis_closer']) === true) {
  399. $closer = $tokens[$opener]['parenthesis_closer'];
  400. }
  401. }
  402. }
  403. if ($closer === $end) {
  404. return true;
  405. }
  406. // Examine what comes after the array, if anything.
  407. return $this->isStaticValue($phpcsFile, $tokens, ($closer + 1), $end, $nestedArrays);
  408. }
  409. // Ok, so this unsafe token was not one of the exceptions, i.e. this is a PHP 5.6+ syntax.
  410. return false;
  411. }
  412. /**
  413. * Throw an error if a scalar expression is found.
  414. *
  415. * @since 8.2.0
  416. *
  417. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  418. * @param int $stackPtr The position of the token to link the error to.
  419. * @param string $type Type of usage found.
  420. * @param string $content Optional. The value for the declaration as found.
  421. *
  422. * @return void
  423. */
  424. protected function throwError(File $phpcsFile, $stackPtr, $type, $content = '')
  425. {
  426. $error = static::ERROR_PHRASE;
  427. $phrase = '';
  428. $errorCode = 'Found';
  429. if (isset($this->errorPhrases[$type]) === true) {
  430. $errorCode = $this->stringToErrorCode($type) . 'Found';
  431. $phrase = $this->errorPhrases[$type];
  432. }
  433. $data = array($phrase);
  434. if (empty($content) === false) {
  435. $error .= ' Found: %s';
  436. $data[] = $content;
  437. }
  438. $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
  439. }
  440. /**
  441. * Helper function to find the end of multi variable/constant declarations.
  442. *
  443. * Checks whether a certain part of a declaration needs to be skipped over or
  444. * if it is the real end of the declaration.
  445. *
  446. * @since 8.2.0
  447. *
  448. * @param array $tokens Token stack of the current file.
  449. * @param int $endPtr The token to examine as a candidate end pointer.
  450. * @param int $targetLevel Target nesting level.
  451. *
  452. * @return bool|int True if this is the real end. Int stackPtr to skip to if not.
  453. */
  454. private function isRealEndOfDeclaration($tokens, $endPtr, $targetLevel)
  455. {
  456. // Ignore anything within short array definition brackets for now.
  457. if ($tokens[$endPtr]['code'] === \T_OPEN_SHORT_ARRAY
  458. && (isset($tokens[$endPtr]['bracket_opener'])
  459. && $tokens[$endPtr]['bracket_opener'] === $endPtr)
  460. && isset($tokens[$endPtr]['bracket_closer'])
  461. ) {
  462. // Skip forward to the end of the short array definition.
  463. return $tokens[$endPtr]['bracket_closer'];
  464. }
  465. // Skip past comma's at a lower nesting level.
  466. if ($tokens[$endPtr]['code'] === \T_COMMA) {
  467. // Check if a comma is at the nesting level we're targetting.
  468. $nestingLevel = 0;
  469. if (isset($tokens[$endPtr]['nested_parenthesis']) === true) {
  470. $nestingLevel = \count($tokens[$endPtr]['nested_parenthesis']);
  471. }
  472. if ($nestingLevel > $targetLevel) {
  473. return $endPtr;
  474. }
  475. }
  476. return true;
  477. }
  478. }