class RequireExplicitAssertionSniff
Hierarchy
- class \SlevomatCodingStandard\Sniffs\PHP\RequireExplicitAssertionSniff implements \PHP_CodeSniffer\Sniffs\Sniff
Expanded class hierarchy of RequireExplicitAssertionSniff
File
-
vendor/
slevomat/ coding-standard/ SlevomatCodingStandard/ Sniffs/ PHP/ RequireExplicitAssertionSniff.php, line 44
Namespace
SlevomatCodingStandard\Sniffs\PHPView source
class RequireExplicitAssertionSniff implements Sniff {
public const CODE_REQUIRED_EXPLICIT_ASSERTION = 'RequiredExplicitAssertion';
/** @var bool */
public $enableIntegerRanges = false;
/** @var bool */
public $enableAdvancedStringTypes = false;
/**
* @return array<int, (int|string)>
*/
public function register() : array {
return [
T_DOC_COMMENT_OPEN_TAG,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $docCommentOpenPointer
*/
public function process(File $phpcsFile, $docCommentOpenPointer) : void {
$tokens = $phpcsFile->getTokens();
$tokenCodes = [
T_VARIABLE,
T_FOREACH,
T_WHILE,
T_LIST,
T_OPEN_SHORT_ARRAY,
];
$commentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer'];
$codePointer = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $commentClosePointer);
if ($codePointer === null || !in_array($tokens[$codePointer]['code'], $tokenCodes, true)) {
$firstPointerOnPreviousLine = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $docCommentOpenPointer);
if ($firstPointerOnPreviousLine === null || !in_array($tokens[$firstPointerOnPreviousLine]['code'], $tokenCodes, true)) {
return;
}
$codePointer = $firstPointerOnPreviousLine;
}
/** @var list<Annotation<VarTagValueNode>> $variableAnnotations */
$variableAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer, '@var');
if (count($variableAnnotations) === 0) {
return;
}
foreach (array_reverse($variableAnnotations) as $variableAnnotation) {
if ($variableAnnotation->isInvalid()) {
continue;
}
$variableName = $variableAnnotation->getValue()->variableName;
if ($variableName === '') {
continue;
}
$variableAnnotationType = $variableAnnotation->getValue()->type;
if ($variableAnnotationType instanceof UnionTypeNode || $variableAnnotationType instanceof IntersectionTypeNode) {
foreach ($variableAnnotationType->types as $typeNode) {
if (!$this->isValidTypeNode($typeNode)) {
continue 2;
}
}
}
elseif (!$this->isValidTypeNode($variableAnnotationType)) {
continue;
}
/** @var IdentifierTypeNode|ThisTypeNode|UnionTypeNode|GenericTypeNode $variableAnnotationType */
$variableAnnotationType = $variableAnnotationType;
$assertion = $this->createAssert($variableName, $variableAnnotationType);
if ($assertion === null) {
continue;
}
if ($tokens[$codePointer]['code'] === T_VARIABLE) {
$pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1);
if ($tokens[$pointerAfterVariable]['code'] !== T_EQUAL) {
continue;
}
if ($variableName !== $tokens[$codePointer]['content']) {
continue;
}
$pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $codePointer + 1);
$indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer);
}
elseif ($tokens[$codePointer]['code'] === T_LIST) {
$listParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1);
$variablePointerInList = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableName, $listParenthesisOpener + 1, $tokens[$listParenthesisOpener]['parenthesis_closer']);
if ($variablePointerInList === null) {
continue;
}
$pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $codePointer + 1);
$indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer);
}
elseif ($tokens[$codePointer]['code'] === T_OPEN_SHORT_ARRAY) {
$pointerAfterList = TokenHelper::findNextEffective($phpcsFile, $tokens[$codePointer]['bracket_closer'] + 1);
if ($tokens[$pointerAfterList]['code'] !== T_EQUAL) {
continue;
}
$variablePointerInList = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableName, $codePointer + 1, $tokens[$codePointer]['bracket_closer']);
if ($variablePointerInList === null) {
continue;
}
$pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $tokens[$codePointer]['bracket_closer'] + 1);
$indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer);
}
else {
if ($tokens[$codePointer]['code'] === T_WHILE) {
$variablePointerInWhile = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableName, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer']);
if ($variablePointerInWhile === null) {
continue;
}
$pointerAfterVariableInWhile = TokenHelper::findNextEffective($phpcsFile, $variablePointerInWhile + 1);
if ($tokens[$pointerAfterVariableInWhile]['code'] !== T_EQUAL) {
continue;
}
}
else {
$asPointer = TokenHelper::findNext($phpcsFile, T_AS, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer']);
$variablePointerInForeach = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableName, $asPointer + 1, $tokens[$codePointer]['parenthesis_closer']);
if ($variablePointerInForeach === null) {
continue;
}
}
$pointerToAddAssertion = $tokens[$codePointer]['scope_opener'];
$indentation = IndentationHelper::addIndentation(IndentationHelper::getIndentation($phpcsFile, $codePointer));
}
$fix = $phpcsFile->addFixableError('Use assertion instead of inline documentation comment.', $variableAnnotation->getStartPointer(), self::CODE_REQUIRED_EXPLICIT_ASSERTION);
if (!$fix) {
continue;
}
$phpcsFile->fixer
->beginChangeset();
FixerHelper::removeBetweenIncluding($phpcsFile, $variableAnnotation->getStartPointer(), $variableAnnotation->getEndPointer());
$docCommentUseful = false;
$docCommentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer'];
for ($i = $docCommentOpenPointer + 1; $i < $docCommentClosePointer; $i++) {
$tokenContent = trim($phpcsFile->fixer
->getTokenContent($i));
if ($tokenContent === '' || $tokenContent === '*') {
continue;
}
$docCommentUseful = true;
break;
}
$pointerBeforeDocComment = TokenHelper::findPreviousContent($phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentOpenPointer - 1);
$pointerAfterDocComment = TokenHelper::findNextContent($phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentClosePointer + 1);
if (!$docCommentUseful) {
FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeDocComment + 1, $pointerAfterDocComment);
}
if ($pointerToAddAssertion < $docCommentClosePointer && array_key_exists($pointerAfterDocComment + 1, $tokens)) {
$phpcsFile->fixer
->addContentBefore($pointerAfterDocComment + 1, $indentation . $assertion . $phpcsFile->eolChar);
}
else {
$phpcsFile->fixer
->addContent($pointerToAddAssertion, $phpcsFile->eolChar . $indentation . $assertion);
}
$phpcsFile->fixer
->endChangeset();
}
}
private function isValidTypeNode(TypeNode $typeNode) : bool {
if ($typeNode instanceof ThisTypeNode) {
return true;
}
if ($typeNode instanceof IdentifierTypeNode) {
return true;
}
if ($this->enableIntegerRanges && $typeNode instanceof GenericTypeNode && $typeNode->type->name === 'int' && count($typeNode->genericTypes) === 2) {
foreach ($typeNode->genericTypes as $genericType) {
$isValid = $genericType instanceof IdentifierTypeNode && in_array($genericType->name, [
'min',
'max',
], true) || $genericType instanceof ConstTypeNode && $genericType->constExpr instanceof ConstExprIntegerNode;
if (!$isValid) {
return false;
}
}
return true;
}
return false;
}
private function getNextSemicolonInSameScope(File $phpcsFile, int $scopePointer, int $searchAt) : int {
$semicolonPointer = null;
do {
$semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $searchAt);
if (ScopeHelper::isInSameScope($phpcsFile, $scopePointer, $semicolonPointer)) {
break;
}
$searchAt = $semicolonPointer + 1;
} while (true);
return $semicolonPointer;
}
/**
* @param IdentifierTypeNode|ThisTypeNode|UnionTypeNode|IntersectionTypeNode|GenericTypeNode $typeNode
*/
private function createAssert(string $variableName, TypeNode $typeNode) : ?string {
$conditions = [];
if ($typeNode instanceof IdentifierTypeNode || $typeNode instanceof ThisTypeNode || $typeNode instanceof GenericTypeNode) {
$conditions = $this->createConditions($variableName, $typeNode);
return $conditions !== [] ? sprintf('\\assert(%s);', implode(' || ', $conditions)) : null;
}
/** @var IdentifierTypeNode|ThisTypeNode|GenericTypeNode $innerTypeNode */
foreach ($typeNode->types as $innerTypeNode) {
$innerTypeConditions = $this->createConditions($variableName, $innerTypeNode);
if ($innerTypeConditions === []) {
return null;
}
$conditions = array_merge($conditions, $innerTypeConditions);
}
$operator = $typeNode instanceof IntersectionTypeNode ? '&&' : '||';
$formattedConditions = [];
foreach (array_unique($conditions) as $condition) {
$formattedConditions[] = $operator === '||' && strpos($condition, '&&') !== false ? sprintf('(%s)', $condition) : $condition;
}
return sprintf('\\assert(%s);', implode(sprintf(' %s ', $operator), $formattedConditions));
}
/**
* @param IdentifierTypeNode|ThisTypeNode|GenericTypeNode $typeNode
* @return list<string>
*/
private function createConditions(string $variableName, TypeNode $typeNode) : array {
if ($typeNode instanceof GenericTypeNode) {
$conditions = [
sprintf('\\is_int(%s)', $variableName),
];
if ($typeNode->genericTypes[0] instanceof ConstTypeNode) {
$conditions[] = sprintf('%s >= %s', $variableName, (string) $typeNode->genericTypes[0]);
}
if ($typeNode->genericTypes[1] instanceof ConstTypeNode) {
$conditions[] = sprintf('%s <= %s', $variableName, (string) $typeNode->genericTypes[1]);
}
return [
implode(' && ', $conditions),
];
}
if ($typeNode instanceof ThisTypeNode) {
return [
sprintf('%s instanceof $this', $variableName),
];
}
if ($typeNode->name === 'self') {
return [
sprintf('%s instanceof %s', $variableName, $typeNode->name),
];
}
if ($typeNode->name === 'static') {
return [
sprintf('%s instanceof static', $variableName),
];
}
if (in_array($typeNode->name, [
'true',
'false',
'null',
], true)) {
return [
sprintf('%s === %s', $variableName, $typeNode->name),
];
}
if ($typeNode->name === 'mixed' || TypeHintHelper::isVoidTypeHint($typeNode->name) || TypeHintHelper::isNeverTypeHint($typeNode->name)) {
return [];
}
if (TypeHintHelper::isSimpleTypeHint($typeNode->name)) {
return [
sprintf('\\is_%s(%s)', TypeHintHelper::convertLongSimpleTypeHintToShort($typeNode->name), $variableName),
];
}
if (in_array($typeNode->name, [
'resource',
'object',
], true)) {
return [
sprintf('\\is_%s(%s)', $typeNode->name, $variableName),
];
}
if ($typeNode->name === 'numeric') {
return [
sprintf('\\is_int(%s)', $variableName),
sprintf('\\is_float(%s)', $variableName),
];
}
if ($typeNode->name === 'scalar') {
return [
sprintf('\\is_int(%s)', $variableName),
sprintf('\\is_float(%s)', $variableName),
sprintf('\\is_bool(%s)', $variableName),
sprintf('\\is_string(%s)', $variableName),
];
}
if ($this->enableIntegerRanges) {
if ($typeNode->name === 'positive-int') {
return [
sprintf('\\is_int(%1$s) && %1$s > 0', $variableName),
];
}
if ($typeNode->name === 'negative-int') {
return [
sprintf('\\is_int(%1$s) && %1$s < 0', $variableName),
];
}
}
if ($this->enableAdvancedStringTypes && in_array($typeNode->name, [
'non-empty-string',
'non-falsy-string',
'callable-string',
'numeric-string',
], true)) {
$conditions = [
sprintf('\\is_string(%s)', $variableName),
];
if ($typeNode->name === 'non-empty-string') {
$conditions[] = sprintf("%s !== ''", $variableName);
}
elseif ($typeNode->name === 'non-falsy-string') {
$conditions[] = sprintf('(bool) %s === true', $variableName);
}
elseif ($typeNode->name === 'callable-string') {
$conditions[] = sprintf('\\is_callable(%s)', $variableName);
}
else {
$conditions[] = sprintf('\\is_numeric(%s)', $variableName);
}
return [
implode(' && ', $conditions),
];
}
if (TypeHintHelper::isSimpleUnofficialTypeHints($typeNode->name)) {
return [];
}
return [
sprintf('%s instanceof %s', $variableName, $typeNode->name),
];
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overriden Title |
---|---|---|---|---|
RequireExplicitAssertionSniff::$enableAdvancedStringTypes | public | property | @var bool | |
RequireExplicitAssertionSniff::$enableIntegerRanges | public | property | @var bool | |
RequireExplicitAssertionSniff::CODE_REQUIRED_EXPLICIT_ASSERTION | public | constant | ||
RequireExplicitAssertionSniff::createAssert | private | function | * | |
RequireExplicitAssertionSniff::createConditions | private | function | * | |
RequireExplicitAssertionSniff::getNextSemicolonInSameScope | private | function | ||
RequireExplicitAssertionSniff::isValidTypeNode | private | function | ||
RequireExplicitAssertionSniff::process | public | function | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * |
Overrides Sniff::process |
RequireExplicitAssertionSniff::register | public | function | * | Overrides Sniff::register |