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

Breadcrumb

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

class InlineDocCommentDeclarationSniff

Hierarchy

  • class \SlevomatCodingStandard\Sniffs\Commenting\InlineDocCommentDeclarationSniff implements \PHP_CodeSniffer\Sniffs\Sniff

Expanded class hierarchy of InlineDocCommentDeclarationSniff

File

vendor/slevomat/coding-standard/SlevomatCodingStandard/Sniffs/Commenting/InlineDocCommentDeclarationSniff.php, line 41

Namespace

SlevomatCodingStandard\Sniffs\Commenting
View source
class InlineDocCommentDeclarationSniff implements Sniff {
    public const CODE_INVALID_FORMAT = 'InvalidFormat';
    public const CODE_INVALID_COMMENT_TYPE = 'InvalidCommentType';
    public const CODE_MISSING_VARIABLE = 'MissingVariable';
    public const CODE_NO_ASSIGNMENT = 'NoAssignment';
    
    /** @var bool */
    public $allowDocCommentAboveReturn = false;
    
    /** @var bool */
    public $allowAboveNonAssignment = false;
    
    /**
     * @return array<int, (int|string)>
     */
    public function register() : array {
        return [
            T_DOC_COMMENT_OPEN_TAG,
            T_COMMENT,
        ];
    }
    
