Skip to content

Commit

Permalink
Support for @phpstan-assert/@psalm-assert annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
kukulich committed May 6, 2022
1 parent af1f618 commit b4f96a8
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 9 deletions.
79 changes: 79 additions & 0 deletions SlevomatCodingStandard/Helpers/Annotation/AssertAnnotation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Helpers\Annotation;

use InvalidArgumentException;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use SlevomatCodingStandard\Helpers\AnnotationTypeHelper;
use function in_array;
use function sprintf;

/**
* @internal
*/
class AssertAnnotation extends Annotation
{

/** @var AssertTagValueNode|null */
private $contentNode;

public function __construct(string $name, int $startPointer, int $endPointer, ?string $content, ?AssertTagValueNode $contentNode)
{
if (!in_array(
$name,
['@phpstan-assert', '@phpstan-assert-if-true', '@phpstan-assert-if-false', '@psalm-assert', '@psalm-assert-if-true', '@psalm-assert-if-false'],
true
)) {
throw new InvalidArgumentException(sprintf('Unsupported annotation %s.', $name));
}

parent::__construct($name, $startPointer, $endPointer, $content);

$this->contentNode = $contentNode;
}

public function isInvalid(): bool
{
return $this->contentNode === null;
}

public function getContentNode(): AssertTagValueNode
{
$this->errorWhenInvalid();

return $this->contentNode;
}

public function hasDescription(): bool
{
return $this->getDescription() !== null;
}

public function getDescription(): ?string
{
$this->errorWhenInvalid();

return $this->contentNode->description !== '' ? $this->contentNode->description : null;
}

public function getType(): TypeNode
{
$this->errorWhenInvalid();

return $this->contentNode->type;
}

public function export(): string
{
$exported = sprintf('%s %s %s', $this->name, AnnotationTypeHelper::export($this->getType()), $this->contentNode->parameter);

$description = $this->getDescription();
if ($description !== null) {
$exported .= sprintf(' %s', $this->fixDescription($description));
}

return $exported;
}

}
21 changes: 14 additions & 7 deletions SlevomatCodingStandard/Helpers/AnnotationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use SlevomatCodingStandard\Helpers\Annotation\Annotation;
use SlevomatCodingStandard\Helpers\Annotation\AssertAnnotation;
use SlevomatCodingStandard\Helpers\Annotation\ExtendsAnnotation;
use SlevomatCodingStandard\Helpers\Annotation\GenericAnnotation;
use SlevomatCodingStandard\Helpers\Annotation\ImplementsAnnotation;
Expand Down Expand Up @@ -64,7 +65,7 @@ class AnnotationHelper

/**
* @internal
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation $annotation
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation|AssertAnnotation $annotation
* @return TypeNode[]
*/
public static function getAnnotationTypes(Annotation $annotation): array
Expand Down Expand Up @@ -97,7 +98,7 @@ public static function getAnnotationTypes(Annotation $annotation): array

/**
* @internal
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation $annotation
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation $annotation
* @return ConstExprNode[]
*/
public static function getAnnotationConstantExpressions(Annotation $annotation): array
Expand Down Expand Up @@ -125,7 +126,7 @@ public static function getAnnotationConstantExpressions(Annotation $annotation):

