573 lines
17 KiB
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);
|
|
}
|
|
|
|
}
|