Skip to content

Commit 69c03bb

Browse files
Add Throws attribute
1 parent 9876dee commit 69c03bb

18 files changed

+245
-35
lines changed

README.md

+26-25
Original file line numberDiff line numberDiff line change
@@ -94,31 +94,32 @@ This extension works by interacting with the parser that PHPStan uses to parse t
9494

9595
These are the available attributes and their corresponding PHPDoc annotations:
9696

97-
| Attribute | PHPDoc Annotations |
98-
|--------------------------------------------------------------------------------------------------------------------|--------------------------------------|
99-
| [Deprecated](https://github.com/php-static-analysis/attributes/blob/main/doc/Deprecated.md) | `@deprecated` |
100-
| [Impure](https://github.com/php-static-analysis/attributes/blob/main/doc/Impure.md) | `@impure` |
101-
| [Internal](https://github.com/php-static-analysis/attributes/blob/main/doc/Internal.md) | `@internal` |
102-
| [IsReadOnly](https://github.com/php-static-analysis/attributes/blob/main/doc/IsReadOnly.md) | `@readonly` |
103-
| [Method](https://github.com/php-static-analysis/attributes/blob/main/doc/Method.md) | `@method` |
104-
| [Mixin](https://github.com/php-static-analysis/attributes/blob/main/doc/Mixin.md) | `@mixin` |
105-
| [Param](https://github.com/php-static-analysis/attributes/blob/main/doc/Param.md) | `@param` |
106-
| [ParamOut](https://github.com/php-static-analysis/attributes/blob/main/doc/ParamOut.md) | `@param-out` |
107-
| [Property](https://github.com/php-static-analysis/attributes/blob/main/doc/Property.md) | `@property` `@var` |
108-
| [PropertyRead](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyRead.md) | `@property-read` |
109-
| [PropertyWrite](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyWrite.md) | `@property-write` |
110-
| [Pure](https://github.com/php-static-analysis/attributes/blob/main/doc/Pure.md) | `@pure` |
111-
| [RequireExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireExtends.md) | `@require-extends` |
112-
| [RequireImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireImplements.md) | `@require-implements` |
113-
| [Returns](https://github.com/php-static-analysis/attributes/blob/main/doc/Returns.md) | `@return` |
114-
| [SelfOut](https://github.com/php-static-analysis/attributes/blob/main/doc/SelfOut.md) | `@self-out` `@this-out` |
115-
| [Template](https://github.com/php-static-analysis/attributes/blob/main/doc/Template.md) | `@template` |
116-
| [TemplateContravariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateContravariant.md) | `@template-contravariant` |
117-
| [TemplateCovariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateCovariant.md) | `@template-covariant` |
118-
| [TemplateExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateExtends.md) | `@extends` `@template-extends` |
119-
| [TemplateImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateImplements.md) | `@implements` `@template-implements` |
120-
| [TemplateUse](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateUse.md) | `@use` `@template-use` |
121-
| [Type](https://github.com/php-static-analysis/attributes/blob/main/doc/Type.md) | `@var` `@return` |
97+
| Attribute | PHPDoc Annotations |
98+
|-------------------------------------------------------------------------------------------------------------------|--------------------------------------|
99+
| [Deprecated](https://github.com/php-static-analysis/attributes/blob/main/doc/Deprecated.md) | `@deprecated` |
100+
| [Impure](https://github.com/php-static-analysis/attributes/blob/main/doc/Impure.md) | `@impure` |
101+
| [Internal](https://github.com/php-static-analysis/attributes/blob/main/doc/Internal.md) | `@internal` |
102+
| [IsReadOnly](https://github.com/php-static-analysis/attributes/blob/main/doc/IsReadOnly.md) | `@readonly` |
103+
| [Method](https://github.com/php-static-analysis/attributes/blob/main/doc/Method.md) | `@method` |
104+
| [Mixin](https://github.com/php-static-analysis/attributes/blob/main/doc/Mixin.md) | `@mixin` |
105+
| [Param](https://github.com/php-static-analysis/attributes/blob/main/doc/Param.md) | `@param` |
106+
| [ParamOut](https://github.com/php-static-analysis/attributes/blob/main/doc/ParamOut.md) | `@param-out` |
107+
| [Property](https://github.com/php-static-analysis/attributes/blob/main/doc/Property.md) | `@property` `@var` |
108+
| [PropertyRead](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyRead.md) | `@property-read` |
109+
| [PropertyWrite](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyWrite.md) | `@property-write` |
110+
| [Pure](https://github.com/php-static-analysis/attributes/blob/main/doc/Pure.md) | `@pure` |
111+
| [RequireExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireExtends.md) | `@require-extends` |
112+
| [RequireImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireImplements.md) | `@require-implements` |
113+
| [Returns](https://github.com/php-static-analysis/attributes/blob/main/doc/Returns.md) | `@return` |
114+
| [SelfOut](https://github.com/php-static-analysis/attributes/blob/main/doc/SelfOut.md) | `@self-out` `@this-out` |
115+
| [Template](https://github.com/php-static-analysis/attributes/blob/main/doc/Template.md) | `@template` |
116+
| [TemplateContravariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateContravariant.md) | `@template-contravariant` |
117+
| [TemplateCovariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateCovariant.md) | `@template-covariant` |
118+
| [TemplateExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateExtends.md) | `@extends` `@template-extends` |
119+
| [TemplateImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateImplements.md) | `@implements` `@template-implements` |
120+
| [TemplateUse](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateUse.md) | `@use` `@template-use` |
121+
| [Throws](https://github.com/php-static-analysis/attributes/blob/main/doc/Throws.md) | `@throws` |
122+
| [Type](https://github.com/php-static-analysis/attributes/blob/main/doc/Type.md) | `@var` `@return` |
122123

123124

124125

composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
"prefer-stable": true,
2525
"require": {
2626
"php": ">=8.0",
27-
"php-static-analysis/attributes": "^0.1.15 || dev-main",
28-
"php-static-analysis/node-visitor": "^0.1.15 || dev-main",
27+
"php-static-analysis/attributes": "^0.1.16 || dev-main",
28+
"php-static-analysis/node-visitor": "^0.1.16 || dev-main",
2929
"phpstan/phpstan": "^1.8"
3030
},
3131
"require-dev": {

src/Parser/AttributeParser.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function parseString(string $sourceCode): array
3535
private function traverseAst(array $ast): array
3636
{
3737
$traverser = new NodeTraverser();
38-
$nodeVisitor = new AttributeNodeVisitor('phpstan');
38+
$nodeVisitor = new AttributeNodeVisitor(AttributeNodeVisitor::TOOL_PHPSTAN);
3939
$traverser->addVisitor($nodeVisitor);
4040

4141
$ast = $traverser->traverse($ast);

tests/ThrowsAttributeTest.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace test\PhpStaticAnalysis\PHPStanExtension;
4+
5+
class ThrowsAttributeTest extends BaseAttributeTestCase
6+
{
7+
public function testMethodThrowsAttribute(): void
8+
{
9+
$errors = $this->analyse(__DIR__ . '/data/Throws/MethodThrowsAttribute.php');
10+
$expectedErrors = [
11+
'Method test\PhpStaticAnalysis\PHPStanExtension\data\Throws\MethodThrowsAttribute::countNoErrorName() has Exception in PHPDoc @throws tag but it\'s not thrown.' => 72,
12+
];
13+
14+
$this->checkExpectedErrors($errors, $expectedErrors);
15+
}
16+
17+
public function testFunctionThrowsAttribute(): void
18+
{
19+
$errors = $this->analyse(__DIR__ . '/data/Throws/FunctionThrowsAttribute.php');
20+
$this->assertCount(0, $errors);
21+
}
22+
23+
public function testInvalidMethodThrowsAttribute(): void
24+
{
25+
$errors = $this->analyse(__DIR__ . '/data/Throws/InvalidMethodThrowsAttribute.php');
26+
27+
$expectedErrors = [
28+
'PHPDoc tag @throws has invalid value (): Unexpected token "\n ", expected type at offset 14' => 10,
29+
'Parameter #1 ...$exceptions of attribute class PhpStaticAnalysis\Attributes\Throws constructor expects string, int given.' => 10,
30+
'Method test\PhpStaticAnalysis\PHPStanExtension\data\Throws\InvalidMethodThrowsAttribute::getOtherNameLength() has string in PHPDoc @throws tag but it\'s not thrown.' => 16,
31+
'PHPDoc tag @throws with type string is not subtype of Throwable' => 16,
32+
'Attribute class PhpStaticAnalysis\Attributes\Throws does not have the property target.' => 22,
33+
];
34+
35+
$this->checkExpectedErrors($errors, $expectedErrors);
36+
}
37+
38+
public static function getAdditionalConfigFiles(): array
39+
{
40+
return array_merge(
41+
parent::getAdditionalConfigFiles(),
42+
[
43+
__DIR__ . '/conf/throws.neon',
44+
]
45+
);
46+
}
47+
}

tests/conf/throws.neon

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
exceptions:
3+
check:
4+
tooWideThrowType: true

tests/data/Mixin/ClassMixinAttribute.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ class Another
1919
{
2020
}
2121

22-
#[Mixin('ClassMixinAttribute')] // this is the proxied class
22+
#[Mixin(ClassMixinAttribute::class)] // this is the proxied class
2323
#[Mixin(
24-
'MyClass',
25-
'Another',
24+
MyClass::class,
25+
Another::class,
2626
)]
2727
class ClassMixinAttributeProxy
2828
{

tests/data/Param/MethodParamAttribute.php

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace test\PhpStaticAnalysis\PHPStanExtension\data\Param;
44

5+
use Exception;
56
use PhpStaticAnalysis\Attributes\Param;
67

78
class MethodParamAttribute
@@ -12,6 +13,12 @@ public function countNames(array $names): int
1213
return count($names);
1314
}
1415

16+
#[Param(exception: Exception::class)]
17+
public function throwException($exception): void
18+
{
19+
throw $exception;
20+
}
21+
1522
#[Param('string[] $names')]
1623
public function countUnnamedNames(array $names): int
1724
{

tests/data/ParamOut/MethodParamOutAttribute.php

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace test\PhpStaticAnalysis\PHPStanExtension\data\ParamOut;
44

5+
use Exception;
56
use PhpStaticAnalysis\Attributes\ParamOut;
67

78
class MethodParamOutAttribute
@@ -12,6 +13,12 @@ public function setNames(mixed &$names): void
1213
$names = 1;
1314
}
1415

16+
#[ParamOut(exception: Exception::class)]
17+
public function setException(mixed &$exception): void
18+
{
19+
$exception = new Exception();
20+
}
21+
1522
#[ParamOut('int $names')]
1623
public function setUnnamedNames(mixed &$names): void
1724
{

tests/data/Property/ClassPropertyAttribute.php

+2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace test\PhpStaticAnalysis\PHPStanExtension\data\Property;
44

5+
use Exception;
56
use PhpStaticAnalysis\Attributes\Property;
67

78
#[Property(name: 'string')] // the name of the user
9+
#[Property(exception: Exception::class)]
810
#[Property('int $age')]
911
#[Property(
1012
index1: 'string[]',

tests/data/PropertyRead/ClassPropertyReadAttribute.php

+2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace test\PhpStaticAnalysis\PHPStanExtension\data\PropertyRead;
44

5+
use Exception;
56
use PhpStaticAnalysis\Attributes\PropertyRead;
67

78
#[PropertyRead(name: 'string')] // cannot be written to
9+
#[PropertyRead(exception: Exception::class)]
810
#[PropertyRead('int $age')]
911
#[PropertyRead(
1012
index1: 'string[]',

tests/data/PropertyWrite/ClassPropertyWriteAttribute.php

+2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace test\PhpStaticAnalysis\PHPStanExtension\data\PropertyWrite;
44

5+
use Exception;
56
use PhpStaticAnalysis\Attributes\PropertyWrite;
67

78
#[PropertyWrite(name: 'string')] // cannot be read
9+
#[PropertyWrite(exception: Exception::class)]
810
#[PropertyWrite('int $age')]
911
#[PropertyWrite(
1012
index1: 'string[]',

tests/data/RequireExtends/TraitRequireExtendsAttribute.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use PhpStaticAnalysis\Attributes\RequireExtends;
66

7-
#[RequireExtends('ClassRequireExtendsAttribute')] // the class using this trait needs to extend this class
7+
#[RequireExtends(ClassRequireExtendsAttribute::class)] // the class using this trait needs to extend this class
88
trait TraitRequireExtendsAttribute
99
{
1010
}

tests/data/RequireImplements/TraitRequireImplementsAttribute.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
use PhpStaticAnalysis\Attributes\RequireImplements;
66

7-
#[RequireImplements('InterfaceRequireImplementsAttribute')] // the class that uses this trait needs to implement these interfaces
7+
#[RequireImplements(InterfaceRequireImplementsAttribute::class)] // the class that uses this trait needs to implement these interfaces
88
#[RequireImplements(
9-
'InterfaceRequireImplementsAttribute2',
10-
'InterfaceRequireImplementsAttribute3'
9+
InterfaceRequireImplementsAttribute2::class,
10+
InterfaceRequireImplementsAttribute3::class
1111
)]
1212
trait TraitRequireImplementsAttribute
1313
{

tests/data/Returns/MethodReturnsAttribute.php

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace test\PhpStaticAnalysis\PHPStanExtension\data\Returns;
44

5+
use Exception;
56
use PhpStaticAnalysis\Attributes\Returns;
67

78
class MethodReturnsAttribute
@@ -12,6 +13,12 @@ public function getNames(): array
1213
return ['hello', 'world'];
1314
}
1415

16+
#[Returns(Exception::class)]
17+
public function getException()
18+
{
19+
return new Exception();
20+
}
21+
1522
/**
1623
* @deprecated
1724
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace test\PhpStaticAnalysis\PHPStanExtension\data\Throws;
4+
5+
use Exception;
6+
use PhpStaticAnalysis\Attributes\Throws;
7+
8+
#[Throws(Exception::class)]
9+
function countName(string $name): int
10+
{
11+
if ($name == '') {
12+
throw new Exception('Empty string!');
13+
}
14+
return strlen($name);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace test\PhpStaticAnalysis\PHPStanExtension\data\Throws;
4+
5+
use Exception;
6+
use PhpStaticAnalysis\Attributes\Throws;
7+
8+
class InvalidMethodThrowsAttribute
9+
{
10+
#[Throws(0)]
11+
public function getNameLength(string $name): int
12+
{
13+
return strlen($name);
14+
}
15+
16+
#[Throws('string')]
17+
public function getOtherNameLength(string $name): int
18+
{
19+
return strlen($name);
20+
}
21+
22+
#[Throws(Exception::class)]
23+
public string $property;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace test\PhpStaticAnalysis\PHPStanExtension\data\Throws;
4+
5+
use Error;
6+
use Exception;
7+
use PhpStaticAnalysis\Attributes\Throws;
8+
9+
class MethodThrowsAttribute
10+
{
11+
#[Throws(Exception::class)] // returns the number of names
12+
public function countName(string $name): int
13+
{
14+
if ($name == '') {
15+
throw new Exception('Empty string!');
16+
}
17+
return strlen($name);
18+
}
19+
20+
/**
21+
* @deprecated
22+
*/
23+
#[Throws(Exception::class)]
24+
public function countMoreName(string $name): int
25+
{
26+
if ($name == '') {
27+
throw new Exception('Empty string!');
28+
}
29+
return strlen($name);
30+
}
31+
32+
/**
33+
* @throws Exception
34+
*/
35+
#[Throws(Exception::class)]
36+
public function countEvenMoreName(string $name): int
37+
{
38+
if ($name == '') {
39+
throw new Exception('Empty string!');
40+
}
41+
return strlen($name);
42+
}
43+
44+
#[Throws(
45+
Exception::class,
46+
Error::class
47+
)]
48+
public function countTwoNames(string $name1, string $name2): int
49+
{
50+
if ($name1 == '') {
51+
throw new Exception('Empty string!');
52+
}
53+
if ($name2 == '') {
54+
throw new Error('Empty string!');
55+
}
56+
return strlen($name1 . $name2);
57+
}
58+
59+
#[Throws(Exception::class)]
60+
#[Throws(Error::class)]
61+
public function countOtherTwoNames(string $name1, string $name2): int
62+
{
63+
if ($name1 == '') {
64+
throw new Exception('Empty string!');
65+
}
66+
if ($name2 == '') {
67+
throw new Error('Empty string!');
68+
}
69+
return strlen($name1 . $name2);
70+
}
71+
72+
#[Throws(Exception::class)]
73+
public function countNoErrorName(string $name): int
74+
{
75+
return strlen($name);
76+
}
77+
78+
/**
79+
* @throws Exception
80+
*/
81+
public function countNameExtra(string $name): int
82+
{
83+
if ($name == '') {
84+
throw new Exception('Empty string!');
85+
}
86+
return strlen($name);
87+
}
88+
}

0 commit comments

Comments
 (0)