/**
* @internal
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation $annotation
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation $annotation
*/
public static function fixAnnotationType(File $phpcsFile, Annotation $annotation, TypeNode $typeNode, TypeNode $fixedTypeNode): string
{
Expand All @@ -136,7 +137,7 @@ public static function fixAnnotationType(File $phpcsFile, Annotation $annotation

/**
* @internal
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation $annotation
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation $annotation
*/
public static function fixAnnotationConstantFetchNode(
File $phpcsFile,
Expand Down Expand Up @@ -192,7 +193,7 @@ public static function fixAnnotationConstantFetchNode(
}

/**
* @return (VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|GenericAnnotation)[]
* @return (VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation|GenericAnnotation)[]
*/
public static function getAnnotationsByName(File $phpcsFile, int $pointer, string $annotationName): array
{
Expand All @@ -202,7 +203,7 @@ public static function getAnnotationsByName(File $phpcsFile, int $pointer, strin
}

/**
* @return (VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|GenericAnnotation)[][]
* @return (VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation|GenericAnnotation)[][]
*/
public static function getAnnotations(File $phpcsFile, int $pointer): array
{
Expand Down Expand Up @@ -334,6 +335,12 @@ static function () use ($phpcsFile, $pointer): array {
'@psalm-import-type' => TypeImportAnnotation::class,
'@phpstan-import-type' => TypeImportAnnotation::class,
'@mixin' => MixinAnnotation::class,
'@phpstan-assert' => AssertAnnotation::class,
'@phpstan-assert-if-true' => AssertAnnotation::class,
'@phpstan-assert-if-false' => AssertAnnotation::class,
'@psalm-assert' => AssertAnnotation::class,
'@psalm-assert-if-true' => AssertAnnotation::class,
'@psalm-assert-if-false' => AssertAnnotation::class,
];

if (array_key_exists($annotationName, $mapping)) {
Expand Down Expand Up @@ -468,7 +475,7 @@ public static function isAnnotationUseless(
}

/**
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation $annotation
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation|AssertAnnotation $annotation
*/
private static function fixAnnotation(Annotation $annotation, TypeNode $typeNode, TypeNode $fixedTypeNode): Annotation
{
Expand Down
2 changes: 1 addition & 1 deletion build/PHPStan/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ parameters:
path: %currentWorkingDirectory%/SlevomatCodingStandard/Sniffs/ControlStructures/AssignmentInConditionSniff.php
-
message: '#Parameter \#5 \$contentNode of class SlevomatCodingStandard\\Helpers\\Annotation\\\w+Annotation constructor expects PHPStan\\PhpDocParser\\Ast\\PhpDoc\\\w+TagValueNode\|null, PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagValueNode\|null given.#'
count: 13
count: 14
path: %currentWorkingDirectory%/SlevomatCodingStandard/Helpers/AnnotationHelper.php

services:
Expand Down
71 changes: 71 additions & 0 deletions tests/Helpers/Annotation/AssertAnnotationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Helpers\Annotation;

use InvalidArgumentException;
use LogicException;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use SlevomatCodingStandard\Helpers\TestCase;

class AssertAnnotationTest extends TestCase
{

public function testAnnotation(): void
{
$annotation = new AssertAnnotation(
'@phpstan-assert',
1,
10,
'Description',
new AssertTagValueNode(
new IdentifierTypeNode('string'),
'$parameter',
false,
'Description'
)
);

self::assertSame('@phpstan-assert', $annotation->getName());
self::assertSame(1, $annotation->getStartPointer());
self::assertSame(10, $annotation->getEndPointer());
self::assertSame('Description', $annotation->getContent());

self::assertFalse($annotation->isInvalid());
self::assertTrue($annotation->hasDescription());
self::assertSame('Description', $annotation->getDescription());
self::assertSame('@phpstan-assert string $parameter Description', $annotation->export());
}

public function testUnsupportedAnnotation(): void
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('Unsupported annotation @param.');
new AssertAnnotation('@param', 1, 1, null, null);
}

public function testGetContentNodeWhenInvalid(): void
{
self::expectException(LogicException::class);
self::expectExceptionMessage('Invalid @phpstan-assert annotation.');
$annotation = new AssertAnnotation('@phpstan-assert', 1, 1, null, null);
$annotation->getContentNode();
}

public function testGetDescriptionWhenInvalid(): void
{
self::expectException(LogicException::class);
self::expectExceptionMessage('Invalid @phpstan-assert annotation.');
$annotation = new AssertAnnotation('@phpstan-assert', 1, 1, null, null);
$annotation->getDescription();
}

public function testGetTypeWhenInvalid(): void
{
self::expectException(LogicException::class);
self::expectExceptionMessage('Invalid @phpstan-assert annotation.');
$annotation = new AssertAnnotation('@phpstan-assert', 1, 1, null, null);
$annotation->getType();
}

}
39 changes: 38 additions & 1 deletion tests/Sniffs/Namespaces/ReferenceUsedNamesOnlySniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ public function testSearchingInAnnotations(): void
]
);

self::assertSame(50, $report->getErrorCount());
self::assertSame(56, $report->getErrorCount());

self::assertSniffError(
$report,
Expand Down Expand Up @@ -1021,6 +1021,43 @@ public function testSearchingInAnnotations(): void
'Class \Foo\OffsetAccessOffset3 should not be referenced via a fully qualified name, but via a use statement.'
);

self::assertSniffError(
$report,
183,
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
);
self::assertSniffError(
$report,
190,
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
);
self::assertSniffError(
$report,
197,
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
);
self::assertSniffError(
$report,
204,
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
);
self::assertSniffError(
$report,
211,
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
);
self::assertSniffError(
$report,
218,
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
);

self::assertAllFixedInFile($report);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Foo\OffsetAccessOffset2;
use Foo\OffsetAccessType3;
use Foo\OffsetAccessOffset3;
use Foo\Assertion;

/**
* @method \DateTimeImmutable|int|DateTime getProperty()
Expand Down Expand Up @@ -195,3 +196,50 @@ public function returnOffsetAccess()
{}

}

class Assert
{

/**
* @phpstan-assert Assertion $parameter
*/
public function phpstanAssert($parameter)
{
}

/**
* @phpstan-assert-if-true Assertion $parameter
*/
public function phpstanAssertIfTrue($parameter)
{
}

/**
* @phpstan-assert-if-false Assertion $parameter
*/
public function phpstanAssertIfFalse($parameter)
{
}

/**
* @psalm-assert Assertion $parameter
*/
public function psalmAssert($parameter)
{
}

/**
* @psalm-assert-if-true Assertion $parameter
*/
public function psalmAssertIfTrue($parameter)
{
}

/**
* @psalm-assert-if-false Assertion $parameter
*/
public function psalmAssertIfFalse($parameter)
{
}

}
Loading

0 comments on commit b4f96a8

Please sign in to comment.