├── .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 | [![Continuous Integration](https://github.com/php-static-analysis/attributes/workflows/All%20Tests/badge.svg)](https://github.com/php-static-analysis/attributes/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/php-static-analysis/attributes/v/stable)](https://packagist.org/packages/php-static-analysis/attributes) 5 | [![License](https://poser.pugx.org/php-static-analysis/attributes/license)](https://github.com/php-static-analysis/attributes/blob/main/LICENSE) 6 | [![Total Downloads](https://poser.pugx.org/php-static-analysis/attributes/downloads)](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 | --------------------------------------------------------------------------------