'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; } }