Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. RequireExplicitAssertionSniff.php

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\PHP
View 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
RSS feed
Powered by Drupal