Skip to content

Commit b4f96a8

Browse files
committed
Support for @phpstan-assert/@psalm-assert annotations
1 parent af1f618 commit b4f96a8

File tree

7 files changed

+298
-9
lines changed

7 files changed

+298
-9
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Helpers\Annotation;
4+
5+
use InvalidArgumentException;
6+
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use SlevomatCodingStandard\Helpers\AnnotationTypeHelper;
9+
use function in_array;
10+
use function sprintf;
11+
12+
/**
13+
* @internal
14+
*/
15+
class AssertAnnotation extends Annotation
16+
{
17+
18+
/** @var AssertTagValueNode|null */
19+
private $contentNode;
20+
21+
public function __construct(string $name, int $startPointer, int $endPointer, ?string $content, ?AssertTagValueNode $contentNode)
22+
{
23+
if (!in_array(
24+
$name,
25+
['@phpstan-assert', '@phpstan-assert-if-true', '@phpstan-assert-if-false', '@psalm-assert', '@psalm-assert-if-true', '@psalm-assert-if-false'],
26+
true
27+
)) {
28+
throw new InvalidArgumentException(sprintf('Unsupported annotation %s.', $name));
29+
}
30+
31+
parent::__construct($name, $startPointer, $endPointer, $content);
32+
33+
$this->contentNode = $contentNode;
34+
}
35+
36+
public function isInvalid(): bool
37+
{
38+
return $this->contentNode === null;
39+
}
40+
41+
public function getContentNode(): AssertTagValueNode
42+
{
43+
$this->errorWhenInvalid();
44+
45+
return $this->contentNode;
46+
}
47+
48+
public function hasDescription(): bool
49+
{
50+
return $this->getDescription() !== null;
51+
}
52+
53+
public function getDescription(): ?string
54+
{
55+
$this->errorWhenInvalid();
56+
57+
return $this->contentNode->description !== '' ? $this->contentNode->description : null;
58+
}
59+
60+
public function getType(): TypeNode
61+
{
62+
$this->errorWhenInvalid();
63+
64+
return $this->contentNode->type;
65+
}
66+
67+
public function export(): string
68+
{
69+
$exported = sprintf('%s %s %s', $this->name, AnnotationTypeHelper::export($this->getType()), $this->contentNode->parameter);
70+
71+
$description = $this->getDescription();
72+
if ($description !== null) {
73+
$exported .= sprintf(' %s', $this->fixDescription($description));
74+
}
75+
76+
return $exported;
77+
}
78+
79+
}

SlevomatCodingStandard/Helpers/AnnotationHelper.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\PhpDocParser\Parser\TokenIterator;
2424
use PHPStan\PhpDocParser\Parser\TypeParser;
2525
use SlevomatCodingStandard\Helpers\Annotation\Annotation;
26+
use SlevomatCodingStandard\Helpers\Annotation\AssertAnnotation;
2627
use SlevomatCodingStandard\Helpers\Annotation\ExtendsAnnotation;
2728
use SlevomatCodingStandard\Helpers\Annotation\GenericAnnotation;
2829
use SlevomatCodingStandard\Helpers\Annotation\ImplementsAnnotation;
@@ -64,7 +65,7 @@ class AnnotationHelper
6465

6566
/**
6667
* @internal
67-
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation $annotation
68+
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation|AssertAnnotation $annotation
6869
* @return TypeNode[]
6970
*/
7071
public static function getAnnotationTypes(Annotation $annotation): array
@@ -97,7 +98,7 @@ public static function getAnnotationTypes(Annotation $annotation): array
9798

9899
/**
99100
* @internal
100-
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation $annotation
101+
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation $annotation
101102
* @return ConstExprNode[]
102103
*/
103104
public static function getAnnotationConstantExpressions(Annotation $annotation): array
@@ -125,7 +126,7 @@ public static function getAnnotationConstantExpressions(Annotation $annotation):
125126

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

