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\CommentingView 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);
}
}