Skip to content

Commit

Permalink
Add Throws attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-granados committed Feb 25, 2024
1 parent 9876dee commit 69c03bb
Show file tree
Hide file tree
Showing 18 changed files with 245 additions and 35 deletions.
51 changes: 26 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,31 +94,32 @@ This extension works by interacting with the parser that PHPStan uses to parse t

These are the available attributes and their corresponding PHPDoc annotations:

| Attribute | PHPDoc Annotations |
|--------------------------------------------------------------------------------------------------------------------|--------------------------------------|
| [Deprecated](https://github.com/php-static-analysis/attributes/blob/main/doc/Deprecated.md) | `@deprecated` |
| [Impure](https://github.com/php-static-analysis/attributes/blob/main/doc/Impure.md) | `@impure` |
| [Internal](https://github.com/php-static-analysis/attributes/blob/main/doc/Internal.md) | `@internal` |
| [IsReadOnly](https://github.com/php-static-analysis/attributes/blob/main/doc/IsReadOnly.md) | `@readonly` |
| [Method](https://github.com/php-static-analysis/attributes/blob/main/doc/Method.md) | `@method` |
| [Mixin](https://github.com/php-static-analysis/attributes/blob/main/doc/Mixin.md) | `@mixin` |
| [Param](https://github.com/php-static-analysis/attributes/blob/main/doc/Param.md) | `@param` |
| [ParamOut](https://github.com/php-static-analysis/attributes/blob/main/doc/ParamOut.md) | `@param-out` |
| [Property](https://github.com/php-static-analysis/attributes/blob/main/doc/Property.md) | `@property` `@var` |
| [PropertyRead](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyRead.md) | `@property-read` |
| [PropertyWrite](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyWrite.md) | `@property-write` |
| [Pure](https://github.com/php-static-analysis/attributes/blob/main/doc/Pure.md) | `@pure` |
| [RequireExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireExtends.md) | `@require-extends` |
| [RequireImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireImplements.md) | `@require-implements` |
| [Returns](https://github.com/php-static-analysis/attributes/blob/main/doc/Returns.md) | `@return` |
| [SelfOut](https://github.com/php-static-analysis/attributes/blob/main/doc/SelfOut.md) | `@self-out` `@this-out` |
| [Template](https://github.com/php-static-analysis/attributes/blob/main/doc/Template.md) | `@template` |
| [TemplateContravariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateContravariant.md) | `@template-contravariant` |
| [TemplateCovariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateCovariant.md) | `@template-covariant` |
| [TemplateExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateExtends.md) | `@extends` `@template-extends` |
| [TemplateImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateImplements.md) | `@implements` `@template-implements` |
| [TemplateUse](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateUse.md) | `@use` `@template-use` |
| [Type](https://github.com/php-static-analysis/attributes/blob/main/doc/Type.md) | `@var` `@return` |
| Attribute | PHPDoc Annotations |
|-------------------------------------------------------------------------------------------------------------------|--------------------------------------|
| [Deprecated](https://github.com/php-static-analysis/attributes/blob/main/doc/Deprecated.md) | `@deprecated` |
| [Impure](https://github.com/php-static-analysis/attributes/blob/main/doc/Impure.md) | `@impure` |
| [Internal](https://github.com/php-static-analysis/attributes/blob/main/doc/Internal.md) | `@internal` |
| [IsReadOnly](https://github.com/php-static-analysis/attributes/blob/main/doc/IsReadOnly.md) | `@readonly` |
| [Method](https://github.com/php-static-analysis/attributes/blob/main/doc/Method.md) | `@method` |
| [Mixin](https://github.com/php-static-analysis/attributes/blob/main/doc/Mixin.md) | `@mixin` |
| [Param](https://github.com/php-static-analysis/attributes/blob/main/doc/Param.md) | `@param` |
| [ParamOut](https://github.com/php-static-analysis/attributes/blob/main/doc/ParamOut.md) | `@param-out` |
| [Property](https://github.com/php-static-analysis/attributes/blob/main/doc/Property.md) | `@property` `@var` |
| [PropertyRead](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyRead.md) | `@property-read` |
| [PropertyWrite](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyWrite.md) | `@property-write` |
| [Pure](https://github.com/php-static-analysis/attributes/blob/main/doc/Pure.md) | `@pure` |
| [RequireExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireExtends.md) | `@require-extends` |
| [RequireImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireImplements.md) | `@require-implements` |
| [Returns](https://github.com/php-static-analysis/attributes/blob/main/doc/Returns.md) | `@return` |
| [SelfOut](https://github.com/php-static-analysis/attributes/blob/main/doc/SelfOut.md) | `@self-out` `@this-out` |
| [Template](https://github.com/php-static-analysis/attributes/blob/main/doc/Template.md) | `@template` |
| [TemplateContravariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateContravariant.md) | `@template-contravariant` |
| [TemplateCovariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateCovariant.md) | `@template-covariant` |
| [TemplateExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateExtends.md) | `@extends` `@template-extends` |
| [TemplateImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateImplements.md) | `@implements` `@template-implements` |
| [TemplateUse](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateUse.md) | `@use` `@template-use` |
| [Throws](https://github.com/php-static-analysis/attributes/blob/main/doc/Throws.md) | `@throws` |
| [Type](https://github.com/php-static-analysis/attributes/blob/main/doc/Type.md) | `@var` `@return` |



Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.0",
"php-static-analysis/attributes": "^0.1.15 || dev-main",
"php-static-analysis/node-visitor": "^0.1.15 || dev-main",
"php-static-analysis/attributes": "^0.1.16 || dev-main",
"php-static-analysis/node-visitor": "^0.1.16 || dev-main",
"phpstan/phpstan": "^1.8"
},
"require-dev": {
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/AttributeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function parseString(string $sourceCode): array
private function traverseAst(array $ast): array
{
$traverser = new NodeTraverser();
$nodeVisitor = new AttributeNodeVisitor('phpstan');
$nodeVisitor = new AttributeNodeVisitor(AttributeNodeVisitor::TOOL_PHPSTAN);
$traverser->addVisitor($nodeVisitor);

$ast = $traverser->traverse($ast);
Expand Down
47 changes: 47 additions & 0 deletions tests/ThrowsAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace test\PhpStaticAnalysis\PHPStanExtension;

class ThrowsAttributeTest extends BaseAttributeTestCase
{
public function testMethodThrowsAttribute(): void
{
$errors = $this->analyse(__DIR__ . '/data/Throws/MethodThrowsAttribute.php');
$expectedErrors = [
'Method test\PhpStaticAnalysis\PHPStanExtension\data\Throws\MethodThrowsAttribute::countNoErrorName() has Exception in PHPDoc @throws tag but it\'s not thrown.' => 72,
];

$this->checkExpectedErrors($errors, $expectedErrors);
}

public function testFunctionThrowsAttribute(): void
{
$errors = $this->analyse(__DIR__ . '/data/Throws/FunctionThrowsAttribute.php');
$this->assertCount(0, $errors);
}

public function testInvalidMethodThrowsAttribute(): void
{
$errors = $this->analyse(__DIR__ . '/data/Throws/InvalidMethodThrowsAttribute.php');

$expectedErrors = [
'PHPDoc tag @throws has invalid value (): Unexpected token "\n ", expected type at offset 14' => 10,
'Parameter #1 ...$exceptions of attribute class PhpStaticAnalysis\Attributes\Throws constructor expects string, int given.' => 10,
'Method test\PhpStaticAnalysis\PHPStanExtension\data\Throws\InvalidMethodThrowsAttribute::getOtherNameLength() has string in PHPDoc @throws tag but it\'s not thrown.' => 16,
'PHPDoc tag @throws with type string is not subtype of Throwable' => 16,
'Attribute class PhpStaticAnalysis\Attributes\Throws does not have the property target.' => 22,
];

$this->checkExpectedErrors($errors, $expectedErrors);
}

public static function getAdditionalConfigFiles(): array
{
return array_merge(
parent::getAdditionalConfigFiles(),
[
__DIR__ . '/conf/throws.neon',
]
);
}
}
4 changes: 4 additions & 0 deletions tests/conf/throws.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
exceptions:
check:
tooWideThrowType: true
6 changes: 3 additions & 3 deletions tests/data/Mixin/ClassMixinAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ class Another
{
}

#[Mixin('ClassMixinAttribute')] // this is the proxied class
#[Mixin(ClassMixinAttribute::class)] // this is the proxied class
#[Mixin(
'MyClass',
'Another',
MyClass::class,
Another::class,
)]
class ClassMixinAttributeProxy
{
Expand Down
7 changes: 7 additions & 0 deletions tests/data/Param/MethodParamAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace test\PhpStaticAnalysis\PHPStanExtension\data\Param;

use Exception;
use PhpStaticAnalysis\Attributes\Param;

class MethodParamAttribute
Expand All @@ -12,6 +13,12 @@ public function countNames(array $names): int
return count($names);
}

#[Param(exception: Exception::class)]
public function throwException($exception): void
{
throw $exception;
}

#[Param('string[] $names')]
public function countUnnamedNames(array $names): int
{
Expand Down
7 changes: 7 additions & 0 deletions tests/data/ParamOut/MethodParamOutAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace test\PhpStaticAnalysis\PHPStanExtension\data\ParamOut;

use Exception;
use PhpStaticAnalysis\Attributes\ParamOut;

class MethodParamOutAttribute
Expand All @@ -12,6 +13,12 @@ public function setNames(mixed &$names): void
$names = 1;
}

#[ParamOut(exception: Exception::class)]
public function setException(mixed &$exception): void
{
$exception = new Exception();
}

#[ParamOut('int $names')]
public function setUnnamedNames(mixed &$names): void
{
Expand Down
2 changes: 2 additions & 0 deletions tests/data/Property/ClassPropertyAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace test\PhpStaticAnalysis\PHPStanExtension\data\Property;

use Exception;
use PhpStaticAnalysis\Attributes\Property;

#[Property(name: 'string')] // the name of the user
#[Property(exception: Exception::class)]
#[Property('int $age')]
#[Property(
index1: 'string[]',
Expand Down
2 changes: 2 additions & 0 deletions tests/data/PropertyRead/ClassPropertyReadAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace test\PhpStaticAnalysis\PHPStanExtension\data\PropertyRead;

use Exception;
use PhpStaticAnalysis\Attributes\PropertyRead;

#[PropertyRead(name: 'string')] // cannot be written to
#[PropertyRead(exception: Exception::class)]
#[PropertyRead('int $age')]
#[PropertyRead(
index1: 'string[]',
Expand Down
2 changes: 2 additions & 0 deletions tests/data/PropertyWrite/ClassPropertyWriteAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace test\PhpStaticAnalysis\PHPStanExtension\data\PropertyWrite;

use Exception;
use PhpStaticAnalysis\Attributes\PropertyWrite;

#[PropertyWrite(name: 'string')] // cannot be read
#[PropertyWrite(exception: Exception::class)]
#[PropertyWrite('int $age')]
#[PropertyWrite(
index1: 'string[]',
Expand Down
2 changes: 1 addition & 1 deletion tests/data/RequireExtends/TraitRequireExtendsAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use PhpStaticAnalysis\Attributes\RequireExtends;

#[RequireExtends('ClassRequireExtendsAttribute')] // the class using this trait needs to extend this class
#[RequireExtends(ClassRequireExtendsAttribute::class)] // the class using this trait needs to extend this class
trait TraitRequireExtendsAttribute
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

use PhpStaticAnalysis\Attributes\RequireImplements;

#[RequireImplements('InterfaceRequireImplementsAttribute')] // the class that uses this trait needs to implement these interfaces
#[RequireImplements(InterfaceRequireImplementsAttribute::class)] // the class that uses this trait needs to implement these interfaces
#[RequireImplements(
'InterfaceRequireImplementsAttribute2',
'InterfaceRequireImplementsAttribute3'
InterfaceRequireImplementsAttribute2::class,
InterfaceRequireImplementsAttribute3::class
)]
trait TraitRequireImplementsAttribute
{
Expand Down
7 changes: 7 additions & 0 deletions tests/data/Returns/MethodReturnsAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace test\PhpStaticAnalysis\PHPStanExtension\data\Returns;

use Exception;
use PhpStaticAnalysis\Attributes\Returns;

class MethodReturnsAttribute
Expand All @@ -12,6 +13,12 @@ public function getNames(): array
return ['hello', 'world'];
}

#[Returns(Exception::class)]
public function getException()
{
return new Exception();
}

/**
* @deprecated
*/
Expand Down
15 changes: 15 additions & 0 deletions tests/data/Throws/FunctionThrowsAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace test\PhpStaticAnalysis\PHPStanExtension\data\Throws;

use Exception;
use PhpStaticAnalysis\Attributes\Throws;

#[Throws(Exception::class)]
function countName(string $name): int
{
if ($name == '') {
throw new Exception('Empty string!');
}
return strlen($name);
}
24 changes: 24 additions & 0 deletions tests/data/Throws/InvalidMethodThrowsAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace test\PhpStaticAnalysis\PHPStanExtension\data\Throws;

use Exception;
use PhpStaticAnalysis\Attributes\Throws;

class InvalidMethodThrowsAttribute
{
#[Throws(0)]
public function getNameLength(string $name): int
{
return strlen($name);
}

#[Throws('string')]
public function getOtherNameLength(string $name): int
{
return strlen($name);
}

#[Throws(Exception::class)]
public string $property;
}
88 changes: 88 additions & 0 deletions tests/data/Throws/MethodThrowsAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace test\PhpStaticAnalysis\PHPStanExtension\data\Throws;

use Error;
use Exception;
use PhpStaticAnalysis\Attributes\Throws;

class MethodThrowsAttribute
{
#[Throws(Exception::class)] // returns the number of names
public function countName(string $name): int
{
if ($name == '') {
throw new Exception('Empty string!');
}
return strlen($name);
}

/**
* @deprecated
*/
#[Throws(Exception::class)]
public function countMoreName(string $name): int
{
if ($name == '') {
throw new Exception('Empty string!');
}
return strlen($name);
}

/**
* @throws Exception
*/
#[Throws(Exception::class)]
public function countEvenMoreName(string $name): int
{
if ($name == '') {
throw new Exception('Empty string!');
}
return strlen($name);
}

#[Throws(
Exception::class,
Error::class
)]
public function countTwoNames(string $name1, string $name2): int
{
if ($name1 == '') {
throw new Exception('Empty string!');
}
if ($name2 == '') {
throw new Error('Empty string!');
}
return strlen($name1 . $name2);
}

#[Throws(Exception::class)]
#[Throws(Error::class)]
public function countOtherTwoNames(string $name1, string $name2): int
{
if ($name1 == '') {
throw new Exception('Empty string!');
}
if ($name2 == '') {
throw new Error('Empty string!');
}
return strlen($name1 . $name2);
}

#[Throws(Exception::class)]
public function countNoErrorName(string $name): int
{
return strlen($name);
}

/**
* @throws Exception
*/
public function countNameExtra(string $name): int
{
if ($name == '') {
throw new Exception('Empty string!');
}
return strlen($name);
}
}
Loading

0 comments on commit 69c03bb

Please sign in to comment.