google_forms/vendor/slevomat/coding-standard/SlevomatCodingStandard/Helpers/FunctionHelper.php

573 lines
17 KiB
PHP

<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Helpers;
use Generator;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use function array_filter;
use function array_map;
use function array_merge;
use function array_pop;
use function array_reverse;
use function in_array;
use function iterator_to_array;
use function preg_match;
use function preg_replace;
use function sprintf;
use function substr_count;
use const T_ANON_CLASS;
use const T_BITWISE_AND;
use const T_CLASS;
use const T_CLOSURE;
use const T_COLON;
use const T_ELLIPSIS;
use const T_ENUM;
use const T_FUNCTION;
use const T_INTERFACE;
use const T_NULLABLE;
use const T_RETURN;
use const T_SEMICOLON;
use const T_STRING;
use const T_TRAIT;
use const T_USE;
use const T_VARIABLE;
use const T_WHITESPACE;
use const T_YIELD;
use const T_YIELD_FROM;
/**
* @internal
*/
class FunctionHelper
{
public const LINE_INCLUDE_COMMENT = 1;
public const LINE_INCLUDE_WHITESPACE = 2;
public const SPECIAL_FUNCTIONS = [
'array_key_exists',
'array_slice',
'assert',
'boolval',
'call_user_func',
'call_user_func_array',
'chr',
'constant',
'count',
'define',
'defined',
'dirname',
'doubleval',
'extension_loaded',
'floatval',
'func_get_args',
'func_num_args',
'function_exists',
'get_called_class',
'get_class',
'gettype',
'in_array',
'ini_get',
'intval',
'is_array',
'is_bool',
'is_callable',
'is_double',
'is_float',
'is_int',
'is_integer',
'is_long',
'is_null',
'is_object',
'is_real',
'is_resource',
'is_scalar',
'is_string',
'ord',
'sizeof',
'strlen',
'strval',
];
public static function getTypeLabel(File $phpcsFile, int $functionPointer): string
{
return self::isMethod($phpcsFile, $functionPointer) ? 'Method' : 'Function';
}
public static function getName(File $phpcsFile, int $functionPointer): string
{
$tokens = $phpcsFile->getTokens();
return $tokens[TokenHelper::findNext(
$phpcsFile,
T_STRING,
$functionPointer + 1,
$tokens[$functionPointer]['parenthesis_opener']
)]['content'];
}
public static function getFullyQualifiedName(File $phpcsFile, int $functionPointer): string
{
$name = self::getName($phpcsFile, $functionPointer);
$namespace = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $functionPointer);
if (self::isMethod($phpcsFile, $functionPointer)) {
foreach (array_reverse(
$phpcsFile->getTokens()[$functionPointer]['conditions'],
true
) as $conditionPointer => $conditionTokenCode) {
if ($conditionTokenCode === T_ANON_CLASS) {
return sprintf('class@anonymous::%s', $name);
}
if (in_array($conditionTokenCode, [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) {
$name = sprintf(
'%s%s::%s',
NamespaceHelper::NAMESPACE_SEPARATOR,
ClassHelper::getName($phpcsFile, $conditionPointer),
$name
);
break;
}
}
return $namespace !== null ? sprintf('%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, $name) : $name;
}
return $namespace !== null
? sprintf('%s%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name)
: $name;
}
public static function isAbstract(File $phpcsFile, int $functionPointer): bool
{
return !isset($phpcsFile->getTokens()[$functionPointer]['scope_opener']);
}
public static function isMethod(File $phpcsFile, int $functionPointer): bool
{
$functionPointerConditions = $phpcsFile->getTokens()[$functionPointer]['conditions'];
if ($functionPointerConditions === []) {
return false;
}
$lastFunctionPointerCondition = array_pop($functionPointerConditions);
return in_array($lastFunctionPointerCondition, Tokens::$ooScopeTokens, true);
}
public static function findClassPointer(File $phpcsFile, int $functionPointer): ?int
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$functionPointer]['code'] === T_CLOSURE) {
return null;
}
foreach (array_reverse($tokens[$functionPointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) {
if (!in_array($conditionTokenCode, Tokens::$ooScopeTokens, true)) {
continue;
}
return $conditionPointer;
}
return null;
}
/**
* @return list<string>
*/
public static function getParametersNames(File $phpcsFile, int $functionPointer): array
{
$tokens = $phpcsFile->getTokens();
$parametersNames = [];
for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE) {
continue;
}
$parametersNames[] = $tokens[$i]['content'];
}
return $parametersNames;
}
/**
* @return array<string, TypeHint|null>
*/
public static function getParametersTypeHints(File $phpcsFile, int $functionPointer): array
{
$tokens = $phpcsFile->getTokens();
$parametersTypeHints = [];
for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE) {
continue;
}
$parameterName = $tokens[$i]['content'];
$pointerBeforeVariable = TokenHelper::findPreviousExcluding(
$phpcsFile,
array_merge(TokenHelper::$ineffectiveTokenCodes, [T_BITWISE_AND, T_ELLIPSIS]),
$i - 1
);
if (!in_array($tokens[$pointerBeforeVariable]['code'], TokenHelper::getTypeHintTokenCodes(), true)) {
$parametersTypeHints[$parameterName] = null;
continue;
}
$typeHintEndPointer = $pointerBeforeVariable;
$typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer);
$pointerBeforeTypeHint = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1);
$isNullable = $tokens[$pointerBeforeTypeHint]['code'] === T_NULLABLE;
if ($isNullable) {
$typeHintStartPointer = $pointerBeforeTypeHint;
}
$typeHint = TokenHelper::getContent($phpcsFile, $typeHintStartPointer, $typeHintEndPointer);
/** @var string $typeHint */
$typeHint = preg_replace('~\s+~', '', $typeHint);
if (!$isNullable) {
$isNullable = preg_match('~(?:^|\|)null(?:\||$)~i', $typeHint) === 1;
}
$parametersTypeHints[$parameterName] = new TypeHint($typeHint, $isNullable, $typeHintStartPointer, $typeHintEndPointer);
}
return $parametersTypeHints;
}
public static function returnsValue(File $phpcsFile, int $functionPointer): bool
{
$tokens = $phpcsFile->getTokens();
$firstPointerInScope = $tokens[$functionPointer]['scope_opener'] + 1;
for ($i = $firstPointerInScope; $i < $tokens[$functionPointer]['scope_closer']; $i++) {
if (!in_array($tokens[$i]['code'], [T_YIELD, T_YIELD_FROM], true)) {
continue;
}
if (!ScopeHelper::isInSameScope($phpcsFile, $i, $firstPointerInScope)) {
continue;
}
return true;
}
for ($i = $firstPointerInScope; $i < $tokens[$functionPointer]['scope_closer']; $i++) {
if ($tokens[$i]['code'] !== T_RETURN) {
continue;
}
if (!ScopeHelper::isInSameScope($phpcsFile, $i, $firstPointerInScope)) {
continue;
}
$nextEffectiveTokenPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1);
return $tokens[$nextEffectiveTokenPointer]['code'] !== T_SEMICOLON;
}
return false;
}
public static function findReturnTypeHint(File $phpcsFile, int $functionPointer): ?TypeHint
{
$tokens = $phpcsFile->getTokens();
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$functionPointer]['parenthesis_closer'] + 1);
if ($tokens[$nextPointer]['code'] === T_USE) {
$useParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $nextPointer + 1);
$colonPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$useParenthesisOpener]['parenthesis_closer'] + 1);
} else {
$colonPointer = $nextPointer;
}
if ($tokens[$colonPointer]['code'] !== T_COLON) {
return null;
}
$typeHintStartPointer = TokenHelper::findNextEffective($phpcsFile, $colonPointer + 1);
$nullable = $tokens[$typeHintStartPointer]['code'] === T_NULLABLE;
$pointerAfterTypeHint = self::isAbstract($phpcsFile, $functionPointer)
? TokenHelper::findNext($phpcsFile, T_SEMICOLON, $typeHintStartPointer + 1)
: $tokens[$functionPointer]['scope_opener'];
$typeHintEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointerAfterTypeHint - 1);
$typeHint = TokenHelper::getContent($phpcsFile, $typeHintStartPointer, $typeHintEndPointer);
/** @var string $typeHint */
$typeHint = preg_replace('~\s+~', '', $typeHint);
if (!$nullable) {
$nullable = preg_match('~(?:^|\|)null(?:\||$)~i', $typeHint) === 1;
}
return new TypeHint($typeHint, $nullable, $typeHintStartPointer, $typeHintEndPointer);
}
public static function hasReturnTypeHint(File $phpcsFile, int $functionPointer): bool
{
return self::findReturnTypeHint($phpcsFile, $functionPointer) !== null;
}
/**
* @return list<Annotation<ParamTagValueNode>|Annotation<TypelessParamTagValueNode>>
*/
public static function getParametersAnnotations(File $phpcsFile, int $functionPointer): array
{
return AnnotationHelper::getAnnotations($phpcsFile, $functionPointer, '@param');
}
/**
* @return array<string, Annotation<VarTagValueNode>|Annotation<ParamTagValueNode>|Annotation<TypelessParamTagValueNode>>
*/
public static function getValidParametersAnnotations(File $phpcsFile, int $functionPointer): array
{
$tokens = $phpcsFile->getTokens();
$parametersAnnotations = [];
if (self::getName($phpcsFile, $functionPointer) === '__construct') {
for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE) {
continue;
}
$varAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $i, '@var');
if ($varAnnotations === []) {
continue;
}
$parametersAnnotations[$tokens[$i]['content']] = $varAnnotations[0];
}
}
foreach (self::getParametersAnnotations($phpcsFile, $functionPointer) as $parameterAnnotation) {
if ($parameterAnnotation->isInvalid()) {
continue;
}
$parametersAnnotations[$parameterAnnotation->getValue()->parameterName] = $parameterAnnotation;
}
return $parametersAnnotations;
}
/**
* @return array<string, Annotation<VarTagValueNode>|Annotation<ParamTagValueNode>>
*/
public static function getValidPrefixedParametersAnnotations(File $phpcsFile, int $functionPointer): array
{
$tokens = $phpcsFile->getTokens();
$parametersAnnotations = [];
foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) {
if (self::getName($phpcsFile, $functionPointer) === '__construct') {
for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] !== T_VARIABLE) {
continue;
}
/** @var list<Annotation<VarTagValueNode>> $varAnnotations */
$varAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $i, sprintf('@%s-var', $prefix));
if ($varAnnotations === []) {
continue;
}
$parametersAnnotations[$tokens[$i]['content']] = $varAnnotations[0];
}
}
/** @var list<Annotation<ParamTagValueNode>> $annotations */
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $functionPointer, sprintf('@%s-param', $prefix));
foreach ($annotations as $parameterAnnotation) {
if ($parameterAnnotation->isInvalid()) {
continue;
}
$parametersAnnotations[$parameterAnnotation->getValue()->parameterName] = $parameterAnnotation;
}
}
return $parametersAnnotations;
}
/**
* @return Annotation<ReturnTagValueNode>|null
*/
public static function findReturnAnnotation(File $phpcsFile, int $functionPointer): ?Annotation
{
/** @var list<Annotation<ReturnTagValueNode>> $returnAnnotations */
$returnAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $functionPointer, '@return');
if ($returnAnnotations === []) {
return null;
}
return $returnAnnotations[0];
}
/**
* @return list<Annotation>
*/
public static function getValidPrefixedReturnAnnotations(File $phpcsFile, int $functionPointer): array
{
$returnAnnotations = [];
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $functionPointer);
foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) {
$prefixedAnnotationName = sprintf('@%s-return', $prefix);
foreach ($annotations as $annotation) {
if ($annotation->isInvalid()) {
continue;
}
if ($annotation->getName() === $prefixedAnnotationName) {
$returnAnnotations[] = $annotation;
}
}
}
return $returnAnnotations;
}
/**
* @return list<string>
*/
public static function getAllFunctionNames(File $phpcsFile): array
{
$previousFunctionPointer = 0;
return array_map(
static function (int $functionPointer) use ($phpcsFile): string {
return self::getName($phpcsFile, $functionPointer);
},
array_filter(
iterator_to_array(self::getAllFunctionOrMethodPointers($phpcsFile, $previousFunctionPointer)),
static function (int $functionOrMethodPointer) use ($phpcsFile): bool {
return !self::isMethod($phpcsFile, $functionOrMethodPointer);
}
)
);
}
/**
* @param int $flags optional bitmask of self::LINE_INCLUDE_* constants
*/
public static function getFunctionLengthInLines(File $file, int $functionPosition, int $flags = 0): int
{
if (self::isAbstract($file, $functionPosition)) {
return 0;
}
return self::getLineCount($file, $functionPosition, $flags);
}
public static function getLineCount(File $file, int $tokenPosition, int $flags = 0): int
{
$includeWhitespace = ($flags & self::LINE_INCLUDE_WHITESPACE) === self::LINE_INCLUDE_WHITESPACE;
$includeComments = ($flags & self::LINE_INCLUDE_COMMENT) === self::LINE_INCLUDE_COMMENT;
$tokens = $file->getTokens();
$token = $tokens[$tokenPosition];
$tokenOpenerPosition = $token['scope_opener'] ?? $tokenPosition;
$tokenCloserPosition = $token['scope_closer'] ?? $file->numTokens - 1;
$tokenOpenerLine = $tokens[$tokenOpenerPosition]['line'];
$tokenCloserLine = $tokens[$tokenCloserPosition]['line'];
$lineCount = 0;
$lastCommentLine = null;
$previousIncludedPosition = null;
for ($position = $tokenOpenerPosition; $position <= $tokenCloserPosition - 1; $position++) {
$token = $tokens[$position];
if ($includeComments === false) {
if (in_array($token['code'], Tokens::$commentTokens, true)) {
if (
$previousIncludedPosition !== null &&
substr_count($token['content'], $file->eolChar) > 0 &&
$token['line'] === $tokens[$previousIncludedPosition]['line']
) {
// Comment with linebreak starting on same line as included Token
$lineCount++;
}
// Don't include comment
$lastCommentLine = $token['line'];
continue;
}
if (
$previousIncludedPosition !== null &&
$token['code'] === T_WHITESPACE &&
$token['line'] === $lastCommentLine &&
$token['line'] !== $tokens[$previousIncludedPosition]['line']
) {
// Whitespace after block comment... still on comment line...
// Ignore along with the comment
continue;
}
}
if ($token['code'] === T_WHITESPACE) {
$nextNonWhitespacePosition = $file->findNext(T_WHITESPACE, $position + 1, $tokenCloserPosition + 1, true);
if (
$includeWhitespace === false &&
$token['column'] === 1 &&
$nextNonWhitespacePosition !== false &&
$tokens[$nextNonWhitespacePosition]['line'] !== $token['line']
) {
// This line is nothing but whitepace
$position = $nextNonWhitespacePosition - 1;
continue;
}
if ($previousIncludedPosition === $tokenOpenerPosition && $token['line'] === $tokenOpenerLine) {
// Don't linclude line break after opening "{"
// Unless there was code or an (included) comment following the "{"
continue;
}
}
if ($token['code'] !== T_WHITESPACE) {
$previousIncludedPosition = $position;
}
$newLineFoundCount = substr_count($token['content'], $file->eolChar);
$lineCount += $newLineFoundCount;
}
if ($tokens[$previousIncludedPosition]['line'] === $tokenCloserLine) {
// There is code or comment on the closing "}" line...
$lineCount++;
}
return $lineCount;
}
/**
* @return Generator<int>
*/
private static function getAllFunctionOrMethodPointers(File $phpcsFile, int &$previousFunctionPointer): Generator
{
do {
$nextFunctionPointer = TokenHelper::findNext($phpcsFile, T_FUNCTION, $previousFunctionPointer + 1);
if ($nextFunctionPointer === null) {
break;
}
$previousFunctionPointer = $nextFunctionPointer;
yield $nextFunctionPointer;
} while (true);
}
}