137138
/**
138139
* @internal
139-
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation $annotation
140+
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation $annotation
140141
*/
141142
public static function fixAnnotationConstantFetchNode(
142143
File $phpcsFile,
@@ -192,7 +193,7 @@ public static function fixAnnotationConstantFetchNode(
192193
}
193194

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

204205
/**
205-
* @return (VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|GenericAnnotation)[][]
206+
* @return (VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|AssertAnnotation|GenericAnnotation)[][]
206207
*/
207208
public static function getAnnotations(File $phpcsFile, int $pointer): array
208209
{
@@ -334,6 +335,12 @@ static function () use ($phpcsFile, $pointer): array {
334335
'@psalm-import-type' => TypeImportAnnotation::class,
335336
'@phpstan-import-type' => TypeImportAnnotation::class,
336337
'@mixin' => MixinAnnotation::class,
338+
'@phpstan-assert' => AssertAnnotation::class,
339+
'@phpstan-assert-if-true' => AssertAnnotation::class,
340+
'@phpstan-assert-if-false' => AssertAnnotation::class,
341+
'@psalm-assert' => AssertAnnotation::class,
342+
'@psalm-assert-if-true' => AssertAnnotation::class,
343+
'@psalm-assert-if-false' => AssertAnnotation::class,
337344
];
338345

339346
if (array_key_exists($annotationName, $mapping)) {
@@ -468,7 +475,7 @@ public static function isAnnotationUseless(
468475
}
469476

470477
/**
471-
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation $annotation
478+
* @param VariableAnnotation|ParameterAnnotation|ReturnAnnotation|ThrowsAnnotation|PropertyAnnotation|MethodAnnotation|TemplateAnnotation|ExtendsAnnotation|ImplementsAnnotation|UseAnnotation|MixinAnnotation|TypeAliasAnnotation|TypeImportAnnotation|AssertAnnotation $annotation
472479
*/
473480
private static function fixAnnotation(Annotation $annotation, TypeNode $typeNode, TypeNode $fixedTypeNode): Annotation
474481
{

build/PHPStan/phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ parameters:
2020
path: %currentWorkingDirectory%/SlevomatCodingStandard/Sniffs/ControlStructures/AssignmentInConditionSniff.php
2121
-
2222
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.#'
23-
count: 13
23+
count: 14
2424
path: %currentWorkingDirectory%/SlevomatCodingStandard/Helpers/AnnotationHelper.php
2525

2626
services:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Helpers\Annotation;
4+
5+
use InvalidArgumentException;
6+
use LogicException;
7+
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
8+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
9+
use SlevomatCodingStandard\Helpers\TestCase;
10+
11+
class AssertAnnotationTest extends TestCase
12+
{
13+
14+
public function testAnnotation(): void
15+
{
16+
$annotation = new AssertAnnotation(
17+
'@phpstan-assert',
18+
1,
19+
10,
20+
'Description',
21+
new AssertTagValueNode(
22+
new IdentifierTypeNode('string'),
23+
'$parameter',
24+
false,
25+
'Description'
26+
)
27+
);
28+
29+
self::assertSame('@phpstan-assert', $annotation->getName());
30+
self::assertSame(1, $annotation->getStartPointer());
31+
self::assertSame(10, $annotation->getEndPointer());
32+
self::assertSame('Description', $annotation->getContent());
33+
34+
self::assertFalse($annotation->isInvalid());
35+
self::assertTrue($annotation->hasDescription());
36+
self::assertSame('Description', $annotation->getDescription());
37+
self::assertSame('@phpstan-assert string $parameter Description', $annotation->export());
38+
}
39+
40+
public function testUnsupportedAnnotation(): void
41+
{
42+
self::expectException(InvalidArgumentException::class);
43+
self::expectExceptionMessage('Unsupported annotation @param.');
44+
new AssertAnnotation('@param', 1, 1, null, null);
45+
}
46+
47+
public function testGetContentNodeWhenInvalid(): void
48+
{
49+
self::expectException(LogicException::class);
50+
self::expectExceptionMessage('Invalid @phpstan-assert annotation.');
51+
$annotation = new AssertAnnotation('@phpstan-assert', 1, 1, null, null);
52+
$annotation->getContentNode();
53+
}
54+
55+
public function testGetDescriptionWhenInvalid(): void
56+
{
57+
self::expectException(LogicException::class);
58+
self::expectExceptionMessage('Invalid @phpstan-assert annotation.');
59+
$annotation = new AssertAnnotation('@phpstan-assert', 1, 1, null, null);
60+
$annotation->getDescription();
61+
}
62+
63+
public function testGetTypeWhenInvalid(): void
64+
{
65+
self::expectException(LogicException::class);
66+
self::expectExceptionMessage('Invalid @phpstan-assert annotation.');
67+
$annotation = new AssertAnnotation('@phpstan-assert', 1, 1, null, null);
68+
$annotation->getType();
69+
}
70+
71+
}

tests/Sniffs/Namespaces/ReferenceUsedNamesOnlySniffTest.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ public function testSearchingInAnnotations(): void
725725
]
726726
);
727727

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

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

1024+
self::assertSniffError(
1025+
$report,
1026+
183,
1027+
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
1028+
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
1029+
);
1030+
self::assertSniffError(
1031+
$report,
1032+
190,
1033+
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
1034+
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
1035+
);
1036+
self::assertSniffError(
1037+
$report,
1038+
197,
1039+
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
1040+
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
1041+
);
1042+
self::assertSniffError(
1043+
$report,
1044+
204,
1045+
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
1046+
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
1047+
);
1048+
self::assertSniffError(
1049+
$report,
1050+
211,
1051+
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
1052+
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
1053+
);
1054+
self::assertSniffError(
1055+
$report,
1056+
218,
1057+
ReferenceUsedNamesOnlySniff::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME,
1058+
'Class \Foo\Assertion should not be referenced via a fully qualified name, but via a use statement.'
1059+
);
1060+
10241061
self::assertAllFixedInFile($report);
10251062
}
10261063

tests/Sniffs/Namespaces/data/shouldBeInUseStatementSearchingInAnnotations.fixed.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Foo\OffsetAccessOffset2;
2525
use Foo\OffsetAccessType3;
2626
use Foo\OffsetAccessOffset3;
27+
use Foo\Assertion;
2728

2829
/**
2930
* @method \DateTimeImmutable|int|DateTime getProperty()
@@ -195,3 +196,50 @@ public function returnOffsetAccess()
195196
{}
196197

197198
}
199+
200+
class Assert
201+
{
202+
203+
/**
204+
* @phpstan-assert Assertion $parameter
205+
*/
206+
public function phpstanAssert($parameter)
207+
{
208+
}
209+
210+
/**
211+
* @phpstan-assert-if-true Assertion $parameter
212+
*/
213+
public function phpstanAssertIfTrue($parameter)
214+
{
215+
}
216+
217+
/**
218+
* @phpstan-assert-if-false Assertion $parameter
219+
*/
220+
public function phpstanAssertIfFalse($parameter)
221+
{
222+
}
223+
224+
/**
225+
* @psalm-assert Assertion $parameter
226+
*/
227+
public function psalmAssert($parameter)
228+
{
229+
}
230+
231+
/**
232+
* @psalm-assert-if-true Assertion $parameter
233+
*/
234+
public function psalmAssertIfTrue($parameter)
235+
{
236+
}
237+
238+
/**
239+
* @psalm-assert-if-false Assertion $parameter
240+
*/
241+
public function psalmAssertIfFalse($parameter)
242+
{
243+
}
244+
245+
}

0 commit comments

Comments
 (0)