    /**
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
     * @param int $commentOpenPointer
     */
    public function process(File $phpcsFile, $commentOpenPointer) : void {
        $tokens = $phpcsFile->getTokens();
        $commentClosePointer = $tokens[$commentOpenPointer]['code'] === T_COMMENT ? $commentOpenPointer : $tokens[$commentOpenPointer]['comment_closer'];
        $pointerAfterCommentClosePointer = TokenHelper::findNextEffective($phpcsFile, $commentClosePointer + 1);
        if ($pointerAfterCommentClosePointer !== null) {
            do {
                if ($tokens[$pointerAfterCommentClosePointer]['code'] !== T_ATTRIBUTE) {
                    break;
                }
                $pointerAfterCommentClosePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$pointerAfterCommentClosePointer]['attribute_closer'] + 1);
            } while (true);
            if (in_array($tokens[$pointerAfterCommentClosePointer]['code'], [
                T_PRIVATE,
                T_PROTECTED,
                T_PUBLIC,
                T_READONLY,
                T_FINAL,
                T_CONST,
            ], true)) {
                return;
            }
            if ($tokens[$pointerAfterCommentClosePointer]['code'] === T_STATIC) {
                $pointerAfterStatic = TokenHelper::findNextEffective($phpcsFile, $pointerAfterCommentClosePointer + 1);
                if (in_array($tokens[$pointerAfterStatic]['code'], [
                    T_PRIVATE,
                    T_PROTECTED,
                    T_PUBLIC,
                    T_READONLY,
                ], true)) {
                    return;
                }
                if ($tokens[$pointerAfterStatic]['code'] === T_VARIABLE && PropertyHelper::isProperty($phpcsFile, $pointerAfterStatic)) {
                    return;
                }
            }
        }
        if ($tokens[$commentOpenPointer]['code'] === T_COMMENT) {
            $this->checkCommentType($phpcsFile, $commentOpenPointer);
            return;
        }
        
        /** @var list<Annotation<VarTagValueNode>> $annotations */
        $annotations = AnnotationHelper::getAnnotations($phpcsFile, $commentOpenPointer, '@var');
        if ($annotations === []) {
            return;
        }
        if ($this->allowDocCommentAboveReturn) {
            $pointerAfterCommentClosePointer = TokenHelper::findNextEffective($phpcsFile, $commentClosePointer + 1);
            if ($tokens[$pointerAfterCommentClosePointer]['code'] === T_RETURN) {
                return;
            }
        }
        $this->checkFormat($phpcsFile, $annotations);
        $this->checkVariable($phpcsFile, $annotations, $commentOpenPointer, $commentClosePointer);
    }
    private function checkCommentType(File $phpcsFile, int $commentOpenPointer) : void {
        $tokens = $phpcsFile->getTokens();
        if (preg_match('~^/\\*\\s*@var\\s+~', $tokens[$commentOpenPointer]['content']) === 0) {
            return;
        }
        $fix = $phpcsFile->addFixableError('Invalid comment type /* */ for inline documentation comment, use /** */.', $commentOpenPointer, self::CODE_INVALID_COMMENT_TYPE);
        if (!$fix) {
            return;
        }
        $phpcsFile->fixer
            ->beginChangeset();
        $phpcsFile->fixer
            ->replaceToken($commentOpenPointer, sprintf('/**%s', substr($tokens[$commentOpenPointer]['content'], 2)));
        $phpcsFile->fixer
            ->endChangeset();
    }
    
    /**
     * @param list<Annotation<VarTagValueNode>> $annotations
     */
    private function checkFormat(File $phpcsFile, array $annotations) : void {
        foreach ($annotations as $annotation) {
            if (!$annotation->isInvalid() && $annotation->getValue()->variableName !== '') {
                continue;
            }
            $variableName = '$variableName';
            $annotationContent = (string) $annotation->getValue();
            $type = null;
            if ($annotationContent !== '' && preg_match('~(\\$\\w+)(?:\\s+(.+))?$~i', $annotationContent, $matches) === 1) {
                $variableName = $matches[1];
                $type = $matches[2] ?? null;
            }
            // It may be description when it contains whitespaces
            $isFixable = $type !== null && preg_match('~\\s~', $type) === 0;
            if (!$isFixable) {
                $phpcsFile->addError(sprintf('Invalid inline documentation comment format "@var %1$s", expected "@var type %2$s Optional description".', $annotationContent, $variableName), $annotation->getStartPointer(), self::CODE_INVALID_FORMAT);
                continue;
            }
            $fix = $phpcsFile->addFixableError(sprintf('Invalid inline documentation comment format "@var %1$s", expected "@var %2$s %3$s".', $annotationContent, $type, $variableName), $annotation->getStartPointer(), self::CODE_INVALID_FORMAT);
            if (!$fix) {
                continue;
            }
            $phpcsFile->fixer
                ->beginChangeset();
            $phpcsFile->fixer
                ->addContent($annotation->getStartPointer(), sprintf(' %s %s ', $type, $variableName));
            FixerHelper::removeBetweenIncluding($phpcsFile, $annotation->getStartPointer() + 1, $annotation->getEndPointer());
            $phpcsFile->fixer
                ->endChangeset();
        }
    }
    
    /**
     * @param list<Annotation<VarTagValueNode>> $annotations
     */
    private function checkVariable(File $phpcsFile, array $annotations, int $docCommentOpenerPointer, int $docCommentCloserPointer) : void {
        $tokens = $phpcsFile->getTokens();
        $checkedTokens = [
            T_VARIABLE,
            T_FOREACH,
            T_WHILE,
            T_LIST,
            T_OPEN_SHORT_ARRAY,
            T_CLOSURE,
            T_FN,
        ];
        $variableNames = [];
        foreach ($annotations as $variableAnnotation) {
            if ($variableAnnotation->isInvalid()) {
                continue;
            }
            $variableName = $variableAnnotation->getValue()->variableName;
            if ($variableName === '') {
                continue;
            }
            $variableNames[] = $variableName;
        }
        $improveCodePointer = function (int $codePointer) use ($phpcsFile, $tokens, $checkedTokens, $variableNames) : int {
            $shouldSearchClosure = false;
            if (!in_array($tokens[$codePointer]['code'], $checkedTokens, true)) {
                $shouldSearchClosure = true;
            }
            elseif ($tokens[$codePointer]['code'] === T_VARIABLE && (!$this->isAssignment($phpcsFile, $codePointer) || !in_array($tokens[$codePointer]['content'], $variableNames, true))) {
                $shouldSearchClosure = true;
            }
            if (!$shouldSearchClosure) {
                return $codePointer;
            }
            $closurePointer = TokenHelper::findNext($phpcsFile, [
                T_CLOSURE,
                T_FN,
            ], $codePointer + 1);
            if ($closurePointer !== null && $tokens[$codePointer]['line'] === $tokens[$closurePointer]['line']) {
                return $closurePointer;
            }
            return $codePointer;
        };
        $firstPointerOnNextLine = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $docCommentCloserPointer);
        $codePointerAfter = $firstPointerOnNextLine;
        while ($codePointerAfter !== null && $tokens[$codePointerAfter]['code'] === T_DOC_COMMENT_OPEN_TAG) {
            $codePointerAfter = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $codePointerAfter + 1);
        }
        if ($codePointerAfter !== null) {
            if ($tokens[$codePointerAfter]['code'] === T_STATIC) {
                $codePointerAfter = TokenHelper::findNextEffective($phpcsFile, $codePointerAfter + 1);
            }
            $codePointerAfter = $improveCodePointer($codePointerAfter);
        }
        $codePointerBefore = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $docCommentOpenerPointer);
        while ($codePointerBefore !== null && $tokens[$codePointerBefore]['code'] === T_DOC_COMMENT_OPEN_TAG) {
            $codePointerBefore = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $codePointerBefore - 1);
        }
        if ($codePointerBefore !== null) {
            $codePointerBefore = $improveCodePointer($codePointerBefore);
        }
        foreach ($annotations as $variableAnnotation) {
            if ($variableAnnotation->isInvalid()) {
                continue;
            }
            $variableName = $variableAnnotation->getValue()->variableName;
            if ($variableName === '') {
                continue;
            }
            $missingVariableErrorParameters = [
                sprintf('Missing variable %s before or after the documentation comment.', $variableName),
                $docCommentOpenerPointer,
                self::CODE_MISSING_VARIABLE,
            ];
            $noAssignmentErrorParameters = [
                sprintf('No assignment to %s variable before or after the documentation comment.', $variableName),
                $docCommentOpenerPointer,
                self::CODE_NO_ASSIGNMENT,
            ];
            if ($this->allowAboveNonAssignment && $firstPointerOnNextLine !== null) {
                for ($i = $firstPointerOnNextLine; $i < count($tokens); $i++) {
                    if ($tokens[$i]['line'] > $tokens[$firstPointerOnNextLine]['line']) {
                        break;
                    }
                    if ($tokens[$i]['code'] !== T_VARIABLE) {
                        continue;
                    }
                    if ($tokens[$i]['content'] === $variableName) {
                        return;
                    }
                }
            }
            foreach ([
                1 => $codePointerBefore,
                2 => $codePointerAfter,
            ] as $tryNo => $codePointer) {
                if ($codePointer === null || !in_array($tokens[$codePointer]['code'], $checkedTokens, true)) {
                    if ($tryNo === 2) {
                        $phpcsFile->addError(...$missingVariableErrorParameters);
                    }
                    continue;
                }
                if ($tokens[$codePointer]['code'] === T_VARIABLE) {
                    if ($tokens[$codePointer]['content'] !== '$this' && !$this->isAssignment($phpcsFile, $codePointer)) {
                        if ($tryNo === 2) {
                            $phpcsFile->addError(...$noAssignmentErrorParameters);
                        }
                        continue;
                    }
                    if ($variableName !== $tokens[$codePointer]['content']) {
                        if ($tryNo === 2) {
                            $phpcsFile->addError(...$missingVariableErrorParameters);
                        }
                        continue;
                    }
                }
                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) {
                        if ($tryNo === 2) {
                            $phpcsFile->addError(...$missingVariableErrorParameters);
                        }
                        continue;
                    }
                }
                elseif ($tokens[$codePointer]['code'] === T_OPEN_SHORT_ARRAY) {
                    $pointerAfterList = TokenHelper::findNextEffective($phpcsFile, $tokens[$codePointer]['bracket_closer'] + 1);
                    if ($tokens[$pointerAfterList]['code'] !== T_EQUAL) {
                        if ($tryNo === 2) {
                            $phpcsFile->addError(...$noAssignmentErrorParameters);
                        }
                        continue;
                    }
                    $variablePointerInList = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableName, $codePointer + 1, $tokens[$codePointer]['bracket_closer']);
                    if ($variablePointerInList === null) {
                        if ($tryNo === 2) {
                            $phpcsFile->addError(...$missingVariableErrorParameters);
                        }
                        continue;
                    }
                }
                elseif (in_array($tokens[$codePointer]['code'], [
                    T_CLOSURE,
                    T_FN,
                ], true)) {
                    $parameterPointer = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableName, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer']);
                    if ($parameterPointer === null) {
                        if ($tryNo === 2) {
                            $phpcsFile->addError(...$missingVariableErrorParameters);
                        }
                        continue;
                    }
                }
                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) {
                            if ($tryNo === 2) {
                                $phpcsFile->addError(...$missingVariableErrorParameters);
                            }
                            continue;
                        }
                        $pointerAfterVariableInWhile = TokenHelper::findNextEffective($phpcsFile, $variablePointerInWhile + 1);
                        if ($tokens[$pointerAfterVariableInWhile]['code'] !== T_EQUAL) {
                            if ($tryNo === 2) {
                                $phpcsFile->addError(...$noAssignmentErrorParameters);
                            }
                            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) {
                            if ($tryNo === 2) {
                                $phpcsFile->addError(...$missingVariableErrorParameters);
                            }
                            continue;
                        }
                    }
                }
                // No error, don't check second $codePointer
                continue 2;
            }
        }
    }
    private function isAssignment(File $phpcsFile, int $pointer) : bool {
        $tokens = $phpcsFile->getTokens();
        $pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $pointer + 1);
        if ($tokens[$pointerAfterVariable]['code'] === T_SEMICOLON) {
            $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1);
            return $tokens[$pointerBeforeVariable]['code'] === T_STATIC;
        }
        return in_array($tokens[$pointerAfterVariable]['code'], [
            T_EQUAL,
            T_COALESCE_EQUAL,
        ], true);
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
InlineDocCommentDeclarationSniff::$allowAboveNonAssignment public property @var bool
InlineDocCommentDeclarationSniff::$allowDocCommentAboveReturn public property @var bool
InlineDocCommentDeclarationSniff::checkCommentType private function
InlineDocCommentDeclarationSniff::checkFormat private function *
InlineDocCommentDeclarationSniff::checkVariable private function *
InlineDocCommentDeclarationSniff::CODE_INVALID_COMMENT_TYPE public constant
InlineDocCommentDeclarationSniff::CODE_INVALID_FORMAT public constant
InlineDocCommentDeclarationSniff::CODE_MISSING_VARIABLE public constant
InlineDocCommentDeclarationSniff::CODE_NO_ASSIGNMENT public constant
InlineDocCommentDeclarationSniff::isAssignment private function
InlineDocCommentDeclarationSniff::process public function * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*
Overrides Sniff::process
InlineDocCommentDeclarationSniff::register public function * Overrides Sniff::register
RSS feed
Powered by Drupal