*/ protected $targetFunctions = [ 'exit' => true, 'die' => true, ]; /** * All constants natively declared by PHP. * * @since 10.0.0 * * @var array */ private $phpNativeConstants = []; /** * Current file being scanned. * * @since 10.0.0 * * @var string */ private $currentFile = ''; /** * Whether strict types are in effect in the current file. * * @since 10.0.0 * * @var bool */ private $strictTypes = false; /** * Returns an array of tokens this test wants to listen for. * * @since 10.0.0 * * @return array */ public function register() { // Get the PHP natively defined constants only once. $constants = \get_defined_constants(true); unset($constants['user']); $this->phpNativeConstants = []; foreach ($constants as $group) { $this->phpNativeConstants += $group; } // Call the parent method to set up some properties for the abstract. parent::register(); // ... but register our own target tokens. return [ \T_DECLARE, \T_EXIT, ]; } /** * Processes this test, when one of its tokens is encountered. * * @since 10.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token in * the stack passed in $tokens. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process(File $phpcsFile, $stackPtr) { if ($this->bowOutEarly() === true) { return; } $fileName = $phpcsFile->getFilename(); if ($this->currentFile !== $fileName) { // Reset the declare statement related properties for each new file. $this->currentFile = $fileName; $this->strictTypes = false; } /* * Check for strict types declarations. * * Ignore any invalid/incomplete declare statements. */ $tokens = $phpcsFile->getTokens(); if ($tokens[$stackPtr]['code'] === \T_DECLARE) { if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) { // Live coding or parse error. return; } $declarations = GetTokensAsString::noEmpties( $phpcsFile, ($tokens[$stackPtr]['parenthesis_opener'] + 1), ($tokens[$stackPtr]['parenthesis_closer'] - 1) ); if (\preg_match('`\bstrict_types=([01])`i', $declarations, $matches) === 1) { if ($matches[1] === '1') { $this->strictTypes = true; } else { $this->strictTypes = false; } } return; } // Check if this is exit/die used as a fully qualified function call. $isFullyQualified = false; if ($tokens[$stackPtr]['content'][0] === '\\') { // PHPCS 4.x. $isFullyQualified = true; } else { // PHPCS 3.x. $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); if ($tokens[$prev]['code'] === \T_NS_SEPARATOR) { $isFullyQualified = true; } } if ($isFullyQualified === true) { $phpcsFile->addError( 'Using "%s" as a fully qualified function call is not allowed in PHP 8.3 or earlier.', $stackPtr, 'FullyQualified', [\ltrim($tokens[$stackPtr]['content'], '\\')] ); } return parent::process($phpcsFile, $stackPtr); } /** * Do a version check to determine if this sniff needs to run at all. * * @since 10.0.0 * * @return bool */ protected function bowOutEarly() { return (ScannedCode::shouldRunOnOrAbove('8.4') === false); } /** * Process the parameters of a matched function. * * @since 10.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token in the stack. * @param string $functionName The token content (function name) which was matched. * @param array> $parameters Array with information about the parameters. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters) { $targetParam = PassedParameters::getParameterFromStack($parameters, 1, 'status'); if ($targetParam === false) { return; } $tokens = $phpcsFile->getTokens(); $data = [$functionName, $targetParam['clean']]; $integer = 0; $string = 0; $boolean = 0; $float = 0; $null = 0; $concat = 0; $arithm = 0; $total = 0; for ($i = $targetParam['start']; $i <= $targetParam['end']; $i++) { if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) || $tokens[$i]['code'] === \T_NS_SEPARATOR) { continue; } if (($tokens[$i]['code'] === \T_INT_CAST || $tokens[$i]['code'] === \T_STRING_CAST) && $total === 0 ) { // Assume the cast is for the whole parameter, in which case, we're good. return; } if ($tokens[$i]['code'] === \T_NEW) { // For objects, there is no change in behaviour. This was already a type error, // or, in case of a stingable object, was okay and is still okay. return; } // Check for use of PHP native global constants for which we know the type. $trimmedContent = \ltrim($tokens[$i]['content'], '\\'); if (($tokens[$i]['code'] === \T_STRING || $tokens[$i]['code'] === \T_NAME_FULLY_QUALIFIED) && isset($this->phpNativeConstants[$trimmedContent]) === true && MiscHelper::isUseOfGlobalConstant($phpcsFile, $i) === true ) { $type = \gettype($this->phpNativeConstants[$trimmedContent]); switch ($type) { case 'integer': ++$integer; break; case 'string': ++$string; break; case 'double': ++$float; break; case 'boolean': ++$boolean; break; // At this time, PHP doesn't have any native constants of type null. // @codeCoverageIgnoreStart case 'null': ++$null; break; // @codeCoverageIgnoreEnd default: $this->flagTypeError($phpcsFile, $i, $data, $type); return; } ++$total; continue; } if (isset(Collections::nameTokens()[$tokens[$i]['code']]) === true || $tokens[$i]['code'] === \T_VARIABLE ) { // Variable, non-PHP-native constant, function call. Ignore as undetermined. return; } if (($tokens[$i]['code'] === \T_ARRAY || $tokens[$i]['code'] === \T_LIST || $tokens[$i]['code'] === \T_OPEN_SHORT_ARRAY) && $total === 0 // Only flag when the parameter starts with one of these tokens. ) { $this->flagTypeError($phpcsFile, $i, $data, 'array'); return; } ++$total; if ($tokens[$i]['code'] === \T_LNUMBER) { ++$integer; continue; } if (isset(Tokens::$arithmeticTokens[$tokens[$i]['code']])) { ++$arithm; continue; } if (isset(Tokens::$textStringTokens[$tokens[$i]['code']]) || isset(Tokens::$heredocTokens[$tokens[$i]['code']]) ) { ++$string; continue; } if ($tokens[$i]['code'] === \T_STRING_CONCAT) { ++$concat; continue; } if ($tokens[$i]['code'] === \T_DNUMBER) { ++$float; continue; } if ($tokens[$i]['code'] === \T_TRUE || $tokens[$i]['code'] === \T_FALSE) { ++$boolean; continue; } if ($tokens[$i]['code'] === \T_NULL) { ++$null; continue; } } $unrecognized = ($total - $integer - $string - $boolean - $float - $null - $concat - $arithm); if ($unrecognized > 0) { // Ignore as undetermined. return; } if (($integer > 0 && ($total - $integer - $arithm) === 0) || ($string > 0 && ($total - $string - $concat - $integer) === 0) ) { // This is fine, either a purely integer value, a purely string value or a simple operation involving only strings/integers. // No change in behaviour. return; } if ($boolean > 0 && ($total - $boolean) === 0) { if ($this->strictTypes === true) { $this->flagTypeError($phpcsFile, $i, $data, 'boolean'); return; } $phpcsFile->addWarning( 'Passing a boolean value to %s() will be interpreted as an exit code instead of as a status message since PHP 8.4. Found: "%s"', $i, 'BooleanParamFound', $data ); return; } if ($float > 0 && ($total - $float - $arithm - $integer) === 0) { if ($this->strictTypes === true) { $this->flagTypeError($phpcsFile, $i, $data, 'float'); return; } $phpcsFile->addWarning( 'Passing a floating point value to %s() will be interpreted as an exit code instead of as a status message since PHP 8.4. Found: "%s"', $i, 'FloatParamFound', $data ); return; } if ($null > 0 && ($total - $null) === 0) { if ($this->strictTypes === true) { $this->flagTypeError($phpcsFile, $i, $data, 'null'); return; } $phpcsFile->addWarning( 'Passing null to %s() will be interpreted as an exit code instead of as a status message since PHP 8.4. Found: "%s"', $i, 'NullParamFound', $data ); return; } // Ignore everything else as undetermined. } /** * Throw an error about a received parameter type which will be a type error as of PHP 8.4. * * @since 10.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The token position to throw the error on. * @param array $data The data for the error message. * @param string $type The inferred parameter type. * * @return void */ private function flagTypeError(File $phpcsFile, $stackPtr, $data, $type) { $aOrAn = 'a '; if ($type === 'null') { $aOrAn = ''; } elseif ($type === 'array') { $aOrAn = 'an '; } $data[] = $aOrAn; $data[] = $type; $phpcsFile->addError( 'Passing %3$s%4$s to %1$s() will result in a TypeError since PHP 8.4. Found: "%2$s"', $stackPtr, 'TypeError', $data ); } }