ParameterTypeHintSniff.php
Namespace
SlevomatCodingStandard\Sniffs\TypeHintsFile
-
vendor/
slevomat/ coding-standard/ SlevomatCodingStandard/ Sniffs/ TypeHints/ ParameterTypeHintSniff.php
View source
<?php
declare (strict_types=1);
namespace SlevomatCodingStandard\Sniffs\TypeHints;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use SlevomatCodingStandard\Helpers\Annotation;
use SlevomatCodingStandard\Helpers\AnnotationHelper;
use SlevomatCodingStandard\Helpers\AnnotationTypeHelper;
use SlevomatCodingStandard\Helpers\DocCommentHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\FunctionHelper;
use SlevomatCodingStandard\Helpers\NamespaceHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\SuppressHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use SlevomatCodingStandard\Helpers\TypeHint;
use SlevomatCodingStandard\Helpers\TypeHintHelper;
use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function implode;
use function in_array;
use function lcfirst;
use function sprintf;
use function strtolower;
use const T_BITWISE_AND;
use const T_COMMA;
use const T_DOC_COMMENT_CLOSE_TAG;
use const T_DOC_COMMENT_OPEN_TAG;
use const T_DOC_COMMENT_STAR;
use const T_ELLIPSIS;
use const T_FUNCTION;
use const T_OPEN_PARENTHESIS;
use const T_VARIABLE;
class ParameterTypeHintSniff implements Sniff {
public const CODE_MISSING_ANY_TYPE_HINT = 'MissingAnyTypeHint';
public const CODE_MISSING_NATIVE_TYPE_HINT = 'MissingNativeTypeHint';
public const CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION = 'MissingTraversableTypeHintSpecification';
public const CODE_USELESS_ANNOTATION = 'UselessAnnotation';
public const CODE_USELESS_SUPPRESS = 'UselessSuppress';
private const NAME = 'SlevomatCodingStandard.TypeHints.ParameterTypeHint';
/** @var bool|null */
public $enableObjectTypeHint = null;
/** @var bool|null */
public $enableMixedTypeHint = null;
/** @var bool|null */
public $enableUnionTypeHint = null;
/** @var bool|null */
public $enableIntersectionTypeHint = null;
/** @var bool|null */
public $enableStandaloneNullTrueFalseTypeHints = null;
/** @var list<string> */
public $traversableTypeHints = [];
/** @var array<int, string>|null */
private $normalizedTraversableTypeHints;
/**
* @return array<int, (int|string)>
*/
public function register() : array {
return [
T_FUNCTION,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $functionPointer
*/
public function process(File $phpcsFile, $functionPointer) : void {
$this->enableObjectTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableObjectTypeHint, 70200);
$this->enableMixedTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableMixedTypeHint, 80000);
$this->enableUnionTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableUnionTypeHint, 80000);
$this->enableIntersectionTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableIntersectionTypeHint, 80100);
$this->enableStandaloneNullTrueFalseTypeHints = SniffSettingsHelper::isEnabledByPhpVersion($this->enableStandaloneNullTrueFalseTypeHints, 80200);
if (SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, self::NAME)) {
return;
}
if (DocCommentHelper::hasInheritdocAnnotation($phpcsFile, $functionPointer)) {
return;
}
$parametersTypeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $functionPointer);
$parametersAnnotations = FunctionHelper::getValidParametersAnnotations($phpcsFile, $functionPointer);
$prefixedParametersAnnotations = FunctionHelper::getValidPrefixedParametersAnnotations($phpcsFile, $functionPointer);
$this->checkTypeHints($phpcsFile, $functionPointer, $parametersTypeHints, $parametersAnnotations, $prefixedParametersAnnotations);
$this->checkTraversableTypeHintSpecification($phpcsFile, $functionPointer, $parametersTypeHints, $parametersAnnotations, $prefixedParametersAnnotations);
$this->checkUselessAnnotations($phpcsFile, $functionPointer, $parametersTypeHints, $parametersAnnotations);
}
/**
* @param array<string, TypeHint|null> $parametersTypeHints
* @param array<string, Annotation<VarTagValueNode>|Annotation<ParamTagValueNode>|Annotation<TypelessParamTagValueNode>> $parametersAnnotations
* @param array<string, Annotation<VarTagValueNode>|Annotation<ParamTagValueNode>> $prefixedParametersAnnotations
*/
private function checkTypeHints(File $phpcsFile, int $functionPointer, array $parametersTypeHints, array $parametersAnnotations, array $prefixedParametersAnnotations) : void {
$suppressNameAnyTypeHint = self::getSniffName(self::CODE_MISSING_ANY_TYPE_HINT);
$isSuppressedAnyTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressNameAnyTypeHint);
$suppressNameNativeTypeHint = $this->getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT);
$isSuppressedNativeTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressNameNativeTypeHint);
$suppressedErrors = 0;
$parametersWithoutTypeHint = array_keys(array_filter($parametersTypeHints, static function (?TypeHint $parameterTypeHint = null) : bool {
return $parameterTypeHint === null;
}));
$tokens = $phpcsFile->getTokens();
$isConstructor = FunctionHelper::isMethod($phpcsFile, $functionPointer) && strtolower(FunctionHelper::getName($phpcsFile, $functionPointer)) === '__construct';
foreach ($parametersWithoutTypeHint as $parameterName) {
$isPropertyPromotion = false;
if ($isConstructor) {
$parameterPointer = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $parameterName, $tokens[$functionPointer]['parenthesis_opener'], $tokens[$functionPointer]['parenthesis_closer']);
$pointerBeforeParameter = TokenHelper::findPrevious($phpcsFile, [
T_COMMA,
T_OPEN_PARENTHESIS,
], $parameterPointer - 1);
$visibilityPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBeforeParameter + 1);
$isPropertyPromotion = in_array($tokens[$visibilityPointer]['code'], Tokens::$scopeModifiers, true);
}
if (!array_key_exists($parameterName, $parametersAnnotations) || $parametersAnnotations[$parameterName]->getValue() instanceof TypelessParamTagValueNode) {
if (array_key_exists($parameterName, $prefixedParametersAnnotations)) {
continue;
}
if ($isSuppressedAnyTypeHint) {
$suppressedErrors++;
continue;
}
$phpcsFile->addError(sprintf('%s %s() does not have parameter type hint nor @param annotation for its parameter %s.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName), $functionPointer, self::CODE_MISSING_ANY_TYPE_HINT);
continue;
}
$parameterTypeNode = $parametersAnnotations[$parameterName]->getValue()->type;
if ($parameterTypeNode instanceof IdentifierTypeNode && strtolower($parameterTypeNode->name) === 'null' && !$this->enableStandaloneNullTrueFalseTypeHints) {
continue;
}
$originalParameterTypeNode = $parameterTypeNode;
if ($parameterTypeNode instanceof NullableTypeNode) {
$parameterTypeNode = $parameterTypeNode->type;
}
$canTryUnionTypeHint = $this->enableUnionTypeHint && $parameterTypeNode instanceof UnionTypeNode;
$typeHints = [];
$traversableTypeHints = [];
$nullableParameterTypeHint = false;
if (AnnotationTypeHelper::containsOneType($parameterTypeNode)) {
/** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode|ConstTypeNode $parameterTypeNode */
$parameterTypeNode = $parameterTypeNode;
$typeHints[] = AnnotationTypeHelper::getTypeHintFromOneType($parameterTypeNode, false, $this->enableStandaloneNullTrueFalseTypeHints);
}
elseif ($parameterTypeNode instanceof UnionTypeNode || $parameterTypeNode instanceof IntersectionTypeNode) {
$traversableTypeHints = [];
foreach ($parameterTypeNode->types as $typeNode) {
if (!AnnotationTypeHelper::containsOneType($typeNode)) {
continue 2;
}
/** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode|ConstTypeNode $typeNode */
$typeNode = $typeNode;
$typeHint = AnnotationTypeHelper::getTypeHintFromOneType($typeNode, $canTryUnionTypeHint);
if (strtolower($typeHint) === 'null') {
$nullableParameterTypeHint = true;
continue;
}
$isTraversable = TypeHintHelper::isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $typeHint), $this->getTraversableTypeHints());
if (!$typeNode instanceof ArrayTypeNode && !$typeNode instanceof ArrayShapeNode && $isTraversable) {
$traversableTypeHints[] = $typeHint;
}
$typeHints[] = $typeHint;
}
$traversableTypeHints = array_values(array_unique($traversableTypeHints));
if (count($traversableTypeHints) > 1 && !$canTryUnionTypeHint) {
continue;
}
}
$typeHints = array_values(array_unique($typeHints));
if (count($traversableTypeHints) > 0) {
/** @var UnionTypeNode|IntersectionTypeNode $parameterTypeNode */
$parameterTypeNode = $parameterTypeNode;
$itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($parameterTypeNode);
if ($itemsSpecificationTypeHint !== null) {
$typeHints = AnnotationTypeHelper::getTraversableTypeHintsFromType($parameterTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), $canTryUnionTypeHint);
}
}
if (count($typeHints) === 0) {
continue;
}
$typeHintsWithConvertedUnion = [];
foreach ($typeHints as $typeHint) {
if ($this->enableUnionTypeHint && TypeHintHelper::isUnofficialUnionTypeHint($typeHint)) {
$canTryUnionTypeHint = true;
$typeHintsWithConvertedUnion = array_merge($typeHintsWithConvertedUnion, TypeHintHelper::convertUnofficialUnionTypeHintToOfficialTypeHints($typeHint));
}
else {
$typeHintsWithConvertedUnion[] = $typeHint;
}
}
$typeHintsWithConvertedUnion = array_unique($typeHintsWithConvertedUnion);
if (count($typeHintsWithConvertedUnion) > 1 && ($parameterTypeNode instanceof UnionTypeNode && !$canTryUnionTypeHint || $parameterTypeNode instanceof IntersectionTypeNode && !$this->enableIntersectionTypeHint)) {
continue;
}
foreach ($typeHintsWithConvertedUnion as $typeHintNo => $typeHint) {
if ($canTryUnionTypeHint && $typeHint === 'false') {
continue;
}
if ($isPropertyPromotion && $typeHint === 'callable') {
continue 2;
}
if (!TypeHintHelper::isValidTypeHint($typeHint, $this->enableObjectTypeHint, false, $this->enableMixedTypeHint, $this->enableStandaloneNullTrueFalseTypeHints)) {
continue 2;
}
if (TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $functionPointer, $typeHint)) {
continue 2;
}
$typeHintsWithConvertedUnion[$typeHintNo] = TypeHintHelper::convertLongSimpleTypeHintToShort($typeHint);
}
if ($originalParameterTypeNode instanceof NullableTypeNode) {
$nullableParameterTypeHint = true;
}
if ($isSuppressedNativeTypeHint) {
$suppressedErrors++;
continue;
}
$fix = $phpcsFile->addFixableError(sprintf('%s %s() does not have native type hint for its parameter %s but it should be possible to add it based on @param annotation "%s".', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName, AnnotationTypeHelper::print($parameterTypeNode)), $functionPointer, self::CODE_MISSING_NATIVE_TYPE_HINT);
if (!$fix) {
continue;
}
if (in_array('mixed', $typeHintsWithConvertedUnion, true)) {
$parameterTypeHint = 'mixed';
}
elseif ($originalParameterTypeNode instanceof IntersectionTypeNode) {
$parameterTypeHint = implode('&', $typeHintsWithConvertedUnion);
}
else {
$parameterTypeHint = implode('|', $typeHintsWithConvertedUnion);
if ($nullableParameterTypeHint) {
if (count($typeHintsWithConvertedUnion) > 1) {
$parameterTypeHint .= '|null';
}
else {
$parameterTypeHint = '?' . $parameterTypeHint;
}
}
}
$tokens = $phpcsFile->getTokens();
/** @var int $parameterPointer */
$parameterPointer = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $parameterName, $tokens[$functionPointer]['parenthesis_opener'], $tokens[$functionPointer]['parenthesis_closer']);
$beforeParameterPointer = $parameterPointer;
do {
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $beforeParameterPointer - 1, $tokens[$functionPointer]['parenthesis_opener'] + 1);
if ($previousPointer === null || !in_array($tokens[$previousPointer]['code'], [
T_BITWISE_AND,
T_ELLIPSIS,
], true)) {
break;
}
/** @var int $beforeParameterPointer */
$beforeParameterPointer = $previousPointer;
} while (true);
$phpcsFile->fixer
->beginChangeset();
$phpcsFile->fixer
->addContentBefore($beforeParameterPointer, sprintf('%s ', $parameterTypeHint));
$phpcsFile->fixer
->endChangeset();
}
if ($suppressedErrors > 0) {
return;
}
if ($isSuppressedAnyTypeHint) {
$this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressNameAnyTypeHint);
}
if ($isSuppressedNativeTypeHint) {
$this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressNameNativeTypeHint);
}
}
/**
* @param array<string, TypeHint|null> $parametersTypeHints
* @param array<string, Annotation<VarTagValueNode>|Annotation<ParamTagValueNode>|Annotation<TypelessParamTagValueNode>> $parametersAnnotations
* @param array<string, Annotation<VarTagValueNode>|Annotation<ParamTagValueNode>> $prefixedParametersAnnotations
*/
private function checkTraversableTypeHintSpecification(File $phpcsFile, int $functionPointer, array $parametersTypeHints, array $parametersAnnotations, array $prefixedParametersAnnotations) : void {
$suppressName = self::getSniffName(self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION);
$isSniffSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressName);
$suppressUseless = true;
foreach ($parametersTypeHints as $parameterName => $parameterTypeHint) {
if (array_key_exists($parameterName, $prefixedParametersAnnotations)) {
continue;
}
$hasTraversableTypeHint = false;
if ($parameterTypeHint !== null && TypeHintHelper::isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $parameterTypeHint->getTypeHint()), $this->getTraversableTypeHints())) {
$hasTraversableTypeHint = true;
}
elseif (array_key_exists($parameterName, $parametersAnnotations) && !$parametersAnnotations[$parameterName]->getValue() instanceof TypelessParamTagValueNode && AnnotationTypeHelper::containsTraversableType($parametersAnnotations[$parameterName]->getValue()->type, $phpcsFile, $functionPointer, $this->getTraversableTypeHints())) {
$hasTraversableTypeHint = true;
}
if ($hasTraversableTypeHint && !array_key_exists($parameterName, $parametersAnnotations)) {
$suppressUseless = false;
if (!$isSniffSuppressed) {
$phpcsFile->addError(sprintf('%s %s() does not have @param annotation for its traversable parameter %s.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName), $functionPointer, self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION);
}
continue;
}
if (!array_key_exists($parameterName, $parametersAnnotations)) {
continue;
}
if ($parametersAnnotations[$parameterName]->getValue() instanceof TypelessParamTagValueNode) {
continue;
}
$parameterTypeNode = $parametersAnnotations[$parameterName]->getValue()->type;
if (!$hasTraversableTypeHint && !AnnotationTypeHelper::containsTraversableType($parameterTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints()) || AnnotationTypeHelper::containsItemsSpecificationForTraversable($parameterTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints())) {
continue;
}
$suppressUseless = false;
if ($isSniffSuppressed) {
continue;
}
$phpcsFile->addError(sprintf('@param annotation of %s %s() does not specify type hint for items of its traversable parameter %s.', lcfirst(FunctionHelper::getTypeLabel($phpcsFile, $functionPointer)), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName), $parametersAnnotations[$parameterName]->getStartPointer(), self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION);
}
if ($isSniffSuppressed && $suppressUseless) {
$this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressName);
}
}
/**
* @param array<string, TypeHint|null> $parametersTypeHints
* @param array<string, Annotation> $parametersAnnotations
*/
private function checkUselessAnnotations(File $phpcsFile, int $functionPointer, array $parametersTypeHints, array $parametersAnnotations) : void {
$suppressName = self::getSniffName(self::CODE_USELESS_ANNOTATION);
$isSniffSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressName);
$suppressUseless = true;
foreach ($parametersTypeHints as $parameterName => $parameterTypeHint) {
if (!array_key_exists($parameterName, $parametersAnnotations)) {
continue;
}
$parameterAnnotation = $parametersAnnotations[$parameterName];
if ($parameterAnnotation->getValue() instanceof TypelessParamTagValueNode) {
continue;
}
if (!AnnotationHelper::isAnnotationUseless($phpcsFile, $functionPointer, $parameterTypeHint, $parameterAnnotation, $this->getTraversableTypeHints(), $this->enableUnionTypeHint, $this->enableIntersectionTypeHint, $this->enableStandaloneNullTrueFalseTypeHints)) {
continue;
}
$suppressUseless = false;
if ($isSniffSuppressed) {
continue;
}
$fix = $phpcsFile->addFixableError(sprintf('%s %s() has useless @param annotation for parameter %s.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName), $parameterAnnotation->getStartPointer(), self::CODE_USELESS_ANNOTATION);
if (!$fix) {
continue;
}
$docCommentOpenPointer = $parameterAnnotation->getValue() instanceof VarTagValueNode ? TokenHelper::findPrevious($phpcsFile, T_DOC_COMMENT_OPEN_TAG, $parameterAnnotation->getStartPointer() - 1) : DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $functionPointer);
$starPointer = TokenHelper::findPrevious($phpcsFile, T_DOC_COMMENT_STAR, $parameterAnnotation->getStartPointer() - 1, $docCommentOpenPointer);
$changeStart = $starPointer ?? $parameterAnnotation->getStartPointer();
/** @var int $changeEnd */
$changeEnd = TokenHelper::findNext($phpcsFile, [
T_DOC_COMMENT_CLOSE_TAG,
T_DOC_COMMENT_STAR,
], $parameterAnnotation->getEndPointer()) - 1;
$phpcsFile->fixer
->beginChangeset();
FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd);
$phpcsFile->fixer
->endChangeset();
}
if ($isSniffSuppressed && $suppressUseless) {
$this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressName);
}
}
private function reportUselessSuppress(File $phpcsFile, int $pointer, string $suppressName) : void {
$fix = $phpcsFile->addFixableError(sprintf('Useless %s %s', SuppressHelper::ANNOTATION, $suppressName), $pointer, self::CODE_USELESS_SUPPRESS);
if ($fix) {
SuppressHelper::removeSuppressAnnotation($phpcsFile, $pointer, $suppressName);
}
}
private function getSniffName(string $sniffName) : string {
return sprintf('%s.%s', self::NAME, $sniffName);
}
/**
* @return list<string>
*/
private function getTraversableTypeHints() : array {
if ($this->normalizedTraversableTypeHints === null) {
$this->normalizedTraversableTypeHints = array_map(static function (string $typeHint) : string {
return NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint);
}, SniffSettingsHelper::normalizeArray($this->traversableTypeHints));
}
return $this->normalizedTraversableTypeHints;
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
ParameterTypeHintSniff |