├── .github
├── FUNDING.yml
└── workflows
│ └── all_tests.yml
├── .gitignore
├── src
├── Immutable.php
├── IsReadOnly.php
├── SelfOut.php
├── Impure.php
├── RequireExtends.php
├── TemplateExtends.php
├── Pure.php
├── Returns.php
├── TemplateCovariant.php
├── TemplateContravariant.php
├── Deprecated.php
├── Type.php
├── ImportType.php
├── DefineType.php
├── Method.php
├── Mixin.php
├── TemplateUse.php
├── Internal.php
├── Template.php
├── PropertyRead.php
├── PropertyWrite.php
├── RequireImplements.php
├── TemplateImplements.php
├── Throws.php
├── Property.php
├── Param.php
├── Assert.php
├── ParamOut.php
├── AssertIfTrue.php
└── AssertIfFalse.php
├── ecs.php
├── doc
├── Immutable.md
├── Pure.md
├── Deprecated.md
├── IsReadOnly.md
├── Impure.md
├── Internal.md
├── TemplateExtends.md
├── RequireExtends.md
├── TemplateCovariant.md
├── TemplateContravariant.md
├── SelfOut.md
├── Returns.md
├── Method.md
├── Mixin.md
├── Throws.md
├── TemplateImplements.md
├── RequireImplements.md
├── Template.md
├── ImportType.md
├── TemplateUse.md
├── PropertyRead.md
├── PropertyWrite.md
├── DefineType.md
├── Property.md
├── Assert.md
├── Type.md
├── ParamOut.md
├── Param.md
├── AssertIfTrue.md
└── AssertIfFalse.md
├── psalm.xml
├── phpunit.xml
├── tests
├── MixinTest.php
├── TemplateExtendsTest.php
├── MethodTest.php
├── RequireExtendsTest.php
├── PropertyReadTest.php
├── PropertyWriteTest.php
├── PureTest.php
├── ImpureTest.php
├── ImmutableTest.php
├── DefineTypeTest.php
├── TemplateUseTest.php
├── PropertyTest.php
├── TemplateImplementsTest.php
├── ImportTypeTest.php
├── RequireImplementsTest.php
├── AttributeHelper.php
├── IsReadOnlyTest.php
├── TemplateCovariantTest.php
├── TemplateContravariantTest.php
├── SelfOutTest.php
├── ThrowsTest.php
├── DeprecatedTest.php
├── ReturnsTest.php
├── AssertTest.php
├── TemplateTest.php
├── ParamOutTest.php
├── ParamTest.php
├── TypeTest.php
├── AssertIfTrueTest.php
└── AssertIfFalseTest.php
├── LICENSE
├── phpstan.neon
├── composer.json
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [carlos-granados]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | .phpunit.cache
4 | .idea
5 |
--------------------------------------------------------------------------------
/src/Immutable.php:
--------------------------------------------------------------------------------
1 | withPaths([
9 | __DIR__ . '/src',
10 | __DIR__ . '/tests',
11 | ])
12 | ->withPreparedSets(
13 | psr12: true,
14 | );
15 |
--------------------------------------------------------------------------------
/src/SelfOut.php:
--------------------------------------------------------------------------------
1 | from = $from;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/DefineType.php:
--------------------------------------------------------------------------------
1 | types = $types;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Method.php:
--------------------------------------------------------------------------------
1 | methods = $methods;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Mixin.php:
--------------------------------------------------------------------------------
1 | classes = $classes;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/TemplateUse.php:
--------------------------------------------------------------------------------
1 | traits = $traits;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Internal.php:
--------------------------------------------------------------------------------
1 | properties = $properties;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/PropertyWrite.php:
--------------------------------------------------------------------------------
1 | properties = $properties;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/RequireImplements.php:
--------------------------------------------------------------------------------
1 | interfaces = $interfaces;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/TemplateImplements.php:
--------------------------------------------------------------------------------
1 | interfaces = $interfaces;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Throws.php:
--------------------------------------------------------------------------------
1 | exceptions = $exceptions;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/doc/Pure.md:
--------------------------------------------------------------------------------
1 | # `Pure` Attribute
2 |
3 | This attribute is the equivalent of the `@pure` annotation for class methods and functions.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts no arguments.
8 |
9 | ## Example usage
10 |
11 | ```php
12 | properties = $properties;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Param.php:
--------------------------------------------------------------------------------
1 | params = $params;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Assert.php:
--------------------------------------------------------------------------------
1 | params = $params;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ParamOut.php:
--------------------------------------------------------------------------------
1 | params = $params;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/AssertIfTrue.php:
--------------------------------------------------------------------------------
1 | params = $params;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/AssertIfFalse.php:
--------------------------------------------------------------------------------
1 | params = $params;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/doc/Deprecated.md:
--------------------------------------------------------------------------------
1 | # `Deprecated` Attribute
2 |
3 | This attribute is the equivalent of the `@deprecated` annotation for classes, traits, interfaces, class properties, class methods, class constants and functions.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts no arguments.
8 |
9 | ## Example usage
10 |
11 | ```php
12 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/doc/IsReadOnly.md:
--------------------------------------------------------------------------------
1 | # `IsReadOnly` Attribute
2 |
3 | This attribute is the equivalent of the `@readonly` annotation for class properties.
4 |
5 | We could not use `ReadOnly` for the name of this attribute because `readonly` is a reserved word in PHP.
6 |
7 | ## Arguments
8 |
9 | The attribute accepts no arguments.
10 |
11 | ## Example usage
12 |
13 | ```php
14 |
2 |
14 |
15 |
16 | tests
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/doc/TemplateExtends.md:
--------------------------------------------------------------------------------
1 | # `TemplateExtends` Attribute
2 |
3 | This attribute is the equivalent of the `@extends` or `@template-extends` annotations. It can be applied to a class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one string that defines the type of the templated class that is extended. The attribute itself does not have a knowledge of which class types 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 class types accepted by static analysis tools for the `@template-extends` annotation.
10 |
11 | ## Example usage
12 |
13 | ```php
14 | ')] // type of extended class
23 | class ChildClass extends ParentClass {}
24 | ```
25 |
--------------------------------------------------------------------------------
/doc/RequireExtends.md:
--------------------------------------------------------------------------------
1 | # `RequireExtends` Attribute
2 |
3 | This attribute is the equivalent of the `@require-extends` annotation. It can be applied to a trait to specify that the class using it must extend a specific class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one string that defines the class that needs to be extended. The attribute itself does not have a knowledge of which classes 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 classes accepted by static analysis tools for the `@require-extends` annotation.
10 |
11 | ## Example usage
12 |
13 | ```php
14 | assertEquals(['A', 'B', 'C'], self::getMixinsFromReflection($reflection));
31 | }
32 |
33 | public static function getMixinsFromReflection(
34 | ReflectionClass $reflection
35 | ): array {
36 | $instances = AttributeHelper::getInstances($reflection, Mixin::class);
37 | $mixins = [];
38 | foreach ($instances as $instance) {
39 | $mixins = array_merge($mixins, $instance->classes);
40 | }
41 |
42 | return $mixins;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/doc/TemplateCovariant.md:
--------------------------------------------------------------------------------
1 | # `TemplateCovariant` Attribute
2 |
3 | This attribute is the equivalent of the `@template-covariant` annotation. It can be applied to a class, trait or interface.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one string that defines the name of the type variable and an optional string that defines its type. The attribute itself does not have a knowledge of which type variables 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 type variables accepted by static analysis tools for the `@template-covariant` annotation.
10 |
11 | If the class has more than one type variable, you can add a list of `TemplateCovariant` attributes.
12 |
13 | ## Example usage
14 |
15 | ```php
16 | assertEquals('ParentClass', self::getTemplateExtendssFromReflection($reflection));
15 | }
16 |
17 | public static function getTemplateExtendssFromReflection(
18 | ReflectionClass $reflection
19 | ): string {
20 | $instances = AttributeHelper::getInstances($reflection, TemplateExtends::class);
21 | $extends = '';
22 | foreach ($instances as $instance) {
23 | $extends = $instance->class;
24 | }
25 |
26 | return $extends;
27 | }
28 | }
29 |
30 | #[Template('T')]
31 | class ParentClass
32 | {
33 | }
34 |
35 | #[TemplateExtends('ParentClass')]
36 | class ChildClass extends ParentClass
37 | {
38 | }
39 |
--------------------------------------------------------------------------------
/tests/MethodTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([
19 | 'string getString()',
20 | 'void setString(string $text)',
21 | 'static string staticGetter()',
22 | ], self::getMethodsFromReflection($reflection));
23 | }
24 |
25 | public static function getMethodsFromReflection(
26 | ReflectionClass $reflection
27 | ): array {
28 | $instances = AttributeHelper::getInstances($reflection, Method::class);
29 | $methods = [];
30 | foreach ($instances as $instance) {
31 | $methods = array_merge($methods, $instance->methods);
32 | }
33 |
34 | return $methods;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/RequireExtendsTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('RequireParentClass', self::getRequireExtendssFromReflection($reflection));
14 | }
15 |
16 | public static function getRequireExtendssFromReflection(
17 | ReflectionClass $reflection
18 | ): string {
19 | $instances = AttributeHelper::getInstances($reflection, RequireExtends::class);
20 | $extends = '';
21 | foreach ($instances as $instance) {
22 | $extends = $instance->class;
23 | }
24 |
25 | return $extends;
26 | }
27 | }
28 |
29 | class RequireParentClass
30 | {
31 | }
32 |
33 | #[RequireExtends(RequireParentClass::class)]
34 | trait RequireMyTrait
35 | {
36 | }
37 |
38 | class RequireChildClass extends RequireParentClass
39 | {
40 | use RequireMyTrait;
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 php-static-analysis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/PropertyReadTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([
20 | 0 => 'int $age',
21 | 'name' => 'string',
22 | 'index1' => 'string[]',
23 | 'index2' => 'string[]',
24 | ], self::getPropertiesFromReflection($reflection));
25 | }
26 |
27 | public static function getPropertiesFromReflection(
28 | ReflectionClass $reflection
29 | ): array {
30 | $instances = AttributeHelper::getInstances($reflection, PropertyRead::class);
31 | $properties = [];
32 | foreach ($instances as $instance) {
33 | $properties = array_merge($properties, $instance->properties);
34 | }
35 |
36 | return $properties;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/PropertyWriteTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([
20 | 0 => 'int $age',
21 | 'name' => 'string',
22 | 'index1' => 'string[]',
23 | 'index2' => 'string[]',
24 | ], self::getPropertiesFromReflection($reflection));
25 | }
26 |
27 | public static function getPropertiesFromReflection(
28 | ReflectionClass $reflection
29 | ): array {
30 | $instances = AttributeHelper::getInstances($reflection, PropertyWrite::class);
31 | $properties = [];
32 | foreach ($instances as $instance) {
33 | $properties = array_merge($properties, $instance->properties);
34 | }
35 |
36 | return $properties;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/PureTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($this->methodPure());
13 | }
14 |
15 | public function testPureFunction(): void
16 | {
17 | $this->assertTrue(functionPure());
18 | }
19 |
20 | #[Pure]
21 | private function methodPure(): bool
22 | {
23 | return $this->getPure(__FUNCTION__);
24 | }
25 |
26 | private function getPure(string $methodName): bool
27 | {
28 | $reflection = new ReflectionMethod($this, $methodName);
29 | return self::getPureFromReflection($reflection);
30 | }
31 |
32 | public static function getPureFromReflection(
33 | ReflectionMethod | ReflectionFunction $reflection
34 | ): bool {
35 | return AttributeHelper::getInstances($reflection, Pure::class) !== [];
36 | }
37 | }
38 |
39 | #[Pure]
40 | function functionPure(): bool
41 | {
42 | $reflection = new ReflectionFunction(__FUNCTION__);
43 | return PureTest::getPureFromReflection($reflection);
44 | }
45 |
--------------------------------------------------------------------------------
/tests/ImpureTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($this->methodImpure());
13 | }
14 |
15 | public function testImpureFunction(): void
16 | {
17 | $this->assertTrue(functionImpure());
18 | }
19 |
20 | #[Impure]
21 | private function methodImpure(): bool
22 | {
23 | return $this->getImpure(__FUNCTION__);
24 | }
25 |
26 | private function getImpure(string $methodName): bool
27 | {
28 | $reflection = new ReflectionMethod($this, $methodName);
29 | return self::getImpureFromReflection($reflection);
30 | }
31 |
32 | public static function getImpureFromReflection(
33 | ReflectionMethod | ReflectionFunction $reflection
34 | ): bool {
35 | return AttributeHelper::getInstances($reflection, Impure::class) !== [];
36 | }
37 | }
38 |
39 | #[Impure]
40 | function functionImpure(): bool
41 | {
42 | $reflection = new ReflectionFunction(__FUNCTION__);
43 | return ImpureTest::getImpureFromReflection($reflection);
44 | }
45 |
--------------------------------------------------------------------------------
/doc/SelfOut.md:
--------------------------------------------------------------------------------
1 | # `SelfOut` Attribute
2 |
3 | This attribute is the equivalent of the `@self-out` or `@this-out` annotations. It can be used on class methods to specify the type of the current object after calling a method on it.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts a string which describes the type of the object after returning from the method. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@self-out` annotation.
10 |
11 | ## Example usage
12 |
13 | ```php
14 | ')] // this is the new type
26 | public function add($item): void
27 | {
28 | }
29 | }
30 |
31 | ```
32 |
--------------------------------------------------------------------------------
/doc/Returns.md:
--------------------------------------------------------------------------------
1 | # `Returns` Attribute
2 |
3 | This attribute is the equivalent of the `@return` annotation. It can be used on class methods or on regular functions.
4 |
5 | We could not use `Return` for the name of this attribute because `return` is a reserved word in PHP.
6 |
7 | Instead of using this attribute, you can also use the `Type` attribute which provides equivalent functionality.
8 |
9 | ## Arguments
10 |
11 | The attribute accepts a string which describes the type of the value returned by the function or method. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
12 |
13 | We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@return` annotation.
14 |
15 | ## Example usage
16 |
17 | ```php
18 | ')] // this is the return type
25 | public function getNames(): array
26 | {
27 | return ['Fred', 'John'];
28 | }
29 | }
30 | ```
31 |
--------------------------------------------------------------------------------
/tests/ImmutableTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(self::getImmutableFromReflection($reflection));
15 | }
16 |
17 | public function testTraitImmutable(): void
18 | {
19 | $reflection = new ReflectionClass(ImmutableTestTrait::class);
20 | $this->assertTrue(self::getImmutableFromReflection($reflection));
21 | }
22 |
23 | public function testInterfaceImmutable(): void
24 | {
25 | $reflection = new ReflectionClass(ImmutableTestInterface::class);
26 | $this->assertTrue(self::getImmutableFromReflection($reflection));
27 | }
28 |
29 | public static function getImmutableFromReflection(
30 | ReflectionClass $reflection
31 | ): bool {
32 | return AttributeHelper::getInstances($reflection, Immutable::class) !== [];
33 | }
34 | }
35 |
36 | #[Immutable]
37 | trait ImmutableTestTrait
38 | {
39 | }
40 |
41 | #[Immutable]
42 | interface ImmutableTestInterface
43 | {
44 | }
45 |
46 | class ImmutableClass
47 | {
48 | use ImmutableTestTrait;
49 | }
50 |
--------------------------------------------------------------------------------
/doc/Method.md:
--------------------------------------------------------------------------------
1 | # `Method` Attribute
2 |
3 | This attribute is the equivalent of the `@method` annotation and is used to specify the methods defined through magic `__call` methods, including methods called in a parent class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describe the signature of these methods. The attribute itself does not have a knowledge of which signatures are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept all the signatures accepted by static analysis tools for the `@method` annotation.
10 |
11 | The arguments need to be unnamed arguments.
12 |
13 | If the class has more than one method that we want to specify, the signatures for the different functions can either be declared as a list of strings for a single `Method` attribute or as a list of `Method` 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 | assertEquals([
20 | 0 => 'UserName array{firstName: string, lastName: string}',
21 | 'UserAddress' => 'array{street: string, city: string, zip: string}',
22 | 'StringArray' => 'string[]',
23 | 'IntArray' => 'int[]',
24 | ], self::getPropertiesFromReflection($reflection));
25 | }
26 |
27 | public static function getPropertiesFromReflection(
28 | ReflectionClass $reflection
29 | ): array {
30 | $instances = AttributeHelper::getInstances($reflection, DefineType::class);
31 | $properties = [];
32 | foreach ($instances as $instance) {
33 | $properties = array_merge($properties, $instance->types);
34 | }
35 |
36 | return $properties;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/doc/Mixin.md:
--------------------------------------------------------------------------------
1 | # `Mixin` Attribute
2 |
3 | This attribute is the equivalent of the `@mixin` annotation and is used to specify that the class will proxy the methods and properties of the referenced class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describe the name of the referenced classes. The attribute itself does not have a knowledge of which class names are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | The arguments need to be unnamed arguments.
10 |
11 | If the class has more than one mixin that we want to specify, the names of the referenced classes can either be declared as a list of strings for a single `Mixin` attribute or as a list of `Mixin` attributes (or even a combination of both, though we don't expect this to be actually used).
12 |
13 | ## Example usage
14 |
15 | ```php
16 | $name(...$arguments);
37 | }
38 | }
39 |
40 | $b = new B();
41 | $b->doB();
42 | $b->doA(); // works
43 | ```
44 |
--------------------------------------------------------------------------------
/doc/Throws.md:
--------------------------------------------------------------------------------
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 | ')]
25 | #[TemplateUse(
26 | 'TestTrait2',
27 | 'TestTrait3'
28 | )]
29 | class TemplateUseTest extends TestCase
30 | {
31 | use TestTrait, TestTrait2, TestTrait3;
32 |
33 | public function testClassTemplateUse(): void
34 | {
35 | $reflection = new ReflectionClass($this);
36 | $this->assertEquals([
37 | 'TestTrait',
38 | 'TestTrait2',
39 | 'TestTrait3',
40 | ], self::getTemplateUsesFromReflection($reflection));
41 | }
42 |
43 | public static function getTemplateUsesFromReflection(
44 | ReflectionClass $reflection
45 | ): array {
46 | $instances = AttributeHelper::getInstances($reflection, TemplateUse::class);
47 | $uses = [];
48 | foreach ($instances as $instance) {
49 | $uses = array_merge($uses, $instance->traits);
50 | }
51 |
52 | return $uses;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/doc/TemplateImplements.md:
--------------------------------------------------------------------------------
1 | # `TemplateImplements` Attribute
2 |
3 | This attribute is the equivalent of the `@implements` or `@template-implements` annotations. It can be applied to a class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings that define the type of the templated interfaces that are implemented. The attribute itself does not have a knowledge of which interface types 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 interface types accepted by static analysis tools for the `@template-implements` annotation.
10 |
11 | The arguments need to be unnamed arguments.
12 |
13 | If the class has more than one interface that we want to specify, the types for the different interfaces can either be declared as a list of strings for a single `TemplateInterface` attribute or as a list of `TemplateInterface` 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 | ')] // this is the type of the implemented interface
27 | class MyClass implements TemplateInterface {}
28 | ```
29 |
--------------------------------------------------------------------------------
/doc/RequireImplements.md:
--------------------------------------------------------------------------------
1 | # `RequireImplements` Attribute
2 |
3 | This attribute is the equivalent of the `@require-implements` annotation. It can be applied to a trait to indicate that the class using it should implement one or more interfaces.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings that define the interfaces that need to be implemented. The attribute itself does not have a knowledge of which interfaces 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 interface names accepted by static analysis tools for the `@require-implements` annotation.
10 |
11 | The arguments need to be unnamed arguments.
12 |
13 | If the class has more than one interface that we want to require, the different interfaces can either be declared as a list of strings for a single `RequireInterface` attribute or as a list of `RequireInterface` 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 | assertEquals([
23 | 0 => 'int $age',
24 | 'name' => 'string',
25 | 'index1' => 'string[]',
26 | 'index2' => 'string[]',
27 | ], self::getPropertiesFromReflection($reflection));
28 | }
29 |
30 | public function testPropertyProperties(): void
31 | {
32 | $reflection = new ReflectionProperty($this, 'property');
33 | $this->assertEquals(['string'], self::getPropertiesFromReflection($reflection));
34 | }
35 |
36 | public static function getPropertiesFromReflection(
37 | ReflectionProperty | ReflectionClass $reflection
38 | ): array {
39 | $instances = AttributeHelper::getInstances($reflection, Property::class);
40 | $properties = [];
41 | foreach ($instances as $instance) {
42 | $properties = array_merge($properties, $instance->properties);
43 | }
44 |
45 | return $properties;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/doc/Template.md:
--------------------------------------------------------------------------------
1 | # `Template` Attribute
2 |
3 | This attribute is the equivalent of the `@template` annotation. It can be used on class methods or on regular functions. It can also be applied to a class, trait or interface.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one string that defines the name of the type variable and an optional string that defines its type. The attribute itself does not have a knowledge of which type variables 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 type variables accepted by static analysis tools for the `@template` annotation.
10 |
11 | If the function, method or class has more than one type variable, you can add a list of `Template` attributes.
12 |
13 | ## Example usage
14 |
15 | ```php
16 | ')]
25 | #[TemplateImplements(
26 | 'TestInterface2',
27 | 'TestInterface3'
28 | )]
29 | class TemplateImplementsTest extends TestCase implements TestInterface, TestInterface2, TestInterface3
30 | {
31 | public function testClassTemplateImplements(): void
32 | {
33 | $reflection = new ReflectionClass($this);
34 | $this->assertEquals([
35 | 'TestInterface',
36 | 'TestInterface2',
37 | 'TestInterface3',
38 | ], self::getTemplateImplementssFromReflection($reflection));
39 | }
40 |
41 | public static function getTemplateImplementssFromReflection(
42 | ReflectionClass $reflection
43 | ): array {
44 | $instances = AttributeHelper::getInstances($reflection, TemplateImplements::class);
45 | $implements = [];
46 | foreach ($instances as $instance) {
47 | $implements = array_merge($implements, $instance->interfaces);
48 | }
49 |
50 | return $implements;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/ImportTypeTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([
21 | 0 => 'UserName from User',
22 | 'UserAddress' => 'User',
23 | 'StringArray' => 'User',
24 | 'IntArray' => 'User',
25 | ], self::getPropertiesFromReflection($reflection));
26 | }
27 |
28 | public static function getPropertiesFromReflection(
29 | ReflectionClass $reflection
30 | ): array {
31 | $instances = AttributeHelper::getInstances($reflection, ImportType::class);
32 | $properties = [];
33 | foreach ($instances as $instance) {
34 | $properties = array_merge($properties, $instance->from);
35 | }
36 |
37 | return $properties;
38 | }
39 | }
40 |
41 | #[DefineType(UserAddress: 'array{street: string, city: string, zip: string}')]
42 | #[DefineType('UserName array{firstName: string, lastName: string}')]
43 | #[DefineType(
44 | StringArray: 'string[]',
45 | IntArray: 'int[]',
46 | )]
47 | class User
48 | {
49 | }
50 |
--------------------------------------------------------------------------------
/doc/ImportType.md:
--------------------------------------------------------------------------------
1 | # `ImportType` Attribute
2 |
3 | This attribute is the equivalent of the `@import-type` annotation and is used to import aliases for types from another class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which list the class from which the aliased type needs to be imported. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | The arguments can be named arguments and the type is aliased with the name of the argument and the value is the name of the class from which it needs to be imported.
10 |
11 | They can also be unnamed arguments with a string that contains both the name of the alias and the name of the class from which it needs to be imported, but we recommend using named arguments.
12 |
13 | If the class has more than one type alias that we want to specify, the aliases can either be declared as a list of strings for a single `ImportType` attribute or as a list of `ImportType` 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 | assertEquals([
16 | 'RequireTestInterface',
17 | 'RequireTestInterface2',
18 | 'RequireTestInterface3',
19 | ], self::getRequireImplementssFromReflection($reflection));
20 | }
21 |
22 | public static function getRequireImplementssFromReflection(
23 | ReflectionClass $reflection
24 | ): array {
25 | $instances = AttributeHelper::getInstances($reflection, RequireImplements::class);
26 | $implements = [];
27 | foreach ($instances as $instance) {
28 | $implements = array_merge($implements, $instance->interfaces);
29 | }
30 |
31 | return $implements;
32 | }
33 | }
34 |
35 | #[RequireImplements(RequireTestInterface::class)]
36 | #[RequireImplements(
37 | RequireTestInterface2::class,
38 | RequireTestInterface3::class
39 | )]
40 | trait RequireInterfaceTrait
41 | {
42 | }
43 |
44 | interface RequireTestInterface
45 | {
46 | }
47 |
48 | interface RequireTestInterface2
49 | {
50 | }
51 |
52 | interface RequireTestInterface3
53 | {
54 | }
55 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: max
3 | paths:
4 | - src
5 | - tests
6 |
7 | ignoreErrors:
8 | -
9 | identifier: method.impossibleType
10 | paths:
11 | - tests/AssertIfFalseTest.php
12 |
13 | -
14 | identifier: function.impossibleType
15 | paths:
16 | - tests/AssertIfFalseTest.php
17 |
18 | -
19 | identifier: assert.alreadyNarrowedType
20 | paths:
21 | - tests/AssertIfFalseTest.php
22 | - tests/AssertIfTrueTest.php
23 | - tests/AssertTest.php
24 |
25 | -
26 | identifier: method.alreadyNarrowedType
27 | paths:
28 | - tests/AssertIfTrueTest.php
29 |
30 | -
31 | identifier: function.alreadyNarrowedType
32 | paths:
33 | - tests/AssertIfTrueTest.php
34 |
35 |
36 | -
37 | identifier: missingType.iterableValue
38 | paths:
39 | - tests/*
40 |
41 | -
42 | identifier: missingType.generics
43 | paths:
44 | - tests/*
45 |
46 | -
47 | identifier: phpDoc.parseError
48 | paths:
49 | - tests/*
50 |
51 | -
52 | identifier: possiblyImpure.methodCall
53 | paths:
54 | - tests/PureTest.php
55 |
56 | -
57 | identifier: possiblyImpure.new
58 | paths:
59 | - tests/PureTest.php
60 |
--------------------------------------------------------------------------------
/doc/TemplateUse.md:
--------------------------------------------------------------------------------
1 | # `TemplateUse` Attribute
2 |
3 | This attribute is the equivalent of the `@use` or `@template-use` annotations. It can be applied to a class.
4 |
5 | Please notice that the `@use` annotation is applied to the `use` statement for any trait used by the class. But PHP attributes cannot be applied to `use` statements so it needs to be added at the class level instead.
6 |
7 | ## Arguments
8 |
9 | The attribute accepts one or more strings that define the types of the templated traits that are used. The attribute itself does not have a knowledge of which trait types are valid and which are not and this will depend on the implementation for each particular tool.
10 |
11 | We aim to accept all the trait types accepted by static analysis tools for the `@template-use` annotation.
12 |
13 | The arguments need to be unnamed arguments and the value should match the name of one of the traits used by the class.
14 |
15 | If the class has more than one trait that we want to specify, the types for the different traits can either be declared as a list of strings for a single `TemplateUse` attribute or as a list of `TemplateUse` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | ## Example usage
18 |
19 | ```php
20 | ')] // this is the type of the used trait
29 | class MyClass use TemplateInterface {
30 | use TemplateTrait;
31 | }
32 | ```
33 |
--------------------------------------------------------------------------------
/.github/workflows/all_tests.yml:
--------------------------------------------------------------------------------
1 | name: "All Tests"
2 |
3 | on:
4 | pull_request:
5 | push:
6 |
7 | jobs:
8 | test:
9 | name: "Run all checks for all supported PHP versions"
10 |
11 | runs-on: "ubuntu-22.04"
12 |
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | php-version:
17 | - "8.1"
18 | - "8.2"
19 | - "8.3"
20 | - "8.4"
21 |
22 | steps:
23 | - name: "Checkout"
24 | uses: "actions/checkout@v4"
25 |
26 | - name: "Install PHP"
27 | uses: "shivammathur/setup-php@v2"
28 | with:
29 | php-version: "${{ matrix.php-version }}"
30 | tools: composer
31 |
32 | - name: Get composer cache directory
33 | id: composercache
34 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
35 |
36 | - name: Cache dependencies
37 | uses: actions/cache@v4
38 | with:
39 | path: ${{ steps.composercache.outputs.dir }}
40 | key: "php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}"
41 | restore-keys: "php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}"
42 |
43 | - name: "Install composer dependencies"
44 | run: "COMPOSER_ROOT_VERSION=dev-main composer install --no-interaction --no-progress"
45 |
46 | - name: "Run tests"
47 | run: "composer tests"
--------------------------------------------------------------------------------
/tests/AttributeHelper.php:
--------------------------------------------------------------------------------
1 | $attributeClass
14 | * @return list
15 | */
16 | public static function getInstances(\Reflector $reflector, string $attributeClass): array
17 | {
18 | return array_map(
19 | static fn (\ReflectionAttribute $attribute) => $attribute->newInstance(),
20 | $reflector->getAttributes($attributeClass)
21 | );
22 | }
23 |
24 | /**
25 | * Retrieve attribute instances from a function or method and its parameters.
26 | *
27 | * @template T of object
28 | * @param \ReflectionFunction|\ReflectionMethod $reflector
29 | * @param class-string $attributeClass
30 | * @return array{function: list, parameters: array>}
31 | */
32 | public static function getFunctionInstances(\ReflectionFunctionAbstract $reflector, string $attributeClass): array
33 | {
34 | $function = self::getInstances($reflector, $attributeClass);
35 | $parameters = [];
36 | foreach ($reflector->getParameters() as $parameter) {
37 | $parameters[$parameter->name] = self::getInstances($parameter, $attributeClass);
38 | }
39 | return ['function' => $function, 'parameters' => $parameters];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/doc/PropertyRead.md:
--------------------------------------------------------------------------------
1 | # `PropertyRead` Attribute
2 |
3 | This attribute is the equivalent of the `@property-read` annotation and is used to specify the type of properties accessed through magic `__get` methods. These properties can only be read and not written to. It can also be used to override wrong property types from a parent class.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describe the type of the properties. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@property-read` annotation.
10 |
11 | The arguments can be named arguments and the type is applied to the properties with the same name in the class.
12 |
13 | They can also be unnamed arguments with a string that contains both the type and the name of the property, but we recommend using named arguments.
14 |
15 | If the class has more than one property that we want to specify, the types for the different properties can either be declared as a list of strings for a single `PropertyRead` attribute or as a list of `PropertyRead` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | ## Example usage
18 |
19 | ```php
20 | ` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@property-write` annotation.
10 |
11 | The arguments can be named arguments and the type is applied to the properties with the same name in the class.
12 |
13 | They can also be unnamed arguments with a string that contains both the type and the name of the property, but we recommend using named arguments.
14 |
15 | If the class has more than one property that we want to specify, the types for the different properties can either be declared as a list of strings for a single `PropertyWrite` attribute or as a list of `PropertyWrite` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | ## Example usage
18 |
19 | ```php
20 | property = 'Mike';
24 | $this->propertyWithValue = 'John';
25 | $this->propertyWithMultipleReadOnly = 'Mac';
26 | }
27 |
28 | public function testReadOnlyProperty(): void
29 | {
30 | $this->assertTrue($this->readOnlyProperty());
31 | }
32 |
33 | public function testReadOnlyPropertyWithValue(): void
34 | {
35 | $this->assertTrue($this->readOnlyPropertyWithValue());
36 | }
37 |
38 | public function testMultipleReadOnly(): void
39 | {
40 | $errorThrown = false;
41 | try {
42 | $this->multipleReadOnly();
43 | } catch (Error) {
44 | $errorThrown = true;
45 | }
46 | $this->assertTrue($errorThrown);
47 | }
48 |
49 | private function readOnlyProperty(): bool
50 | {
51 | return $this->getReadOnly('property');
52 | }
53 |
54 | private function readOnlyPropertyWithValue(): bool
55 | {
56 | return $this->getReadOnly('propertyWithValue');
57 | }
58 |
59 | private function multipleReadOnly(): bool
60 | {
61 | return $this->getReadOnly('propertyWithMultipleReadOnly');
62 | }
63 |
64 | private function getReadOnly(string $propertyName): bool
65 | {
66 | $reflection = new ReflectionProperty($this, $propertyName);
67 | return AttributeHelper::getInstances($reflection, IsReadOnly::class) !== [];
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/doc/DefineType.md:
--------------------------------------------------------------------------------
1 | # `DefineType` Attribute
2 |
3 | This attribute is the equivalent of the `@type` annotation and is used to define new aliases for types and they are scoped to the class where they are defined.
4 |
5 | We are not using the `Type` name for this attribute because that name is used for the attribute which is equivalent to the `@var` annotation. But if you prefer, you can use the `Type` attribute instead of this one to define these aliases, but we don't recommend it.
6 |
7 | ## Arguments
8 |
9 | The attribute accepts one or more strings which describe the aliased type. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
10 |
11 | We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@type` annotation.
12 |
13 | The arguments can be named arguments and the type is aliased with the name of the argument.
14 |
15 | They can also be unnamed arguments with a string that contains both the name of the alias and the aliased type, but we recommend using named arguments.
16 |
17 | If the class has more than one type alias that we want to specify, the aliases can either be declared as a list of strings for a single `DefineType` attribute or as a list of `DefineType` attributes (or even a combination of both, though we don't expect this to be actually used).
18 |
19 | ## Example usage
20 |
21 | ```php
22 | assertEquals(['TClass of Exception'], self::getTemplateCovariantsFromReflection($reflection));
16 | }
17 |
18 | public function testTraitTemplateCovariant(): void
19 | {
20 | $reflection = new ReflectionClass(TemplateCovariantTestTrait::class);
21 | $this->assertEquals(['TTrait'], self::getTemplateCovariantsFromReflection($reflection));
22 | }
23 |
24 | public function testInterfaceTemplateCovariant(): void
25 | {
26 | $reflection = new ReflectionClass(TemplateCovariantTestInterface::class);
27 | $this->assertEquals(['TInterface'], self::getTemplateCovariantsFromReflection($reflection));
28 | }
29 |
30 | public static function getTemplateCovariantsFromReflection(
31 | ReflectionClass $reflection
32 | ): array {
33 | $instances = AttributeHelper::getInstances($reflection, TemplateCovariant::class);
34 | $templates = [];
35 | foreach ($instances as $instance) {
36 | $templateValue = $instance->name;
37 | if ($instance->of !== null) {
38 | $templateValue .= ' of ' . $instance->of;
39 | }
40 | $templates[] = $templateValue;
41 | }
42 |
43 | return $templates;
44 | }
45 | }
46 |
47 | #[TemplateCovariant('TTrait')]
48 | trait TemplateCovariantTestTrait
49 | {
50 | }
51 |
52 | #[TemplateCovariant('TInterface')]
53 | interface TemplateCovariantTestInterface
54 | {
55 | }
56 |
57 | #[TemplateUse('TemplateCovariantTestTrait')]
58 | class CovariantClass
59 | {
60 | use TemplateCovariantTestTrait;
61 | }
62 |
--------------------------------------------------------------------------------
/doc/Property.md:
--------------------------------------------------------------------------------
1 | # `Property` Attribute
2 |
3 | This attribute is the equivalent of the `@property` annotation and is used to specify the type of properties accessed through magic `__get/__set` methods. It can also be used to override wrong property types from a parent class.
4 |
5 | This attribute can also be used instead of the `Type` attribute to specify the type of a class property, replacing the `@var` annotation.
6 |
7 | ## Arguments
8 |
9 | The attribute accepts one or more strings which describe the type of the properties. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
10 |
11 | We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@property` annotation.
12 |
13 | The arguments can be named arguments and the type is applied to the properties with the same name in the class.
14 |
15 | They can also be unnamed arguments with a string that contains both the type and the name of the property, but we recommend using named arguments.
16 |
17 | If the class has more than one property that we want to specify, the types for the different properties can either be declared as a list of strings for a single `Property` attribute or as a list of `Property` attributes (or even a combination of both, though we don't expect this to be actually used).
18 |
19 | If the attribute is used as a replacement for the `Type` attribute for a property, then you should use a single unnamed argument.
20 |
21 | ## Example usage
22 |
23 | ```php
24 | ')]
37 | private array $nums;
38 |
39 | ...
40 | }
41 | ```
42 |
--------------------------------------------------------------------------------
/tests/TemplateContravariantTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['TClass of Exception'], self::getTemplateContravariantsFromReflection($reflection));
16 | }
17 |
18 | public function testTraitTemplateContravariant(): void
19 | {
20 | $reflection = new ReflectionClass(TemplateContravariantTestTrait::class);
21 | $this->assertEquals(['TTrait'], self::getTemplateContravariantsFromReflection($reflection));
22 | }
23 |
24 | public function testInterfaceTemplateContravariant(): void
25 | {
26 | $reflection = new ReflectionClass(TemplateContravariantTestInterface::class);
27 | $this->assertEquals(['TInterface'], self::getTemplateContravariantsFromReflection($reflection));
28 | }
29 |
30 | public static function getTemplateContravariantsFromReflection(
31 | ReflectionClass $reflection
32 | ): array {
33 | $instances = AttributeHelper::getInstances($reflection, TemplateContravariant::class);
34 | $templates = [];
35 | foreach ($instances as $instance) {
36 | $templateValue = $instance->name;
37 | if ($instance->of !== null) {
38 | $templateValue .= ' of ' . $instance->of;
39 | }
40 | $templates[] = $templateValue;
41 | }
42 |
43 | return $templates;
44 | }
45 | }
46 |
47 | #[TemplateContravariant('TTrait')]
48 | trait TemplateContravariantTestTrait
49 | {
50 | }
51 |
52 | #[TemplateContravariant('TInterface')]
53 | interface TemplateContravariantTestInterface
54 | {
55 | }
56 |
57 | #[TemplateUse('TemplateContravariantTestTrait')]
58 | class ContravariantClass
59 | {
60 | use TemplateContravariantTestTrait;
61 | }
62 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "php-static-analysis/attributes",
3 | "description": "Attributes used instead of PHPDocs for static analysis tools",
4 | "type": "library",
5 | "license": "MIT",
6 | "autoload": {
7 | "psr-4": {
8 | "PhpStaticAnalysis\\Attributes\\": "src/"
9 | }
10 | },
11 | "autoload-dev": {
12 | "files": ["tests/AttributeHelper.php"]
13 | },
14 | "authors": [
15 | {
16 | "name": "Carlos Granados",
17 | "email": "carlos@fastdebug.io"
18 | }
19 | ],
20 | "minimum-stability": "dev",
21 | "prefer-stable": true,
22 | "require": {
23 | "php": ">=8.1"
24 | },
25 | "require-dev": {
26 | "php-static-analysis/node-visitor": "^0.5.0 || dev-main",
27 | "php-static-analysis/phpstan-extension": "^0.5.0 || dev-main",
28 | "php-static-analysis/psalm-plugin": "^0.5.0 || dev-main",
29 | "phpstan/extension-installer": "^1.3",
30 | "phpstan/phpstan": "^2.0",
31 | "phpunit/phpunit": "^9.0",
32 | "symplify/easy-coding-standard": "^12.1",
33 | "vimeo/psalm": "^6.12"
34 | },
35 | "scripts": {
36 | "phpstan": "phpstan analyse",
37 | "phpstan-debug": "phpstan analyse --xdebug --debug",
38 | "ecs": "ecs",
39 | "ecs-fix": "ecs --fix",
40 | "phpunit": "phpunit",
41 | "psalm": "psalm",
42 | "tests": [
43 | "@ecs",
44 | "@phpstan",
45 | "@phpunit",
46 | "@psalm"
47 | ]
48 | },
49 | "config": {
50 | "allow-plugins": {
51 | "phpstan/extension-installer": true,
52 | "php-static-analysis/psalm-plugin": true
53 | },
54 | "sort-packages": true
55 | },
56 | "suggest": {
57 | "php-static-analysis/phpstan-extension": "PHPStan extension to read static analysis attributes",
58 | "php-static-analysis/psalm-plugin": "Psalm plugin to read static analysis attributes",
59 | "php-static-analysis/rector-rule": "RectorPHP rule to convert PHPDoc annotations to static analysis attributes"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/doc/Assert.md:
--------------------------------------------------------------------------------
1 | # `Assert` Attribute
2 |
3 | This attribute is the equivalent of the `@assert` annotation. It can be used on class methods or on regular functions.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describes the assertion that is performed on the parameter. The attribute itself does not have a knowledge of which assertions are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept all the types accepted by static analysis tools for the `@assert` annotation.
10 |
11 | The arguments can be named arguments and the assertion is applied to the parameter with the same name in the function or the class.
12 |
13 | You can also pass an unnamed argument with a string that contains both the assertion and the name of the parameter, but we recommend using named arguments.
14 |
15 | If the function or method has more than one parameter, the assertions for the different parameters can either be declared as a list of strings for a single `Assert` attribute or as a list of `Assert` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | You can also directly apply the attribute to any of the method/function parameters. In that case, the name of the argument is optional and, if added, should match the name of the parameter to which it is applied.
18 |
19 | ## Example usage
20 |
21 | ```php
22 | ` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@var` annotation.
16 |
17 | If used to replace the `@type` tag, the value should be a string that includes both the name of the alias and the type being aliased.
18 |
19 | ## Example usage
20 |
21 | ```php
22 | ')]
33 | private array $nums;
34 |
35 | #[Type('Array')]
36 | private function returnsArray()
37 | {
38 | return [1];
39 | }
40 | ...
41 | }
42 | ```
43 |
44 | ## Caveat
45 |
46 | This attribute can only be used to specify a type for class properties or class constants. It cannot replace the `@var` annotation when applied to define the type of a variable within arbitrary PHP code like in this example:
47 |
48 | ```php
49 | /** @var Array $result */
50 | $result = $this->getResult();
51 | ```
52 |
53 | This is because PHP attributes cannot be applied to arbitrary code, they can only be applied to specific targets like classes, functions, methods or properties. So the `@var` annotation might still be needed. However, if your code has good type coverage, ideally you should never need to use this kind of annotation.
--------------------------------------------------------------------------------
/tests/SelfOutTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('self', $this->methodSelfOut());
15 | }
16 |
17 | public function testMethodSelfOutArray(): void
18 | {
19 | $this->assertEquals(['self'], $this->methodSelfOutArray());
20 | }
21 |
22 | public function testInvalidTypeMethodSelfOut(): void
23 | {
24 | $errorThrown = false;
25 | try {
26 | $this->invalidTypeMethodSelfOut();
27 | } catch (TypeError) {
28 | $errorThrown = true;
29 | }
30 | $this->assertTrue($errorThrown);
31 | }
32 |
33 | public function testMethodSelfOutWithTooManyParameters(): void
34 | {
35 | $this->assertEquals('self', $this->methodSelfOutWithTooManyParameters());
36 | }
37 |
38 | public function testMultipleMethodSelfOut(): void
39 | {
40 | $errorThrown = false;
41 | try {
42 | $this->multipleMethodSelfOut();
43 | } catch (Error) {
44 | $errorThrown = true;
45 | }
46 | $this->assertTrue($errorThrown);
47 | }
48 |
49 | #[SelfOut('self')]
50 | private function methodSelfOut(): string
51 | {
52 | return $this->getSelfOut(__FUNCTION__);
53 | }
54 |
55 | #[SelfOut('self')]
56 | private function methodSelfOutArray(): array
57 | {
58 | return [$this->getSelfOut(__FUNCTION__)];
59 | }
60 |
61 | #[SelfOut(0)]
62 | private function invalidTypeMethodSelfOut(): string
63 | {
64 | return $this->getSelfOut(__FUNCTION__);
65 | }
66 |
67 | #[SelfOut('self', 'string')]
68 | private function methodSelfOutWithTooManyParameters(): string
69 | {
70 | return $this->getSelfOut(__FUNCTION__);
71 | }
72 |
73 | #[SelfOut('self')]
74 | #[SelfOut('self')]
75 | private function multipleMethodSelfOut(): string
76 | {
77 | return $this->getSelfOut(__FUNCTION__);
78 | }
79 |
80 | private function getSelfOut(string $methodName): string
81 | {
82 | $reflection = new ReflectionMethod($this, $methodName);
83 | $instances = AttributeHelper::getInstances($reflection, SelfOut::class);
84 | $selfOut = '';
85 | foreach ($instances as $instance) {
86 | $selfOut = $instance->type;
87 | }
88 |
89 | return $selfOut;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/doc/ParamOut.md:
--------------------------------------------------------------------------------
1 | # `ParamOut` Attribute
2 |
3 | This attribute is the equivalent of the `@param-out` annotation and is used to specify the output type of a parameter passed by reference. It can be used on class methods or on regular functions.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describes the output types of the parameters. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@param-out` annotation.
10 |
11 | The arguments can be named arguments and the output type is applied to the parameter with the same name in the function or the class.
12 |
13 | You can also pass an unnamed argument with a string that contains both the type and the name of the parameter, but we recommend using named arguments.
14 |
15 | If the function or method has more than one parameter passed by reference, the output types for the different parameters can either be declared as a list of strings for a single `ParamOut` attribute or as a list of `ParamOut` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | You can also directly apply the attribute to any of the method/function parameters. In that case, the name of the argument is optional and, if added, should match the name of the parameter to which it is applied.
18 |
19 | ## Example usage
20 |
21 | ```php
22 | ` or `Collection`. We aim to accept all the types accepted by static analysis tools for the `@param` annotation.
10 |
11 | The arguments can be named arguments and the type is applied to the parameter with the same name in the function or the class.
12 |
13 | You can also pass an unnamed argument with a string that contains both the type and the name of the parameter, but we recommend using named arguments.
14 |
15 | If the function or method has more than one parameter, the types for the different parameters can either be declared as a list of strings for a single `Param` attribute or as a list of `Param` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | If any of the parameters is variadic, the `...` operator needs to be listed with the type, not the argument name.
18 |
19 | You can also directly apply the attribute to any of the method/function parameters. In that case, the name of the argument is optional and, if added, should match the name of the parameter to which it is applied.
20 |
21 | ## Example usage
22 |
23 | ```php
24 | 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 | $instances = AttributeHelper::getInstances($reflection, Throws::class);
85 | $throwss = [];
86 | foreach ($instances as $instance) {
87 | $throwss = array_merge($throwss, $instance->exceptions);
88 | }
89 |
90 | return $throwss;
91 | }
92 | }
93 |
94 | #[Throws(Exception::class)]
95 | function functionThrows(): array
96 | {
97 | $reflection = new ReflectionFunction(__FUNCTION__);
98 | return ThrowsTest::getThrowsFromReflection($reflection);
99 | }
100 |
--------------------------------------------------------------------------------
/doc/AssertIfTrue.md:
--------------------------------------------------------------------------------
1 | # `AssertIfTrue` Attribute
2 |
3 | This attribute is the equivalent of the `@assert-if-true` annotation. It can be used on class methods or on regular functions.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describes the assertion that is performed on the parameter and that will the function return true. The attribute itself does not have a knowledge of which assertions are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept all the types accepted by static analysis tools for the `@assert-if-true` annotation.
10 |
11 | The arguments can be named arguments and the assertion is applied to the parameter with the same name in the function or the class.
12 |
13 | You can also pass an unnamed argument with a string that contains both the assertion and the name of the parameter, but we recommend using named arguments. This later form can also be used to pass more complex assertions on members of the class, for example.
14 |
15 | If the function or method has more than one parameter, the assertions for the different parameters can either be declared as a list of strings for a single `AssertIfTrue` attribute or as a list of `AssertIfTrue` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | You can also directly apply the attribute to any of the method/function parameters. In that case, the name of the argument is optional and, if added, should match the name of the parameter to which it is applied.
18 |
19 | ## Example usage
20 |
21 | ```php
22 | getName()')]
42 | public function methodThatAssertsThatNameIsNotNull($param): bool
43 | {
44 | }
45 |
46 | // Multiple params listed in a single attribute
47 | #[AssertIfTrue(
48 | param1: 'string',
49 | param2: 'Foo',
50 | )]
51 | public function methodThatAssertsBothParameters($param1, $param2): bool
52 | {
53 | }
54 |
55 | // Multiple params listed in multiple attributes
56 | #[AssertIfTrue(param1: 'string')]
57 | #[AssertIfTrue(param2: 'Foo')]
58 | public function methodThatAssertsBothParameters($param1, $param2): bool
59 | {
60 | }
61 |
62 | // Attribute applied at parameter level
63 | public function assertOnParam(
64 | #[AssertIfTrue('string')]
65 | array $param
66 | ): bool {
67 | }
68 | }
69 | ```
70 |
--------------------------------------------------------------------------------
/doc/AssertIfFalse.md:
--------------------------------------------------------------------------------
1 | # `AssertIfFalse` Attribute
2 |
3 | This attribute is the equivalent of the `@assert-if-false` annotation. It can be used on class methods or on regular functions.
4 |
5 | ## Arguments
6 |
7 | The attribute accepts one or more strings which describes the assertion that is performed on the parameter and that will the function return false. The attribute itself does not have a knowledge of which assertions are valid and which are not and this will depend on the implementation for each particular tool.
8 |
9 | We expect that the attribute will be able to accept all the types accepted by static analysis tools for the `@assert-if-false` annotation.
10 |
11 | The arguments can be named arguments and the assertion is applied to the parameter with the same name in the function or the class.
12 |
13 | You can also pass an unnamed argument with a string that contains both the assertion and the name of the parameter, but we recommend using named arguments. This later form can also be used to pass more complex assertions on members of the class, for example.
14 |
15 | If the function or method has more than one parameter, the assertions for the different parameters can either be declared as a list of strings for a single `AssertIfFalse` attribute or as a list of `AssertIfFalse` attributes (or even a combination of both, though we don't expect this to be actually used).
16 |
17 | You can also directly apply the attribute to any of the method/function parameters. In that case, the name of the argument is optional and, if added, should match the name of the parameter to which it is applied.
18 |
19 | ## Example usage
20 |
21 | ```php
22 | getName()')]
42 | public function methodThatAssertsThatNameIsNotNull($param): bool
43 | {
44 | }
45 |
46 | // Multiple params listed in a single attribute
47 | #[AssertIfFalse(
48 | param1: 'string',
49 | param2: 'Foo',
50 | )]
51 | public function methodThatAssertsBothParameters($param1, $param2): bool
52 | {
53 | }
54 |
55 | // Multiple params listed in multiple attributes
56 | #[AssertIfFalse(param1: 'string')]
57 | #[AssertIfFalse(param2: 'Foo')]
58 | public function methodThatAssertsBothParameters($param1, $param2): bool
59 | {
60 | }
61 |
62 | // Attribute applied at parameter level
63 | public function assertOnParam(
64 | #[AssertIfFalse('string')]
65 | array $param
66 | ): bool {
67 | }
68 | }
69 | ```
70 |
--------------------------------------------------------------------------------
/tests/DeprecatedTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(self::getDeprecatedFromReflection($reflection));
18 | }
19 |
20 | public function testClassDeprecated(): void
21 | {
22 | $reflection = new ReflectionClass($this);
23 | $this->assertTrue(self::getDeprecatedFromReflection($reflection));
24 | }
25 |
26 | public function testTraitDeprecated(): void
27 | {
28 | $reflection = new ReflectionClass(DeprecatedTestTrait::class);
29 | $this->assertTrue(self::getDeprecatedFromReflection($reflection));
30 | }
31 |
32 | public function testInterfaceDeprecated(): void
33 | {
34 | $reflection = new ReflectionClass(DeprecatedTestInterface::class);
35 | $this->assertTrue(self::getDeprecatedFromReflection($reflection));
36 | }
37 |
38 | public function testMethodDeprecated(): void
39 | {
40 | $this->assertTrue($this->methodDeprecated());
41 | }
42 |
43 | public function testInvalidTypeMethodDeprecated(): void
44 | {
45 | $errorThrown = false;
46 | try {
47 | $this->invalidTypeMethodDeprecated();
48 | } catch (Error $e) {
49 | $errorThrown = true;
50 | }
51 | $this->assertTrue($errorThrown);
52 | }
53 |
54 | public function testFunctionDeprecated(): void
55 | {
56 | $this->assertTrue(functionDeprecated());
57 | }
58 |
59 | #[Deprecated]
60 | private function methodDeprecated(): bool
61 | {
62 | return $this->getDeprecated(__FUNCTION__);
63 | }
64 |
65 | #[Deprecated]
66 | #[Deprecated]
67 | private function invalidTypeMethodDeprecated(): bool
68 | {
69 | return $this->getDeprecated(__FUNCTION__);
70 | }
71 |
72 | private function getDeprecated(string $methodName): bool
73 | {
74 | $reflection = new ReflectionMethod($this, $methodName);
75 | return self::getDeprecatedFromReflection($reflection);
76 | }
77 |
78 | public static function getDeprecatedFromReflection(
79 | ReflectionMethod | ReflectionFunction | ReflectionClass | ReflectionProperty $reflection
80 | ): bool {
81 | return AttributeHelper::getInstances($reflection, Deprecated::class) !== [];
82 | }
83 | }
84 |
85 | #[Deprecated]
86 | trait DeprecatedTestTrait
87 | {
88 | }
89 |
90 | #[Deprecated]
91 | interface DeprecatedTestInterface
92 | {
93 | }
94 |
95 | #[Deprecated]
96 | function functionDeprecated(): bool
97 | {
98 | $reflection = new ReflectionFunction(__FUNCTION__);
99 | return DeprecatedTest::getDeprecatedFromReflection($reflection);
100 | }
101 |
102 | class DeprecatedClass
103 | {
104 | use DeprecatedTestTrait;
105 | }
106 |
--------------------------------------------------------------------------------
/tests/ReturnsTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('string', $this->methodReturns());
13 | }
14 |
15 | public function testMethodReturnsArray(): void
16 | {
17 | $this->assertEquals(['string[]'], $this->methodReturnsArray());
18 | }
19 |
20 | public function testInvalidTypeMethodReturns(): void
21 | {
22 | $errorThrown = false;
23 | try {
24 | $this->invalidTypeMethodReturns();
25 | } catch (TypeError) {
26 | $errorThrown = true;
27 | }
28 | $this->assertTrue($errorThrown);
29 | }
30 |
31 | public function testMethodReturnsWithTooManyParameters(): void
32 | {
33 | $this->assertEquals('string', $this->methodReturnsWithTooManyParameters());
34 | }
35 |
36 | public function testMultipleMethodReturns(): void
37 | {
38 | $errorThrown = false;
39 | try {
40 | $this->multipleMethodReturns();
41 | } catch (Error) {
42 | $errorThrown = true;
43 | }
44 | $this->assertTrue($errorThrown);
45 | }
46 |
47 | public function testFunctionReturns(): void
48 | {
49 | $this->assertEquals('string', functionReturns());
50 | }
51 |
52 | #[Returns('string')]
53 | private function methodReturns(): string
54 | {
55 | return $this->getReturns(__FUNCTION__);
56 | }
57 |
58 | #[Returns('string[]')]
59 | private function methodReturnsArray(): array
60 | {
61 | return [$this->getReturns(__FUNCTION__)];
62 | }
63 |
64 | #[Returns(0)]
65 | private function invalidTypeMethodReturns(): string
66 | {
67 | return $this->getReturns(__FUNCTION__);
68 | }
69 |
70 | #[Returns('string', 'string')]
71 | private function methodReturnsWithTooManyParameters(): string
72 | {
73 | return $this->getReturns(__FUNCTION__);
74 | }
75 |
76 | #[Returns('string')]
77 | #[Returns('string')]
78 | private function multipleMethodReturns(): string
79 | {
80 | return $this->getReturns(__FUNCTION__);
81 | }
82 |
83 | private function getReturns(string $methodName): string
84 | {
85 | $reflection = new ReflectionMethod($this, $methodName);
86 | return self::getReturnsFromReflection($reflection);
87 | }
88 |
89 | public static function getReturnsFromReflection(
90 | ReflectionMethod | ReflectionFunction $reflection
91 | ): string {
92 | $instances = AttributeHelper::getInstances($reflection, Returns::class);
93 | $returns = '';
94 | foreach ($instances as $instance) {
95 | $returns = $instance->type;
96 | }
97 |
98 | return $returns;
99 | }
100 | }
101 |
102 | #[Returns('string')]
103 | function functionReturns(): string
104 | {
105 | $reflection = new ReflectionFunction(__FUNCTION__);
106 | return ReturnsTest::getReturnsFromReflection($reflection);
107 | }
108 |
--------------------------------------------------------------------------------
/tests/AssertTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['param' => 'string'], $this->methodAssert('Test'));
13 | }
14 |
15 | public function testUnnamedMethodAssert(): void
16 | {
17 | $this->assertEquals(['string $param'], $this->unnamedMethodAssert('Test'));
18 | }
19 |
20 | public function testInvalidTypeMethodAssert(): void
21 | {
22 | $errorThrown = false;
23 | try {
24 | $this->invalidTypeMethodAssert('Test');
25 | } catch (TypeError) {
26 | $errorThrown = true;
27 | }
28 | $this->assertTrue($errorThrown);
29 | }
30 |
31 | public function testSeveralMethodAsserts(): void
32 | {
33 | $this->assertEquals([
34 | 'param1' => 'string',
35 | 'param2' => 'string'
36 | ], $this->severalMethodAsserts('Test', 'Test'));
37 | }
38 |
39 | public function testMultipleMethodAsserts(): void
40 | {
41 | $this->assertEquals([
42 | 'param1' => 'string',
43 | 'param2' => 'string'
44 | ], $this->multipleMethodAsserts('Test', 'Test'));
45 | }
46 |
47 | public function testFunctionAssert(): void
48 | {
49 | $this->assertEquals(['param' => 'string'], functionAssert('Test'));
50 | }
51 |
52 | public function testAssertOnParam(): void
53 | {
54 | $this->assertEquals(['param' => 'string'], $this->assertOnParam('Test'));
55 | }
56 |
57 | #[Assert(param: 'string')]
58 | private function methodAssert(string $param): array
59 | {
60 | return $this->getAsserts(__FUNCTION__);
61 | }
62 |
63 | #[Assert('string $param')]
64 | private function unnamedMethodAssert(string $param): array
65 | {
66 | return $this->getAsserts(__FUNCTION__);
67 | }
68 |
69 | #[Assert(0)]
70 | private function invalidTypeMethodAssert(string $param): array
71 | {
72 | return $this->getAsserts(__FUNCTION__);
73 | }
74 |
75 | #[Assert(
76 | param1: 'string',
77 | param2: 'string',
78 | )]
79 | private function severalMethodAsserts(string $param1, string $param2): array
80 | {
81 | return $this->getAsserts(__FUNCTION__);
82 | }
83 |
84 | #[Assert(param1: 'string')]
85 | #[Assert(param2: 'string')]
86 | private function multipleMethodAsserts(string $param1, string $param2): array
87 | {
88 | return $this->getAsserts(__FUNCTION__);
89 | }
90 |
91 | private function assertOnParam(
92 | #[Assert('string')]
93 | string $param
94 | ): array {
95 | return $this->getAsserts(__FUNCTION__);
96 | }
97 |
98 | private function getAsserts(string $functionName): array
99 | {
100 | $reflection = new ReflectionMethod($this, $functionName);
101 | return self::getAssertsFromReflection($reflection);
102 | }
103 |
104 | public static function getAssertsFromReflection(
105 | ReflectionMethod | ReflectionFunction $reflection
106 | ): array {
107 | $instances = AttributeHelper::getFunctionInstances($reflection, Assert::class);
108 | $asserts = [];
109 |
110 | foreach ($instances['function'] as $instance) {
111 | $asserts = array_merge($asserts, $instance->params);
112 | }
113 |
114 | foreach ($instances['parameters'] as $name => $attrs) {
115 | foreach ($attrs as $instance) {
116 | $argument = $instance->params[array_key_first($instance->params)];
117 | $asserts[$name] = $argument;
118 | }
119 | }
120 |
121 | return $asserts;
122 | }
123 | }
124 |
125 | #[Assert(param: 'string')]
126 | function functionAssert(string $param): array
127 | {
128 | $reflection = new ReflectionFunction(__FUNCTION__);
129 | return AssertTest::getAssertsFromReflection($reflection);
130 | }
131 |
--------------------------------------------------------------------------------
/tests/TemplateTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['TClass'], self::getTemplatesFromReflection($reflection));
17 | }
18 |
19 | public function testTraitTemplate(): void
20 | {
21 | $reflection = new ReflectionClass(TemplateTestTrait::class);
22 | $this->assertEquals(['TTrait'], self::getTemplatesFromReflection($reflection));
23 | }
24 |
25 | public function testInterfaceTemplate(): void
26 | {
27 | $reflection = new ReflectionClass(TemplateTestInterface::class);
28 | $this->assertEquals(['TInterface'], self::getTemplatesFromReflection($reflection));
29 | }
30 |
31 | public function testMethodTemplate(): void
32 | {
33 | $this->assertEquals(['TMethod'], $this->methodTemplate('Test'));
34 | }
35 |
36 | public function testMethodTemplateWithType(): void
37 | {
38 | $this->assertEquals(['TMethod of Exception'], $this->methodTemplateWithType('Test'));
39 | }
40 |
41 | public function testInvalidTypeMethodTemplate(): void
42 | {
43 | $errorThrown = false;
44 | try {
45 | $this->invalidTypeMethodTemplate();
46 | } catch (TypeError) {
47 | $errorThrown = true;
48 | }
49 | $this->assertTrue($errorThrown);
50 | }
51 |
52 | public function testMethodWithMultipleTemplates(): void
53 | {
54 | $this->assertEquals(['T1', 'T2'], $this->methodWithMultipleTemplates('Test', 'Test'));
55 | }
56 |
57 | public function testFunctionTemplate(): void
58 | {
59 | $this->assertEquals(['TFunction'], functionTemplate('Test'));
60 | }
61 |
62 | #[Template('TMethod')]
63 | #[Param(param: 'TMethod')]
64 | private function methodTemplate($param): array
65 | {
66 | return $this->getTemplates(__FUNCTION__);
67 | }
68 |
69 | #[Template('TMethod', Exception::class)]
70 | #[Param(param: 'TMethod')]
71 | private function methodTemplateWithType($param): array
72 | {
73 | return $this->getTemplates(__FUNCTION__);
74 | }
75 |
76 | #[Template(0)]
77 | private function invalidTypeMethodTemplate(): array
78 | {
79 | return $this->getTemplates(__FUNCTION__);
80 | }
81 |
82 | #[Template('T1')]
83 | #[Template('T2')]
84 | #[Param(param1: 'T1')]
85 | #[Param(param2: 'T2')]
86 | private function methodWithMultipleTemplates($param1, $param2): array
87 | {
88 | return $this->getTemplates(__FUNCTION__);
89 | }
90 |
91 | private function getTemplates(string $methodName): array
92 | {
93 | $reflection = new ReflectionMethod($this, $methodName);
94 | return self::getTemplatesFromReflection($reflection);
95 | }
96 |
97 | public static function getTemplatesFromReflection(
98 | ReflectionMethod | ReflectionFunction | ReflectionClass $reflection
99 | ): array {
100 | $instances = AttributeHelper::getInstances($reflection, Template::class);
101 | $templates = [];
102 | foreach ($instances as $instance) {
103 | $templateValue = $instance->name;
104 | if ($instance->of !== null) {
105 | $templateValue .= ' of ' . $instance->of;
106 | }
107 | $templates[] = $templateValue;
108 | }
109 |
110 | return $templates;
111 | }
112 | }
113 |
114 | #[Template('TTrait')]
115 | trait TemplateTestTrait
116 | {
117 | }
118 |
119 | #[Template('TInterface')]
120 | interface TemplateTestInterface
121 | {
122 | }
123 |
124 | #[TemplateUse('TemplateTestTrait')]
125 | class TemplateClass
126 | {
127 | use TemplateTestTrait;
128 | }
129 |
130 |
131 | #[Template('TFunction')]
132 | #[Param(param: 'TFunction')]
133 | function functionTemplate($param): array
134 | {
135 | $reflection = new ReflectionFunction(__FUNCTION__);
136 | return TemplateTest::getTemplatesFromReflection($reflection);
137 | }
138 |
--------------------------------------------------------------------------------
/tests/ParamOutTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['param' => 'string'], $this->methodParamOut($text));
14 | }
15 |
16 | public function testUnnamedMethodParamOut(): void
17 | {
18 | $text = 'Test';
19 | $this->assertEquals(['string $param'], $this->unnamedMethodParamOut($text));
20 | }
21 |
22 | public function testInvalidTypeMethodParamOut(): void
23 | {
24 | $errorThrown = false;
25 | $text = 'Test';
26 | try {
27 | $this->invalidTypeMethodParamOut($text);
28 | } catch (TypeError) {
29 | $errorThrown = true;
30 | }
31 | $this->assertTrue($errorThrown);
32 | }
33 |
34 | public function testSeveralMethodParamOuts(): void
35 | {
36 | $text = 'Test';
37 | $this->assertEquals([
38 | 'param1' => 'string',
39 | 'param2' => 'string'
40 | ], $this->severalMethodParamOuts($text, $text));
41 | }
42 |
43 | public function testMultipleMethodParamOuts(): void
44 | {
45 | $text = 'Test';
46 | $this->assertEquals([
47 | 'param1' => 'string',
48 | 'param2' => 'string'
49 | ], $this->multipleMethodParamOuts($text, $text));
50 | }
51 |
52 | public function testFunctionParamOut(): void
53 | {
54 | $text = 'Test';
55 | $this->assertEquals(['param' => 'string'], functionParamOut($text));
56 | }
57 |
58 | public function testParamOutOnParam(): void
59 | {
60 | $text = 'Test';
61 | $this->assertEquals(['param' => 'string'], $this->paramOutOnParam($text));
62 | }
63 |
64 | #[ParamOut(param: 'string')]
65 | private function methodParamOut(string &$param): array
66 | {
67 | return $this->getParamOuts(__FUNCTION__);
68 | }
69 |
70 | #[ParamOut('string $param')]
71 | private function unnamedMethodParamOut(string &$param): array
72 | {
73 | return $this->getParamOuts(__FUNCTION__);
74 | }
75 |
76 | #[ParamOut(0)]
77 | private function invalidTypeMethodParamOut(string &$param): array
78 | {
79 | return $this->getParamOuts(__FUNCTION__);
80 | }
81 |
82 | #[ParamOut(
83 | param1: 'string',
84 | param2: 'string',
85 | )]
86 | private function severalMethodParamOuts(string &$param1, string &$param2): array
87 | {
88 | return $this->getParamOuts(__FUNCTION__);
89 | }
90 |
91 | #[ParamOut(param1: 'string')]
92 | #[ParamOut(param2: 'string')]
93 | private function multipleMethodParamOuts(string &$param1, string &$param2): array
94 | {
95 | return $this->getParamOuts(__FUNCTION__);
96 | }
97 |
98 | private function paramOutOnParam(
99 | #[ParamOut('string')]
100 | string &$param
101 | ): array {
102 | return $this->getParamOuts(__FUNCTION__);
103 | }
104 |
105 | private function getParamOuts(string $functionName): array
106 | {
107 | $reflection = new ReflectionMethod($this, $functionName);
108 | return self::getParamOutsFromReflection($reflection);
109 | }
110 |
111 | public static function getParamOutsFromReflection(
112 | ReflectionMethod | ReflectionFunction $reflection
113 | ): array {
114 | $instances = AttributeHelper::getFunctionInstances($reflection, ParamOut::class);
115 | $paramOuts = [];
116 |
117 | foreach ($instances['function'] as $instance) {
118 | $paramOuts = array_merge($paramOuts, $instance->params);
119 | }
120 |
121 | foreach ($instances['parameters'] as $name => $attrs) {
122 | foreach ($attrs as $instance) {
123 | $argument = $instance->params[array_key_first($instance->params)];
124 | $paramOuts[$name] = $argument;
125 | }
126 | }
127 |
128 | return $paramOuts;
129 | }
130 | }
131 |
132 | #[ParamOut(param: 'string')]
133 | function functionParamOut(string &$param): array
134 | {
135 | $reflection = new ReflectionFunction(__FUNCTION__);
136 | return ParamOutTest::getParamOutsFromReflection($reflection);
137 | }
138 |
--------------------------------------------------------------------------------
/tests/ParamTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['param' => 'string'], $this->methodParam('Test'));
13 | }
14 |
15 | public function testUnnamedMethodParam(): void
16 | {
17 | $this->assertEquals(['string $param'], $this->unnamedMethodParam('Test'));
18 | }
19 |
20 | public function testInvalidTypeMethodParam(): void
21 | {
22 | $errorThrown = false;
23 | try {
24 | $this->invalidTypeMethodParam('Test');
25 | } catch (TypeError) {
26 | $errorThrown = true;
27 | }
28 | $this->assertTrue($errorThrown);
29 | }
30 |
31 | public function testSeveralMethodParams(): void
32 | {
33 | $this->assertEquals([
34 | 'param1' => 'string',
35 | 'param2' => 'string'
36 | ], $this->severalMethodParams('Test', 'Test'));
37 | }
38 |
39 | public function testMultipleMethodParams(): void
40 | {
41 | $this->assertEquals([
42 | 'param1' => 'string',
43 | 'param2' => 'string'
44 | ], $this->multipleMethodParams('Test', 'Test'));
45 | }
46 |
47 | public function testFunctionParam(): void
48 | {
49 | $this->assertEquals(['param' => 'string'], functionParam('Test'));
50 | }
51 |
52 | public function testVariadicMethodParam(): void
53 | {
54 | $this->assertEquals(['params' => 'string ...'], $this->variadicMethodParam('Test'));
55 | }
56 |
57 | public function testParamOnParam(): void
58 | {
59 | $this->assertEquals(['param' => 'string'], $this->paramOnParam('Test'));
60 | }
61 |
62 | #[Param(param: 'string')]
63 | private function methodParam(string $param): array
64 | {
65 | return $this->getParams(__FUNCTION__);
66 | }
67 |
68 | #[Param('string $param')]
69 | private function unnamedMethodParam(string $param): array
70 | {
71 | return $this->getParams(__FUNCTION__);
72 | }
73 |
74 | #[Param(0)]
75 | private function invalidTypeMethodParam(string $param): array
76 | {
77 | return $this->getParams(__FUNCTION__);
78 | }
79 |
80 | #[Param(
81 | param1: 'string',
82 | param2: 'string',
83 | )]
84 | private function severalMethodParams(string $param1, string $param2): array
85 | {
86 | return $this->getParams(__FUNCTION__);
87 | }
88 |
89 | #[Param(param1: 'string')]
90 | #[Param(param2: 'string')]
91 | private function multipleMethodParams(string $param1, string $param2): array
92 | {
93 | return $this->getParams(__FUNCTION__);
94 | }
95 |
96 | #[Param(params: 'string ...')]
97 | private function variadicMethodParam(string ...$params): array
98 | {
99 | return $this->getParams(__FUNCTION__);
100 | }
101 |
102 | private function paramOnParam(
103 | #[Param('string')]
104 | string $param
105 | ): array {
106 | return $this->getParams(__FUNCTION__);
107 | }
108 |
109 | private function getParams(string $functionName): array
110 | {
111 | $reflection = new ReflectionMethod($this, $functionName);
112 | return self::getParamsFromReflection($reflection);
113 | }
114 |
115 | public static function getParamsFromReflection(
116 | ReflectionMethod | ReflectionFunction $reflection
117 | ): array {
118 | $instances = AttributeHelper::getFunctionInstances($reflection, Param::class);
119 | $params = [];
120 |
121 | foreach ($instances['function'] as $instance) {
122 | $params = array_merge($params, $instance->params);
123 | }
124 |
125 | foreach ($instances['parameters'] as $name => $attrs) {
126 | foreach ($attrs as $instance) {
127 | $argument = $instance->params[array_key_first($instance->params)];
128 | $params[$name] = $argument;
129 | }
130 | }
131 |
132 | return $params;
133 | }
134 | }
135 |
136 | #[Param(param: 'string')]
137 | function functionParam(string $param): array
138 | {
139 | $reflection = new ReflectionFunction(__FUNCTION__);
140 | return ParamTest::getParamsFromReflection($reflection);
141 | }
142 |
--------------------------------------------------------------------------------
/tests/TypeTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('FloatArray float[]', self::getTypeFromReflection($reflection));
34 | }
35 |
36 | public function testClassConstantType(): void
37 | {
38 | $reflection = new ReflectionClassConstant($this, 'NAME');
39 | $this->assertEquals('string', self::getTypeFromReflection($reflection));
40 | }
41 |
42 | public function testPropertyType(): void
43 | {
44 | $this->assertEquals('string', $this->propertyType());
45 | }
46 |
47 | public function testArrayPropertyType(): void
48 | {
49 | $this->assertEquals('string[]', $this->arrayPropertyType());
50 | }
51 |
52 | public function testInvalidTypePropertyType(): void
53 | {
54 | $errorThrown = false;
55 | try {
56 | $this->invalidPropertyType();
57 | } catch (TypeError) {
58 | $errorThrown = true;
59 | }
60 | $this->assertTrue($errorThrown);
61 | }
62 |
63 | public function testPropertyTypeWithTooManyParameters(): void
64 | {
65 | $this->assertEquals('string', $this->propertyTypeWithTooManyParameters());
66 | }
67 |
68 | public function testMultiplePropertyTypes(): void
69 | {
70 | $errorThrown = false;
71 | try {
72 | $this->multiplePropertyTypes();
73 | } catch (Error) {
74 | $errorThrown = true;
75 | }
76 | $this->assertTrue($errorThrown);
77 | }
78 |
79 | public function testMethodType(): void
80 | {
81 | $this->assertEquals('string', $this->methodType());
82 | }
83 |
84 | public function testFunctionType(): void
85 | {
86 | $this->assertEquals('string', functionType());
87 | }
88 |
89 | private function propertyType(): string
90 | {
91 | return $this->getType('property');
92 | }
93 |
94 | private function arrayPropertyType(): string
95 | {
96 | return $this->getType('arrayProperty');
97 | }
98 |
99 | private function invalidPropertyType(): string
100 | {
101 | return $this->getType('invalidTypeProperty');
102 | }
103 |
104 | private function propertyTypeWithTooManyParameters(): string
105 | {
106 | return $this->getType('propertyWithTooManyParameters');
107 | }
108 |
109 | private function multiplePropertyTypes(): string
110 | {
111 | return $this->getType('propertyWithMultipleTypes');
112 | }
113 |
114 | #[Type('string')]
115 | private function methodType(): string
116 | {
117 | return $this->getMethodType(__FUNCTION__);
118 | }
119 |
120 | private function getType(string $propertyName): string
121 | {
122 | $reflection = new ReflectionProperty($this, $propertyName);
123 | return self::getTypeFromReflection($reflection);
124 | }
125 |
126 | private function getMethodType(string $methodName): string
127 | {
128 | $reflection = new ReflectionMethod($this, $methodName);
129 | return self::getTypeFromReflection($reflection);
130 | }
131 |
132 | public static function getTypeFromReflection(
133 | ReflectionProperty | ReflectionClassConstant | ReflectionMethod | ReflectionFunction | ReflectionClass $reflection
134 | ): string {
135 | $instances = AttributeHelper::getInstances($reflection, Type::class);
136 | $type = '';
137 | foreach ($instances as $instance) {
138 | $type = $instance->type;
139 | }
140 |
141 | return $type;
142 | }
143 | }
144 |
145 | #[Type('string')]
146 | function functionType(): string
147 | {
148 | $reflection = new ReflectionFunction(__FUNCTION__);
149 | return TypeTest::getTypeFromReflection($reflection);
150 | }
151 |
--------------------------------------------------------------------------------
/tests/AssertIfTrueTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['param' => 'string'], $this->methodAssert('Test'));
13 | }
14 |
15 | public function testUnnamedMethodAssert(): void
16 | {
17 | $this->assertEquals(['string $param'], $this->unnamedMethodAssert('Test'));
18 | }
19 |
20 | public function testExpressionMethodAssert(): void
21 | {
22 | $this->assertEquals(['string $this->getName()'], $this->expressionMethodAssert('Test'));
23 | }
24 |
25 | public function testInvalidTypeMethodAssert(): void
26 | {
27 | $errorThrown = false;
28 | try {
29 | $this->invalidTypeMethodAssert('Test');
30 | } catch (TypeError) {
31 | $errorThrown = true;
32 | }
33 | $this->assertTrue($errorThrown);
34 | }
35 |
36 | public function testSeveralMethodAsserts(): void
37 | {
38 | $this->assertEquals([
39 | 'param1' => 'string',
40 | 'param2' => 'string'
41 | ], $this->severalMethodAsserts('Test', 'Test'));
42 | }
43 |
44 | public function testMultipleMethodAsserts(): void
45 | {
46 | $this->assertEquals([
47 | 'param1' => 'string',
48 | 'param2' => 'string'
49 | ], $this->multipleMethodAsserts('Test', 'Test'));
50 | }
51 |
52 | public function testFunctionAssert(): void
53 | {
54 | $this->assertEquals(['param' => 'string'], functionAssertIfTrue('Test'));
55 | }
56 |
57 | public function testAssertOnParam(): void
58 | {
59 | $this->assertEquals(['param' => 'string'], $this->assertOnParam('Test'));
60 | }
61 |
62 | #[AssertIfTrue(param: 'string')]
63 | private function methodAssert(string $param): array
64 | {
65 | return $this->getAsserts(__FUNCTION__);
66 | }
67 |
68 | #[AssertIfTrue('string $param')]
69 | private function unnamedMethodAssert(string $param): array
70 | {
71 | return $this->getAsserts(__FUNCTION__);
72 | }
73 |
74 | #[AssertIfTrue('string $this->getName()')]
75 | private function expressionMethodAssert(string $param): array
76 | {
77 | return $this->getAsserts(__FUNCTION__);
78 | }
79 |
80 | #[AssertIfTrue(0)]
81 | private function invalidTypeMethodAssert(string $param): array
82 | {
83 | return $this->getAsserts(__FUNCTION__);
84 | }
85 |
86 | #[AssertIfTrue(
87 | param1: 'string',
88 | param2: 'string',
89 | )]
90 | private function severalMethodAsserts(string $param1, string $param2): array
91 | {
92 | return $this->getAsserts(__FUNCTION__);
93 | }
94 |
95 | #[AssertIfTrue(param1: 'string')]
96 | #[AssertIfTrue(param2: 'string')]
97 | private function multipleMethodAsserts(string $param1, string $param2): array
98 | {
99 | return $this->getAsserts(__FUNCTION__);
100 | }
101 |
102 | private function assertOnParam(
103 | #[AssertIfTrue('string')]
104 | string $param
105 | ): array {
106 | return $this->getAsserts(__FUNCTION__);
107 | }
108 |
109 | private function getAsserts(string $functionName): array
110 | {
111 | $reflection = new ReflectionMethod($this, $functionName);
112 | return self::getAssertsFromReflection($reflection);
113 | }
114 |
115 | public static function getAssertsFromReflection(
116 | ReflectionMethod | ReflectionFunction $reflection
117 | ): array {
118 | $instances = AttributeHelper::getFunctionInstances($reflection, AssertIfTrue::class);
119 | $asserts = [];
120 |
121 | foreach ($instances['function'] as $instance) {
122 | $asserts = array_merge($asserts, $instance->params);
123 | }
124 |
125 | foreach ($instances['parameters'] as $name => $attrs) {
126 | foreach ($attrs as $instance) {
127 | $argument = $instance->params[array_key_first($instance->params)];
128 | $asserts[$name] = $argument;
129 | }
130 | }
131 |
132 | return $asserts;
133 | }
134 | }
135 |
136 | #[AssertIfTrue(param: 'string')]
137 | function functionAssertIfTrue(string $param): array
138 | {
139 | $reflection = new ReflectionFunction(__FUNCTION__);
140 | return AssertIfTrueTest::getAssertsFromReflection($reflection);
141 | }
142 |
--------------------------------------------------------------------------------
/tests/AssertIfFalseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['param' => 'string'], $this->methodAssert('Test'));
13 | }
14 |
15 | public function testUnnamedMethodAssert(): void
16 | {
17 | $this->assertEquals(['string $param'], $this->unnamedMethodAssert('Test'));
18 | }
19 |
20 | public function testExpressionMethodAssert(): void
21 | {
22 | $this->assertEquals(['string $this->getName()'], $this->expressionMethodAssert('Test'));
23 | }
24 |
25 | public function testInvalidTypeMethodAssert(): void
26 | {
27 | $errorThrown = false;
28 | try {
29 | $this->invalidTypeMethodAssert('Test');
30 | } catch (TypeError) {
31 | $errorThrown = true;
32 | }
33 | $this->assertTrue($errorThrown);
34 | }
35 |
36 | public function testSeveralMethodAsserts(): void
37 | {
38 | $this->assertEquals([
39 | 'param1' => 'string',
40 | 'param2' => 'string'
41 | ], $this->severalMethodAsserts('Test', 'Test'));
42 | }
43 |
44 | public function testMultipleMethodAsserts(): void
45 | {
46 | $this->assertEquals([
47 | 'param1' => 'string',
48 | 'param2' => 'string'
49 | ], $this->multipleMethodAsserts('Test', 'Test'));
50 | }
51 |
52 | public function testFunctionAssert(): void
53 | {
54 | $this->assertEquals(['param' => 'string'], functionAssertIfFalse('Test'));
55 | }
56 |
57 | public function testAssertOnParam(): void
58 | {
59 | $this->assertEquals(['param' => 'string'], $this->assertOnParam('Test'));
60 | }
61 |
62 | #[AssertIfFalse(param: 'string')]
63 | private function methodAssert(string $param): array
64 | {
65 | return $this->getAsserts(__FUNCTION__);
66 | }
67 |
68 | #[AssertIfFalse('string $param')]
69 | private function unnamedMethodAssert(string $param): array
70 | {
71 | return $this->getAsserts(__FUNCTION__);
72 | }
73 |
74 | #[AssertIfFalse('string $this->getName()')]
75 | private function expressionMethodAssert(string $param): array
76 | {
77 | return $this->getAsserts(__FUNCTION__);
78 | }
79 |
80 | #[AssertIfFalse(0)]
81 | private function invalidTypeMethodAssert(string $param): array
82 | {
83 | return $this->getAsserts(__FUNCTION__);
84 | }
85 |
86 | #[AssertIfFalse(
87 | param1: 'string',
88 | param2: 'string',
89 | )]
90 | private function severalMethodAsserts(string $param1, string $param2): array
91 | {
92 | return $this->getAsserts(__FUNCTION__);
93 | }
94 |
95 | #[AssertIfFalse(param1: 'string')]
96 | #[AssertIfFalse(param2: 'string')]
97 | private function multipleMethodAsserts(string $param1, string $param2): array
98 | {
99 | return $this->getAsserts(__FUNCTION__);
100 | }
101 |
102 | private function assertOnParam(
103 | #[AssertIfFalse('string')]
104 | string $param
105 | ): array {
106 | return $this->getAsserts(__FUNCTION__);
107 | }
108 |
109 | private function getAsserts(string $functionName): array
110 | {
111 | $reflection = new ReflectionMethod($this, $functionName);
112 | return self::getAssertsFromReflection($reflection);
113 | }
114 |
115 | public static function getAssertsFromReflection(
116 | ReflectionMethod | ReflectionFunction $reflection
117 | ): array {
118 | $instances = AttributeHelper::getFunctionInstances($reflection, AssertIfFalse::class);
119 | $asserts = [];
120 |
121 | foreach ($instances['function'] as $instance) {
122 | $asserts = array_merge($asserts, $instance->params);
123 | }
124 |
125 | foreach ($instances['parameters'] as $name => $attrs) {
126 | foreach ($attrs as $instance) {
127 | $argument = $instance->params[array_key_first($instance->params)];
128 | $asserts[$name] = $argument;
129 | }
130 | }
131 |
132 | return $asserts;
133 | }
134 | }
135 |
136 | #[AssertIfFalse(param: 'string')]
137 | function functionAssertIfFalse(string $param): array
138 | {
139 | $reflection = new ReflectionFunction(__FUNCTION__);
140 | return AssertIfFalseTest::getAssertsFromReflection($reflection);
141 | }
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Static Analysis Attributes
2 |
3 | [](https://github.com/php-static-analysis/attributes/actions)
4 | [](https://packagist.org/packages/php-static-analysis/attributes)
5 | [](https://github.com/php-static-analysis/attributes/blob/main/LICENSE)
6 | [](https://packagist.org/packages/php-static-analysis/attributes/stats)
7 |
8 | Since the release of PHP 8.0 more and more libraries, frameworks and tools have been updated to use attributes instead of annotations in PHPDocs.
9 |
10 | However, static analysis tools like PHPStan or Psalm or IDEs like PhpStorm or VS Code have not made this transition to attributes and they still rely on annotations in PHPDocs for a lot of their functionality.
11 |
12 | This library aims to provide a set of PHP attributes which could replace the most commonly used annotations accepted by these tools and will aim to provide related repositories with the extensions or plugins that would allow these attributes to be used in these tools.
13 |
14 | In particular, these repositories are:
15 |
16 | - [PHPStan extension](https://github.com/php-static-analysis/phpstan-extension)
17 | - [Psalm plugin](https://github.com/php-static-analysis/psalm-plugin)
18 | - [RectorPHP rules to migrate annotations to attributes](https://github.com/php-static-analysis/rector-rule)
19 |
20 | ## Example
21 |
22 | In order to show how code would look with these attributes, we can look at the following example. This is how a class looks like with the current annotations:
23 |
24 | ```php
25 | */
30 | private array $result;
31 |
32 | /**
33 | * @param array $array1 the first array
34 | * @param array $array2 the second array
35 | * @return array
36 | */
37 | public function addArrays(array $array1, array $array2): array
38 | {
39 | $this->result = $array1 + $array2;
40 | return $this->result;
41 | }
42 | }
43 | ```
44 |
45 | And this is how it would look like using the new attributes:
46 |
47 | ```php
48 | ')]
57 | private array $result;
58 |
59 | #[Param(array1: 'array')] // the first array
60 | #[Param(array2: 'array')] // the second array
61 | #[Returns('array')]
62 | public function addArrays(array $array1, array $array2): array
63 | {
64 | $this->array = $array1 + $array2;
65 | return $this->array;
66 | }
67 | }
68 | ```
69 |
70 | ## Installation
71 |
72 | To use these attributes, require this library in Composer:
73 |
74 | ```
75 | composer require php-static-analysis/attributes
76 | ```
77 |
78 | And then install any needed extensions/plugins for the tools that you use.
79 |
80 | ## List of implemented attributes
81 |
82 | These are the available attributes and their corresponding PHPDoc annotations:
83 |
84 | | Attribute | PHPDoc Annotations |
85 | |-------------------------------------------------------|--------------------------------------|
86 | | [Assert](doc/Assert.md) | `@assert` |
87 | | [AssertIfFalse](doc/AssertIfFalse.md) | `@assert-if-false` |
88 | | [AssertIfTrue](doc/AssertIfTrue.md) | `@assert-if-true` |
89 | | [DefineType](doc/DefineType.md) | `@type` |
90 | | [Deprecated](doc/Deprecated.md) | `@deprecated` |
91 | | [Immutable](doc/Immutable.md) | `@immutable` |
92 | | [ImportType](doc/ImportType.md) | `@import-type` |
93 | | [Impure](doc/Impure.md) | `@impure` |
94 | | [Internal](doc/Internal.md) | `@internal` |
95 | | [IsReadOnly](doc/IsReadOnly.md) | `@readonly` |
96 | | [Method](doc/Method.md) | `@method` |
97 | | [Mixin](doc/Mixin.md) | `@mixin` |
98 | | [Param](doc/Param.md) | `@param` |
99 | | [ParamOut](doc/ParamOut.md) | `@param-out` |
100 | | [Property](doc/Property.md) | `@property` `@var` |
101 | | [PropertyRead](doc/PropertyRead.md) | `@property-read` |
102 | | [PropertyWrite](doc/PropertyWrite.md) | `@property-write` |
103 | | [Pure](doc/Pure.md) | `@pure` |
104 | | [RequireExtends](doc/RequireExtends.md) | `@require-extends` |
105 | | [RequireImplements](doc/RequireImplements.md) | `@require-implements` |
106 | | [Returns](doc/Returns.md) | `@return` |
107 | | [SelfOut](doc/SelfOut.md) | `@self-out` `@this-out` |
108 | | [Template](doc/Template.md) | `@template` |
109 | | [TemplateContravariant](doc/TemplateContravariant.md) | `@template-contravariant` |
110 | | [TemplateCovariant](doc/TemplateCovariant.md) | `@template-covariant` |
111 | | [TemplateExtends](doc/TemplateExtends.md) | `@extends` `@template-extends` |
112 | | [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements` |
113 | | [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
114 | | [Throws](doc/Throws.md) | `@throws` |
115 | | [Type](doc/Type.md) | `@var` `@return` `@type` |
116 |
117 | ## PhpStorm Support
118 | A plugin that fully supports these attributes will need to be created. Until this is ready you can get partial support by installing PHPStan, our PHPStan extension and enabling PHPStan support in PhpStorm (as described [here](https://www.jetbrains.com/help/phpstorm/using-phpstan.html)). You will then be able to see errors and messages related to these attributes in your code.
119 |
120 | Alternatively install Psalm, our Psalm extension and enable Psalm support in PhpStorm (as described [here](https://www.jetbrains.com/help/phpstorm/using-psalm.html))
121 |
122 | ## VS Code Support
123 | An extension that fully supports these attributes will need to be created. Until this is ready you can get partial support by installing PHPStan, our PHPStan extension and a VS Code extension that supports PHPStan (for example [this one](https://github.com/SanderRonde/phpstan-vscode)). When you enable the extension you will be able to see errors and messages related to these attributes in your code.
124 |
125 | Alternatively install Psalm, our Psalm extension and a VS Code extension that supports Psam (for example [this one](https://github.com/psalm/psalm-vscode-plugin))
126 |
127 | ## Sponsor this project
128 |
129 | If you would like to support the development of this project, please consider [sponsoring me](https://github.com/sponsors/carlos-granados)
130 |
131 |
--------------------------------------------------------------------------------