123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- <?php
- /**
- * PHPCompatibility, an external standard for PHP_CodeSniffer.
- *
- * @package PHPCompatibility
- * @copyright 2012-2019 PHPCompatibility Contributors
- * @license https://opensource.org/licenses/LGPL-3.0 LGPL3
- * @link https://github.com/PHPCompatibility/PHPCompatibility
- */
- namespace PHPCompatibility\Sniffs\InitialValue;
- use PHPCompatibility\Sniff;
- use PHPCompatibility\PHPCSHelper;
- use PHP_CodeSniffer_File as File;
- use PHP_CodeSniffer_Tokens as Tokens;
- /**
- * Detect constant scalar expressions being used to set an initial value.
- *
- * Since PHP 5.6, it is now possible to provide a scalar expression involving
- * numeric and string literals and/or constants in contexts where PHP previously
- * expected a static value, such as constant and property declarations and
- * default values for function parameters.
- *
- * PHP version 5.6
- *
- * @link https://www.php.net/manual/en/migration56.new-features.php#migration56.new-features.const-scalar-exprs
- * @link https://wiki.php.net/rfc/const_scalar_exprs
- *
- * @since 8.2.0
- */
- class NewConstantScalarExpressionsSniff extends Sniff
- {
- /**
- * Error message.
- *
- * @since 8.2.0
- *
- * @var string
- */
- const ERROR_PHRASE = 'Constant scalar expressions are not allowed %s in PHP 5.5 or earlier.';
- /**
- * Partial error phrases to be used in combination with the error message constant.
- *
- * @since 8.2.0
- *
- * @var array
- */
- protected $errorPhrases = array(
- 'const' => 'when defining constants using the const keyword',
- 'property' => 'in property declarations',
- 'staticvar' => 'in static variable declarations',
- 'default' => 'in default function arguments',
- );
- /**
- * Tokens which were allowed to be used in these declarations prior to PHP 5.6.
- *
- * This list will be enriched in the setProperties() method.
- *
- * @since 8.2.0
- *
- * @var array
- */
- protected $safeOperands = array(
- \T_LNUMBER => \T_LNUMBER,
- \T_DNUMBER => \T_DNUMBER,
- \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
- \T_TRUE => \T_TRUE,
- \T_FALSE => \T_FALSE,
- \T_NULL => \T_NULL,
- \T_LINE => \T_LINE,
- \T_FILE => \T_FILE,
- \T_DIR => \T_DIR,
- \T_FUNC_C => \T_FUNC_C,
- \T_CLASS_C => \T_CLASS_C,
- \T_TRAIT_C => \T_TRAIT_C,
- \T_METHOD_C => \T_METHOD_C,
- \T_NS_C => \T_NS_C,
- // Special cases:
- \T_NS_SEPARATOR => \T_NS_SEPARATOR,
- /*
- * This can be neigh anything, but for any usage except constants,
- * the T_STRING will be combined with non-allowed tokens, so we should be good.
- */
- \T_STRING => \T_STRING,
- );
- /**
- * Returns an array of tokens this test wants to listen for.
- *
- * @since 8.2.0
- *
- * @return array
- */
- public function register()
- {
- // Set the properties up only once.
- $this->setProperties();
- return array(
- \T_CONST,
- \T_VARIABLE,
- \T_FUNCTION,
- \T_CLOSURE,
- \T_STATIC,
- );
- }
- /**
- * Make some adjustments to the $safeOperands property.
- *
- * @since 8.2.0
- *
- * @return void
- */
- public function setProperties()
- {
- $this->safeOperands += Tokens::$heredocTokens;
- $this->safeOperands += Tokens::$emptyTokens;
- }
- /**
- * Do a version check to determine if this sniff needs to run at all.
- *
- * @since 8.2.0
- *
- * @return bool
- */
- protected function bowOutEarly()
- {
- return ($this->supportsBelow('5.5') !== true);
- }
- /**
- * Processes this test, when one of its tokens is encountered.
- *
- * @since 8.2.0
- *
- * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
- * @param int $stackPtr The position of the current token in the
- * stack passed in $tokens.
- *
- * @return void|int Null or integer stack pointer to skip forward.
- */
- public function process(File $phpcsFile, $stackPtr)
- {
- if ($this->bowOutEarly() === true) {
- return;
- }
- $tokens = $phpcsFile->getTokens();
- switch ($tokens[$stackPtr]['type']) {
- case 'T_FUNCTION':
- case 'T_CLOSURE':
- $params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
- if (empty($params)) {
- // No parameters.
- return;
- }
- $funcToken = $tokens[$stackPtr];
- if (isset($funcToken['parenthesis_owner'], $funcToken['parenthesis_opener'], $funcToken['parenthesis_closer']) === false
- || $funcToken['parenthesis_owner'] !== $stackPtr
- || isset($tokens[$funcToken['parenthesis_opener']], $tokens[$funcToken['parenthesis_closer']]) === false
- ) {
- // Hmm.. something is going wrong as these should all be available & valid.
- return;
- }
- $opener = $funcToken['parenthesis_opener'];
- $closer = $funcToken['parenthesis_closer'];
- // Which nesting level is the one we are interested in ?
- $nestedParenthesisCount = 1;
- if (isset($tokens[$opener]['nested_parenthesis'])) {
- $nestedParenthesisCount += \count($tokens[$opener]['nested_parenthesis']);
- }
- foreach ($params as $param) {
- if (isset($param['default']) === false) {
- continue;
- }
- $end = $param['token'];
- while (($end = $phpcsFile->findNext(array(\T_COMMA, \T_CLOSE_PARENTHESIS), ($end + 1), ($closer + 1))) !== false) {
- $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $nestedParenthesisCount);
- if ($maybeSkipTo !== true) {
- $end = $maybeSkipTo;
- continue;
- }
- // Ignore closing parenthesis/bracket if not 'ours'.
- if ($tokens[$end]['code'] === \T_CLOSE_PARENTHESIS && $end !== $closer) {
- continue;
- }
- // Ok, we've found the end of the param default value declaration.
- break;
- }
- if ($this->isValidAssignment($phpcsFile, $param['token'], $end) === false) {
- $this->throwError($phpcsFile, $param['token'], 'default', $param['content']);
- }
- }
- /*
- * No need for the sniff to be triggered by the T_VARIABLEs in the function
- * definition as we've already examined them above, so let's skip over them.
- */
- return $closer;
- case 'T_VARIABLE':
- case 'T_STATIC':
- case 'T_CONST':
- $type = 'const';
- // Filter out non-property declarations.
- if ($tokens[$stackPtr]['code'] === \T_VARIABLE) {
- if ($this->isClassProperty($phpcsFile, $stackPtr) === false) {
- return;
- }
- $type = 'property';
- // Move back one token to have the same starting point as the others.
- $stackPtr = ($stackPtr - 1);
- }
- // Filter out late static binding and class properties.
- if ($tokens[$stackPtr]['code'] === \T_STATIC) {
- $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
- if ($next === false || $tokens[$next]['code'] !== \T_VARIABLE) {
- // Late static binding.
- return;
- }
- if ($this->isClassProperty($phpcsFile, $next) === true) {
- // Class properties are examined based on the T_VARIABLE token.
- return;
- }
- unset($next);
- $type = 'staticvar';
- }
- $endOfStatement = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($stackPtr + 1));
- if ($endOfStatement === false) {
- // No semi-colon - live coding.
- return;
- }
- $targetNestingLevel = 0;
- if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
- $targetNestingLevel = \count($tokens[$stackPtr]['nested_parenthesis']);
- }
- // Examine each variable/constant in multi-declarations.
- $start = $stackPtr;
- $end = $stackPtr;
- while (($end = $phpcsFile->findNext(array(\T_COMMA, \T_SEMICOLON, \T_OPEN_SHORT_ARRAY, \T_CLOSE_TAG), ($end + 1), ($endOfStatement + 1))) !== false) {
- $maybeSkipTo = $this->isRealEndOfDeclaration($tokens, $end, $targetNestingLevel);
- if ($maybeSkipTo !== true) {
- $end = $maybeSkipTo;
- continue;
- }
- $start = $phpcsFile->findNext(Tokens::$emptyTokens, ($start + 1), $end, true);
- if ($start === false
- || ($tokens[$stackPtr]['code'] === \T_CONST && $tokens[$start]['code'] !== \T_STRING)
- || ($tokens[$stackPtr]['code'] !== \T_CONST && $tokens[$start]['code'] !== \T_VARIABLE)
- ) {
- // Shouldn't be possible.
- continue;
- }
- if ($this->isValidAssignment($phpcsFile, $start, $end) === false) {
- // Create the "found" snippet.
- $content = '';
- $tokenCount = ($end - $start);
- if ($tokenCount < 20) {
- // Prevent large arrays from being added to the error message.
- $content = $phpcsFile->getTokensAsString($start, ($tokenCount + 1));
- }
- $this->throwError($phpcsFile, $start, $type, $content);
- }
- $start = $end;
- }
- // Skip to the end of the statement to prevent duplicate messages for multi-declarations.
- return $endOfStatement;
- }
- }
- /**
- * Is a value declared and is the value declared valid pre-PHP 5.6 ?
- *
- * @since 8.2.0
- *
- * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
- * @param int $stackPtr The position of the current token in the
- * stack passed in $tokens.
- * @param int $end The end of the value definition.
- * This will normally be a comma or semi-colon.
- *
- * @return bool
- */
- protected function isValidAssignment(File $phpcsFile, $stackPtr, $end)
- {
- $tokens = $phpcsFile->getTokens();
- $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $end, true);
- if ($next === false || $tokens[$next]['code'] !== \T_EQUAL) {
- // No value assigned.
- return true;
- }
- return $this->isStaticValue($phpcsFile, $tokens, ($next + 1), ($end - 1));
- }
- /**
- * Is a value declared and is the value declared constant as accepted in PHP 5.5 and lower ?
- *
- * @since 8.2.0
- *
- * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
- * @param array $tokens The token stack of the current file.
- * @param int $start The stackPtr from which to start examining.
- * @param int $end The end of the value definition (inclusive),
- * i.e. this token will be examined as part of
- * the snippet.
- * @param int $nestedArrays Optional. Array nesting level when examining
- * the content of an array.
- *
- * @return bool
- */
- protected function isStaticValue(File $phpcsFile, $tokens, $start, $end, $nestedArrays = 0)
- {
- $nextNonSimple = $phpcsFile->findNext($this->safeOperands, $start, ($end + 1), true);
- if ($nextNonSimple === false) {
- return true;
- }
- /*
- * OK, so we have at least one token which needs extra examination.
- */
- switch ($tokens[$nextNonSimple]['code']) {
- case \T_MINUS:
- case \T_PLUS:
- if ($this->isNumber($phpcsFile, $start, $end, true) !== false) {
- // Int or float with sign.
- return true;
- }
- return false;
- case \T_NAMESPACE:
- case \T_PARENT:
- case \T_SELF:
- case \T_DOUBLE_COLON:
- $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
- if ($tokens[$nextNonSimple]['code'] === \T_NAMESPACE) {
- // Allow only `namespace\...`.
- if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_NS_SEPARATOR) {
- return false;
- }
- } elseif ($tokens[$nextNonSimple]['code'] === \T_PARENT
- || $tokens[$nextNonSimple]['code'] === \T_SELF
- ) {
- // Allow only `parent::` and `self::`.
- if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_DOUBLE_COLON) {
- return false;
- }
- } elseif ($tokens[$nextNonSimple]['code'] === \T_DOUBLE_COLON) {
- // Allow only `T_STRING::T_STRING`.
- if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_STRING) {
- return false;
- }
- $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($nextNonSimple - 1), null, true);
- // No need to worry about parent/self, that's handled above and
- // the double colon is skipped over in that case.
- if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== \T_STRING) {
- return false;
- }
- }
- // Examine what comes after the namespace/parent/self/double colon, if anything.
- return $this->isStaticValue($phpcsFile, $tokens, ($nextNonEmpty + 1), $end, $nestedArrays);
- case \T_ARRAY:
- case \T_OPEN_SHORT_ARRAY:
- ++$nestedArrays;
- $arrayItems = $this->getFunctionCallParameters($phpcsFile, $nextNonSimple);
- if (empty($arrayItems) === false) {
- foreach ($arrayItems as $item) {
- // Check for a double arrow, but only if it's for this array item, not for a nested array.
- $doubleArrow = false;
- $maybeDoubleArrow = $phpcsFile->findNext(
- array(\T_DOUBLE_ARROW, \T_ARRAY, \T_OPEN_SHORT_ARRAY),
- $item['start'],
- ($item['end'] + 1)
- );
- if ($maybeDoubleArrow !== false && $tokens[$maybeDoubleArrow]['code'] === \T_DOUBLE_ARROW) {
- // Double arrow is for this nesting level.
- $doubleArrow = $maybeDoubleArrow;
- }
- if ($doubleArrow === false) {
- if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], $item['end'], $nestedArrays) === false) {
- return false;
- }
- } else {
- // Examine array key.
- if ($this->isStaticValue($phpcsFile, $tokens, $item['start'], ($doubleArrow - 1), $nestedArrays) === false) {
- return false;
- }
- // Examine array value.
- if ($this->isStaticValue($phpcsFile, $tokens, ($doubleArrow + 1), $item['end'], $nestedArrays) === false) {
- return false;
- }
- }
- }
- }
- --$nestedArrays;
- /*
- * Find the end of the array.
- * We already know we will have a valid closer as otherwise we wouldn't have been
- * able to get the array items.
- */
- $closer = ($nextNonSimple + 1);
- if ($tokens[$nextNonSimple]['code'] === \T_OPEN_SHORT_ARRAY
- && isset($tokens[$nextNonSimple]['bracket_closer']) === true
- ) {
- $closer = $tokens[$nextNonSimple]['bracket_closer'];
- } else {
- $maybeOpener = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonSimple + 1), ($end + 1), true);
- if ($tokens[$maybeOpener]['code'] === \T_OPEN_PARENTHESIS) {
- $opener = $maybeOpener;
- if (isset($tokens[$opener]['parenthesis_closer']) === true) {
- $closer = $tokens[$opener]['parenthesis_closer'];
- }
- }
- }
- if ($closer === $end) {
- return true;
- }
- // Examine what comes after the array, if anything.
- return $this->isStaticValue($phpcsFile, $tokens, ($closer + 1), $end, $nestedArrays);
- }
- // Ok, so this unsafe token was not one of the exceptions, i.e. this is a PHP 5.6+ syntax.
- return false;
- }
- /**
- * Throw an error if a scalar expression is found.
- *
- * @since 8.2.0
- *
- * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
- * @param int $stackPtr The position of the token to link the error to.
- * @param string $type Type of usage found.
- * @param string $content Optional. The value for the declaration as found.
- *
- * @return void
- */
- protected function throwError(File $phpcsFile, $stackPtr, $type, $content = '')
- {
- $error = static::ERROR_PHRASE;
- $phrase = '';
- $errorCode = 'Found';
- if (isset($this->errorPhrases[$type]) === true) {
- $errorCode = $this->stringToErrorCode($type) . 'Found';
- $phrase = $this->errorPhrases[$type];
- }
- $data = array($phrase);
- if (empty($content) === false) {
- $error .= ' Found: %s';
- $data[] = $content;
- }
- $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
- }
- /**
- * Helper function to find the end of multi variable/constant declarations.
- *
- * Checks whether a certain part of a declaration needs to be skipped over or
- * if it is the real end of the declaration.
- *
- * @since 8.2.0
- *
- * @param array $tokens Token stack of the current file.
- * @param int $endPtr The token to examine as a candidate end pointer.
- * @param int $targetLevel Target nesting level.
- *
- * @return bool|int True if this is the real end. Int stackPtr to skip to if not.
- */
- private function isRealEndOfDeclaration($tokens, $endPtr, $targetLevel)
- {
- // Ignore anything within short array definition brackets for now.
- if ($tokens[$endPtr]['code'] === \T_OPEN_SHORT_ARRAY
- && (isset($tokens[$endPtr]['bracket_opener'])
- && $tokens[$endPtr]['bracket_opener'] === $endPtr)
- && isset($tokens[$endPtr]['bracket_closer'])
- ) {
- // Skip forward to the end of the short array definition.
- return $tokens[$endPtr]['bracket_closer'];
- }
- // Skip past comma's at a lower nesting level.
- if ($tokens[$endPtr]['code'] === \T_COMMA) {
- // Check if a comma is at the nesting level we're targetting.
- $nestingLevel = 0;
- if (isset($tokens[$endPtr]['nested_parenthesis']) === true) {
- $nestingLevel = \count($tokens[$endPtr]['nested_parenthesis']);
- }
- if ($nestingLevel > $targetLevel) {
- return $endPtr;
- }
- }
- return true;
- }
- }
|