Skip to content

Commit

Permalink
Add RequireExtends and RequireImplements attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-granados committed Feb 24, 2024
1 parent c0a2543 commit 4ad035e
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 21 deletions.
44 changes: 23 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,27 @@ And then install any needed extensions/plugins for the tools that you use.

These are the available attributes and their corresponding PHPDoc annotations:

| Attribute | PHPDoc Annotations |
|-------------------------------------------------------|--------------------------------------|
| [Deprecated](doc/Deprecated.md) | `@deprecated` |
| [Internal](doc/Internal.md) | `@internal` |
| [IsReadOnly](doc/IsReadOnly.md) | `@readonly` |
| [Method](doc/Method.md) | `@method` |
| [Mixin](doc/Mixin.md) | `@mixin` |
| [Param](doc/Param.md) | `@param` |
| [ParamOut](doc/ParamOut.md) | `@param-out` |
| [Property](doc/Property.md) | `@property` `@var` |
| [PropertyRead](doc/PropertyRead.md) | `@property-read` |
| [PropertyWrite](doc/PropertyWrite.md) | `@property-write` |
| [Returns](doc/Returns.md) | `@return` |
| [SelfOut](doc/SelfOut.md) | `@self-out` `@this-out` |
| [Template](doc/Template.md) | `@template` |
| [TemplateContravariant](doc/TemplateContravariant.md) | `@template-contravariant` |
| [TemplateCovariant](doc/TemplateCovariant.md) | `@template-covariant` |
| [TemplateExtends](doc/TemplateExtends.md) | `@extends` `@template-extends` |
| [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements` |
| [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
| [Type](doc/Type.md) | `@var` `@return` |
| Attribute | PHPDoc Annotations |
|-------------------------------------------------------|-------------------------------------|
| [Deprecated](doc/Deprecated.md) | `@deprecated` |
| [Internal](doc/Internal.md) | `@internal` |
| [IsReadOnly](doc/IsReadOnly.md) | `@readonly` |
| [Method](doc/Method.md) | `@method` |
| [Mixin](doc/Mixin.md) | `@mixin` |
| [Param](doc/Param.md) | `@param` |
| [ParamOut](doc/ParamOut.md) | `@param-out` |
| [Property](doc/Property.md) | `@property` `@var` |
| [PropertyRead](doc/PropertyRead.md) | `@property-read` |
| [PropertyWrite](doc/PropertyWrite.md) | `@property-write` |
| [RequireExtends](doc/RequireExtends.md) | `@require-extends` |
| [RequireImplements](doc/RequireImplements.md) | `@require-implements` |
| [Returns](doc/Returns.md) | `@return` |
| [SelfOut](doc/SelfOut.md) | `@self-out` `@this-out` |
| [Template](doc/Template.md) | `@template` |
| [TemplateContravariant](doc/TemplateContravariant.md) | `@template-contravariant` |
| [TemplateCovariant](doc/TemplateCovariant.md) | `@template-covariant` |
| [TemplateExtends](doc/TemplateExtends.md) | `@extends` `@template-extends` |
| [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements`|
| [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
| [Type](doc/Type.md) | `@var` `@return` |

28 changes: 28 additions & 0 deletions doc/RequireExtends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# `RequireExtends` Attribute

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.

## Arguments

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.

We aim to accept all the classes accepted by static analysis tools for the `@require-extends` annotation.

## Example usage

```php
<?php

use PhpStaticAnalysis\Attributes\RequireExtends;

abstract class Parent {
}

#[RequireExtends('ParentClass')]
trait myTrait {
}

class Child extends Parent {
use myTrait;
}
```
34 changes: 34 additions & 0 deletions doc/RequireImplements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# `RequireImplements` Attribute

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.

## Arguments

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.

We aim to accept all the interface names accepted by static analysis tools for the `@require-implements` annotation.

The arguments need to be unnamed arguments.

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).

## Example usage

```php
<?php

use PhpStaticAnalysis\Attributes\RequireImplements;

interface RequireInterface
{
}

#[RequireImplements('RequireInterface')]
trait MyTrait
{
}

class MyClass implements RequireInterface {
use MyTrait;
}
```
18 changes: 18 additions & 0 deletions src/RequireExtends.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace PhpStaticAnalysis\Attributes;

use Attribute;

#[Attribute(
Attribute::TARGET_CLASS
)]
final class RequireExtends
{
public function __construct(
string $class
) {
}
}
19 changes: 19 additions & 0 deletions src/RequireImplements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace PhpStaticAnalysis\Attributes;

use Attribute;

#[Attribute(
Attribute::TARGET_CLASS |
Attribute::IS_REPEATABLE
)]
final class RequireImplements
{
public function __construct(
string ...$interfaces
) {
}
}
44 changes: 44 additions & 0 deletions tests/RequireExtendsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

use PhpStaticAnalysis\Attributes\RequireExtends;
use PHPUnit\Framework\TestCase;

class RequireExtendsTest extends TestCase
{
public function testClassRequireExtends(): void
{
$reflection = new ReflectionClass(RequireMyTrait::class);
$this->assertEquals('RequireParentClass', self::getRequireExtendssFromReflection($reflection));
}

public static function getRequireExtendssFromReflection(
ReflectionClass $reflection
): string {
$attributes = $reflection->getAttributes();
$extends = '';
foreach ($attributes as $attribute) {
if ($attribute->getName() === RequireExtends::class) {
$attribute->newInstance();
$extends = $attribute->getArguments()[0];
}
}

return $extends;
}
}

class RequireParentClass
{
}

#[RequireExtends('RequireParentClass')]
trait RequireMyTrait
{
}

class RequireChildClass extends RequireParentClass
{
use RequireMyTrait;
}
57 changes: 57 additions & 0 deletions tests/RequireImplementsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

use PhpStaticAnalysis\Attributes\RequireImplements;
use PHPUnit\Framework\TestCase;

class RequireImplementsTest extends TestCase implements RequireTestInterface, RequireTestInterface2, RequireTestInterface3
{
use RequireInterfaceTrait;

public function testClassRequireImplements(): void
{
$reflection = new ReflectionClass(RequireInterfaceTrait::class);
$this->assertEquals([
'RequireTestInterface',
'RequireTestInterface2',
'RequireTestInterface3',
], self::getRequireImplementssFromReflection($reflection));
}

public static function getRequireImplementssFromReflection(
ReflectionClass $reflection
): array {
$attributes = $reflection->getAttributes();
$implements = [];
foreach ($attributes as $attribute) {
if ($attribute->getName() === RequireImplements::class) {
$attribute->newInstance();
$implements = array_merge($implements, $attribute->getArguments());
}
}

return $implements;
}
}

#[RequireImplements('RequireTestInterface')]
#[RequireImplements(
'RequireTestInterface2',
'RequireTestInterface3'
)]
trait RequireInterfaceTrait
{
}

interface RequireTestInterface
{
}

interface RequireTestInterface2
{
}

interface RequireTestInterface3
{
}

0 comments on commit 4ad035e

Please sign in to comment.