Skip to content

Commit 911b2f7

Browse files
Add Throws attribute
1 parent c6014ec commit 911b2f7

23 files changed

+177
-23
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,6 @@ These are the available attributes and their corresponding PHPDoc annotations:
116116
| [TemplateExtends](doc/TemplateExtends.md) | `@extends` `@template-extends` |
117117
| [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements` |
118118
| [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
119+
| [Throws](doc/Throws.md) | `@throws` |
119120
| [Type](doc/Type.md) | `@var` `@return` |
120121

doc/Impure.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ImpureExample
1717
{
1818
public static int $i = 0;
1919

20-
#[Impure]
20+
#[Impure] // this function is impure
2121
public static function addCumulative(int $left) : int
2222
{
2323
self::$i += $left;

doc/IsReadOnly.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use PhpStaticAnalysis\Attributes\IsReadOnly;
1717

1818
class IsReadOnlyExample
1919
{
20-
#[IsReadOnly]
20+
#[IsReadOnly] // this property cannot be written to
2121
public string $name;
2222

2323
...

doc/Method.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ If the class has more than one method that we want to specify, the signatures fo
1919

2020
use PhpStaticAnalysis\Attributes\Method;
2121

22-
#[Method('string getString()')]
22+
#[Method('string getString()')] // these methods are available
2323
#[Method(
2424
'void setString(string $text)',
2525
'static string staticGetter()',

doc/Mixin.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class A
2424
}
2525
}
2626

27-
#[Mixin('A')]
27+
#[Mixin(A::class)] // this class proxies A
2828
class B
2929
{
3030
public function doB(): void

doc/Property.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ If the attribute is used as a replacement for the `Type` attribute for a propert
2525

2626
use PhpStaticAnalysis\Attributes\Property;
2727

28-
#[Property(name: 'string')]
28+
#[Property(name: 'string')] // these properties are available
2929
#[Property('int $age')]
3030
#[Property(
3131
index1: 'string[]',

doc/PropertyRead.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ If the class has more than one property that we want to specify, the types for t
2121

2222
use PhpStaticAnalysis\Attributes\PropertyRead;
2323

24-
#[PropertyRead(name: 'string')]
24+
#[PropertyRead(name: 'string')] // these properties cannot be written to
2525
#[PropertyRead('int $age')]
2626
#[PropertyRead(
2727
index1: 'string[]',

doc/PropertyWrite.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ If the class has more than one property that we want to specify, the types for t
2121

2222
use PhpStaticAnalysis\Attributes\PropertyWrite;
2323

24-
#[PropertyWrite(name: 'string')]
24+
#[PropertyWrite(name: 'string')] // these properties cannot be read
2525
#[PropertyWrite('int $age')]
2626
#[PropertyWrite(
2727
index1: 'string[]',

doc/Pure.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use PhpStaticAnalysis\Attributes\Pure;
1515

1616
class PureExample
1717
{
18-
#[Pure]
18+
#[Pure] // this function is pure
1919
public static function add(int $left, int $right) : int
2020
{
2121
return $left + $right;

doc/RequireExtends.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use PhpStaticAnalysis\Attributes\RequireExtends;
1818
abstract class Parent {
1919
}
2020

21-
#[RequireExtends('ParentClass')]
21+
#[RequireExtends(Parent::class)] // needs to extend this class
2222
trait myTrait {
2323
}
2424

doc/RequireImplements.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ interface RequireInterface
2323
{
2424
}
2525

26-
#[RequireImplements('RequireInterface')]
26+
#[RequireImplements(RequireInterface::class)] //needs to implement this interface
2727
trait MyTrait
2828
{
2929
}

doc/Returns.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use PhpStaticAnalysis\Attributes\Returns;
2121

2222
class ReturnsExample
2323
{
24-
#[Returns('Array<string>')]
24+
#[Returns('Array<string>')] // this is the return type
2525
public function getNames(): array
2626
{
2727
return ['Fred', 'John'];

doc/SelfOut.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class SelfOutExample
2222
{
2323
#[Template('TItemValue')]
2424
#[Param(item: 'TItemValue')]
25-
#[SelfOut('self<TValue|TItemValue>')]
25+
#[SelfOut('self<TValue|TItemValue>')] // this is the new type
2626
public function add($item): void
2727
{
2828
}

doc/TemplateExtends.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ use PhpStaticAnalysis\Attributes\TemplateExtends;
1919
#[Template('T')]
2020
class ParentClass {}
2121

22-
#[TemplateExtends('ParentClass<int>')]
22+
#[TemplateExtends('ParentClass<int>')] // type of extended class
2323
class ChildClass extends ParentClass {}
2424
```

doc/TemplateImplements.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ use PhpStaticAnalysis\Attributes\TemplateImplements;
2323
#[Template('T')]
2424
interface TemplateInterface {}
2525

26-
#[TemplateImplements('TemplateInterface<int>')]
26+
#[TemplateImplements('TemplateInterface<int>')] // this is the type of the implemented interface
2727
class MyClass implements TemplateInterface {}
2828
```

doc/TemplateUse.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use PhpStaticAnalysis\Attributes\TemplateUse;
2525
#[Template('T')]
2626
trait TemplateTrait {}
2727

28-
#[TemplateUse('TemplateTrait<int>')]
28+
#[TemplateUse('TemplateTrait<int>')] // this is the type of the used trait
2929
class MyClass use TemplateInterface {
3030
use TemplateTrait;
3131
}

doc/Throws.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# `Throws` Attribute
2+
3+
This attribute is the equivalent of the `@throws` annotation. It can be applied to a method or function to indicate the exceptions that are thrown by them.
4+
5+
## Arguments
6+
7+
The attribute accepts one or more strings that define the types of exceptions that are thrown. The attribute itself does not have a knowledge of which exceptions are valid and which are not and this will depend on the implementation for each particular tool.
8+
9+
We aim to accept all the exceptions accepted by static analysis tools for the `@throws` annotation.
10+
11+
The arguments need to be unnamed arguments and the value should match the type of the exception thrown by the class.
12+
13+
If the class throws more than one type of exceptions, the types of the different exceptions can either be declared as a list of strings for a single `Throws` attribute or as a list of `Throws` attributes (or even a combination of both, though we don't expect this to be actually used).
14+
15+
## Example usage
16+
17+
```php
18+
<?php
19+
20+
use SpecialException;
21+
use PhpStaticAnalysis\Attributes\Throws;
22+
23+
class MyClass use TemplateInterface {
24+
25+
#[Throws(SpecialException::class)]
26+
public function throwsException(): void
27+
{
28+
throw new SpecialException();
29+
}
30+
}
31+
```

doc/Type.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use PhpStaticAnalysis\Attributes\Type;
2121

2222
class TypeExample
2323
{
24-
#[Type('string')]
24+
#[Type('string')] // the type of this constant
2525
public const ATTRIBUTE_NAME = 'Type';
2626

2727
#[Type('Array<int>')]

src/Throws.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStaticAnalysis\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(
10+
Attribute::TARGET_METHOD |
11+
Attribute::TARGET_FUNCTION |
12+
Attribute::IS_REPEATABLE
13+
)]
14+
final class Throws
15+
{
16+
public function __construct(
17+
string ...$exceptions
18+
) {
19+
}
20+
}

tests/MixinTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ class C
1717
{
1818
}
1919

20-
#[Mixin('A')]
20+
#[Mixin(A::class)]
2121
#[Mixin(
22-
'B',
23-
'C',
22+
B::class,
23+
C::class,
2424
)]
2525
class MixinTest extends TestCase
2626
{

tests/RequireExtendsTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class RequireParentClass
3333
{
3434
}
3535

36-
#[RequireExtends('RequireParentClass')]
36+
#[RequireExtends(RequireParentClass::class)]
3737
trait RequireMyTrait
3838
{
3939
}

tests/RequireImplementsTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ public static function getRequireImplementssFromReflection(
3535
}
3636
}
3737

38-
#[RequireImplements('RequireTestInterface')]
38+
#[RequireImplements(RequireTestInterface::class)]
3939
#[RequireImplements(
40-
'RequireTestInterface2',
41-
'RequireTestInterface3'
40+
RequireTestInterface2::class,
41+
RequireTestInterface3::class
4242
)]
4343
trait RequireInterfaceTrait
4444
{

tests/ThrowsTest.php

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpStaticAnalysis\Attributes\Throws;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class ThrowsTest extends TestCase
9+
{
10+
public function testMethodThrows(): void
11+
{
12+
$this->assertEquals(['Exception'], $this->methodThrows());
13+
}
14+
15+
public function testInvalidTypeMethodThrows(): void
16+
{
17+
$errorThrown = false;
18+
try {
19+
$this->invalidTypeMethodThrows();
20+
} catch (TypeError) {
21+
$errorThrown = true;
22+
}
23+
$this->assertTrue($errorThrown);
24+
}
25+
26+
public function testSeveralMethodThrows(): void
27+
{
28+
$this->assertEquals([
29+
'Exception',
30+
'Exception'
31+
], $this->severalMethodThrowss());
32+
}
33+
34+
public function testMultipleMethodThrows(): void
35+
{
36+
$this->assertEquals([
37+
'Exception',
38+
'Exception'
39+
], $this->multipleMethodThrowss());
40+
}
41+
42+
public function testFunctionThrows(): void
43+
{
44+
$this->assertEquals(['Exception'], functionThrows());
45+
}
46+
47+
#[Throws(Exception::class)]
48+
private function methodThrows(): array
49+
{
50+
return $this->getThrows(__FUNCTION__);
51+
}
52+
53+
#[Throws(0)]
54+
private function invalidTypeMethodThrows(): array
55+
{
56+
return $this->getThrows(__FUNCTION__);
57+
}
58+
59+
#[Throws(
60+
Exception::class,
61+
Exception::class,
62+
)]
63+
private function severalMethodThrowss(): array
64+
{
65+
return $this->getThrows(__FUNCTION__);
66+
}
67+
68+
#[Throws(Exception::class)]
69+
#[Throws(Exception::class)]
70+
private function multipleMethodThrowss(): array
71+
{
72+
return $this->getThrows(__FUNCTION__);
73+
}
74+
75+
private function getThrows(string $functionName): array
76+
{
77+
$reflection = new ReflectionMethod($this, $functionName);
78+
return self::getThrowsFromReflection($reflection);
79+
}
80+
81+
public static function getThrowsFromReflection(
82+
ReflectionMethod | ReflectionFunction $reflection
83+
): array {
84+
$attributes = $reflection->getAttributes();
85+
$throwss = [];
86+
foreach ($attributes as $attribute) {
87+
if ($attribute->getName() === Throws::class) {
88+
$attribute->newInstance();
89+
$throwss = array_merge($throwss, $attribute->getArguments());
90+
}
91+
}
92+
93+
return $throwss;
94+
}
95+
}
96+
97+
#[Throws(Exception::class)]
98+
function functionThrows(): array
99+
{
100+
$reflection = new ReflectionFunction(__FUNCTION__);
101+
return ThrowsTest::getThrowsFromReflection($reflection);
102+
}

0 commit comments

Comments
 (0)