Skip to content

Commit 18a0567

Browse files
Add ParamOut attribute
1 parent 2b04bef commit 18a0567

File tree

5 files changed

+232
-1
lines changed

5 files changed

+232
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ These are the available attributes and their corresponding PHPDoc annotations:
100100
| [Method](doc/Method.md) | `@method` |
101101
| [Mixin](doc/Mixin.md) | `@mixin` |
102102
| [Param](doc/Param.md) | `@param` |
103+
| [ParamOut](doc/ParamOut.md) | `@param-out` |
103104
| [Property](doc/Property.md) | `@property` `@var` |
104105
| [PropertyRead](doc/PropertyRead.md) | `@property-read` |
105106
| [PropertyWrite](doc/PropertyWrite.md) | `@property-write` |

doc/ParamOut.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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<string>` or `Collection<int>`. 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+
<?php
23+
24+
use PhpStaticAnalysis\Attributes\ParamOut;
25+
26+
class ParamExample
27+
{
28+
// Single parameter
29+
#[ParamOut(param: 'string')]
30+
public function methodParamWithName(mixed &$param)
31+
{
32+
}
33+
34+
// Single parameter with unnamed argument
35+
#[ParamOut('string $param')]
36+
public function methodParamWithoutName(mixed &$param)
37+
{
38+
}
39+
40+
// Multiple params listed in a single attribute
41+
#[ParamOut(
42+
param1: 'string',
43+
param2: 'string',
44+
)]
45+
public function severalMethodParamsWithName(mixed &$param1, mixed &$param2)
46+
{
47+
}
48+
49+
// Multiple params listed in multiple attributes
50+
#[ParamOut(param1: 'string')]
51+
#[ParamOut(param2: 'string')]
52+
public function multipleMethodParamsWithName(mixed &$param1, mixed &$param2)
53+
{
54+
}
55+
56+
// Attribute applied at parameter level
57+
public function paramOnParam(
58+
#[ParamOut('string')]
59+
mixed &$param
60+
) {
61+
}
62+
}
63+
```

phpstan.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ parameters:
1111
- '#^Constructor of class PhpStaticAnalysis\\Attributes\\[a-zA-Z]+ has an unused parameter \$[a-zA-Z]+\.$#'
1212
- '#^(Function|Method) [a-zA-Z\:]+\(\) return type has no value type specified in iterable type array.$#'
1313
- '#^Parameter \#1 \.*\$[a-zA-Z]+ of attribute class PhpStaticAnalysis\\Attributes\\[a-zA-Z]+ constructor expects string, int given.$#'
14-
- '#^PHPDoc tag @[a-zA-Z]+ has invalid value \(\): Unexpected token "\\n ", expected type at offset [0-9]+$#'
14+
- '#^PHPDoc tag @[a-z\-A-Z]+ has invalid value \(\): Unexpected token "\\n ", expected type at offset [0-9]+$#'
1515
- '#^Attribute class PhpStaticAnalysis\\Attributes\\[a-zA-Z]+ constructor invoked with [0-9]+ parameter(s)?, [0-9]+ required.$#'
1616
- '#^Attribute class PhpStaticAnalysis\\Attributes\\[a-zA-Z]+ is not repeatable but is already present above the (property|method).$#'

src/ParamOut.php

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

tests/ParamOutTest.php

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpStaticAnalysis\Attributes\ParamOut;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class ParamOutTest extends TestCase
9+
{
10+
public function testMethodParamOut(): void
11+
{
12+
$text = 'Test';
13+
$this->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+
$attributes = $reflection->getAttributes();
115+
$paramOuts = [];
116+
foreach ($attributes as $attribute) {
117+
if ($attribute->getName() === ParamOut::class) {
118+
$attribute->newInstance();
119+
$paramOuts = array_merge($paramOuts, $attribute->getArguments());
120+
}
121+
}
122+
123+
$parameters = $reflection->getParameters();
124+
foreach ($parameters as $parameter) {
125+
$attributes = $parameter->getAttributes();
126+
foreach ($attributes as $attribute) {
127+
if ($attribute->getName() === ParamOut::class) {
128+
$attribute->newInstance();
129+
$arguments = $attribute->getArguments();
130+
$argument = $arguments[array_key_first($arguments)];
131+
$paramOuts[$parameter->name] = $argument;
132+
;
133+
}
134+
}
135+
}
136+
137+
return $paramOuts;
138+
}
139+
}
140+
141+
#[ParamOut(param: 'string')]
142+
function functionParamOut(string &$param): array
143+
{
144+
$reflection = new ReflectionFunction(__FUNCTION__);
145+
return ParamOutTest::getParamOutsFromReflection($reflection);
146+
}

0 commit comments

Comments
 (0)