From e9da29716a4b407808fad909e6d35fd78cf64016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ols=CC=8Cavsky=CC=81?= Date: Wed, 15 Jun 2022 18:37:12 +0200 Subject: [PATCH] Add support for attributes --- .../ExportedAttributeArgumentNode.php | 71 +++++++++++++++ .../ExportedNode/ExportedAttributeNode.php | 87 +++++++++++++++++++ .../ExportedClassConstantNode.php | 31 ++++++- .../ExportedNode/ExportedClassNode.php | 20 +++++ .../ExportedNode/ExportedEnumNode.php | 31 ++++++- .../ExportedNode/ExportedFunctionNode.php | 21 +++++ .../ExportedNode/ExportedMethodNode.php | 21 +++++ .../ExportedNode/ExportedParameterNode.php | 26 ++++++ .../ExportedNode/ExportedPropertiesNode.php | 23 +++++ src/Dependency/ExportedNodeResolver.php | 37 ++++++++ 10 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 src/Dependency/ExportedNode/ExportedAttributeArgumentNode.php create mode 100644 src/Dependency/ExportedNode/ExportedAttributeNode.php diff --git a/src/Dependency/ExportedNode/ExportedAttributeArgumentNode.php b/src/Dependency/ExportedNode/ExportedAttributeArgumentNode.php new file mode 100644 index 0000000000..f3ee48130d --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeArgumentNode.php @@ -0,0 +1,71 @@ +name === $node->name + && $this->value === $node->value + && $this->byRef === $node->byRef; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): ExportedNode + { + return new self( + $properties['name'], + $properties['value'], + $properties['byRef'], + ); + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'value' => $this->value, + 'byRef' => $this->byRef, + ], + ]; + } + + /** + * @param mixed[] $data + * @return self + */ + public static function decode(array $data): ExportedNode + { + return new self( + $data['name'], + $data['value'], + $data['byRef'], + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedAttributeNode.php b/src/Dependency/ExportedNode/ExportedAttributeNode.php new file mode 100644 index 0000000000..5f748f8492 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeNode.php @@ -0,0 +1,87 @@ +args) !== count($node->args)) { + return false; + } + + foreach ($this->args as $i => $ourAttribute) { + $theirAttribute = $node->args[$i]; + if (!$ourAttribute->equals($theirAttribute)) { + return false; + } + } + + return $this->name === $node->name; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): ExportedNode + { + return new self( + $properties['name'], + $properties['args'], + ); + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'args' => $this->args, + ], + ]; + } + + /** + * @param mixed[] $data + * @return self + */ + public static function decode(array $data): ExportedNode + { + return new self( + $data['name'], + array_map(static function (array $parameterData): ExportedAttributeArgumentNode { + if ($parameterData['type'] !== ExportedAttributeArgumentNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeArgumentNode::decode($parameterData['data']); + }, $data['args']), + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index c24510b3cc..6a59ca1b02 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -4,12 +4,22 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; +use function array_map; +use function count; class ExportedClassConstantNode implements ExportedNode, JsonSerializable { - public function __construct(private string $name, private string $value) + /** + * @param ExportedAttributeNode[] $attributes + */ + public function __construct( + private string $name, + private string $value, + private array $attributes, + ) { } @@ -19,6 +29,17 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $ourAttribute) { + $theirAttribute = $node->attributes[$i]; + if (!$ourAttribute->equals($theirAttribute)) { + return false; + } + } + return $this->name === $node->name && $this->value === $node->value; } @@ -32,6 +53,7 @@ public static function __set_state(array $properties): ExportedNode return new self( $properties['name'], $properties['value'], + $properties['attributes'], ); } @@ -44,6 +66,12 @@ public static function decode(array $data): ExportedNode return new self( $data['name'], $data['value'], + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } @@ -58,6 +86,7 @@ public function jsonSerialize() 'data' => [ 'name' => $this->name, 'value' => $this->value, + 'attributes' => $this->attributes, ], ]; } diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php index c46001f515..ac55e42a39 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -17,6 +17,7 @@ class ExportedClassNode implements ExportedNode, JsonSerializable * @param string[] $usedTraits * @param ExportedTraitUseAdaptation[] $traitUseAdaptations * @param ExportedNode[] $statements + * @param ExportedAttributeNode[] $attributes */ public function __construct( private string $name, @@ -28,6 +29,7 @@ public function __construct( private array $usedTraits, private array $traitUseAdaptations, private array $statements, + private array $attributes, ) { } @@ -50,6 +52,16 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + if (count($this->traitUseAdaptations) !== count($node->traitUseAdaptations)) { return false; } @@ -97,6 +109,7 @@ public static function __set_state(array $properties): ExportedNode $properties['usedTraits'], $properties['traitUseAdaptations'], $properties['statements'], + $properties['attributes'], ); } @@ -118,6 +131,7 @@ public function jsonSerialize() 'usedTraits' => $this->usedTraits, 'traitUseAdaptations' => $this->traitUseAdaptations, 'statements' => $this->statements, + 'attributes' => $this->attributes, ], ]; } @@ -147,6 +161,12 @@ public static function decode(array $data): ExportedNode return $nodeType::decode($node['data']); }, $data['statements']), + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php index 831a823658..ad4670721e 100644 --- a/src/Dependency/ExportedNode/ExportedEnumNode.php +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -4,6 +4,7 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; use function array_map; use function count; @@ -14,8 +15,16 @@ class ExportedEnumNode implements ExportedNode, JsonSerializable /** * @param string[] $implements * @param ExportedNode[] $statements + * @param ExportedAttributeNode[] $attributes */ - public function __construct(private string $name, private ?string $scalarType, private ?ExportedPhpDocNode $phpDoc, private array $implements, private array $statements) + public function __construct( + private string $name, + private ?string $scalarType, + private ?ExportedPhpDocNode $phpDoc, + private array $implements, + private array $statements, + private array $attributes, + ) { } @@ -49,6 +58,18 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if ($attribute->equals($node->attributes[$i])) { + continue; + } + + return false; + } + return $this->name === $node->name && $this->scalarType === $node->scalarType && $this->implements === $node->implements; @@ -66,6 +87,7 @@ public static function __set_state(array $properties): ExportedNode $properties['phpDoc'], $properties['implements'], $properties['statements'], + $properties['attributes'], ); } @@ -83,6 +105,7 @@ public function jsonSerialize() 'phpDoc' => $this->phpDoc, 'implements' => $this->implements, 'statements' => $this->statements, + 'attributes' => $this->attributes, ], ]; } @@ -103,6 +126,12 @@ public static function decode(array $data): ExportedNode return $nodeType::decode($node['data']); }, $data['statements']), + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedFunctionNode.php b/src/Dependency/ExportedNode/ExportedFunctionNode.php index 58f2704c79..9ec002b367 100644 --- a/src/Dependency/ExportedNode/ExportedFunctionNode.php +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -14,6 +14,7 @@ class ExportedFunctionNode implements ExportedNode, JsonSerializable /** * @param ExportedParameterNode[] $parameters + * @param ExportedAttributeNode[] $attributes */ public function __construct( private string $name, @@ -21,6 +22,7 @@ public function __construct( private bool $byRef, private ?string $returnType, private array $parameters, + private array $attributes, ) { } @@ -54,6 +56,17 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $ourAttribute) { + $theirAttribute = $node->attributes[$i]; + if (!$ourAttribute->equals($theirAttribute)) { + return false; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->returnType === $node->returnType; @@ -71,6 +84,7 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['returnType'], $properties['parameters'], + $properties['attributes'], ); } @@ -88,6 +102,7 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } @@ -109,6 +124,12 @@ public static function decode(array $data): ExportedNode } return ExportedParameterNode::decode($parameterData['data']); }, $data['parameters']), + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedMethodNode.php b/src/Dependency/ExportedNode/ExportedMethodNode.php index 7cb3376fab..27e9406881 100644 --- a/src/Dependency/ExportedNode/ExportedMethodNode.php +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -14,6 +14,7 @@ class ExportedMethodNode implements ExportedNode, JsonSerializable /** * @param ExportedParameterNode[] $parameters + * @param ExportedAttributeNode[] $attributes */ public function __construct( private string $name, @@ -26,6 +27,7 @@ public function __construct( private bool $static, private ?string $returnType, private array $parameters, + private array $attributes, ) { } @@ -59,6 +61,17 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $ourAttribute) { + $theirAttribute = $node->attributes[$i]; + if (!$ourAttribute->equals($theirAttribute)) { + return false; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->public === $node->public @@ -86,6 +99,7 @@ public static function __set_state(array $properties): ExportedNode $properties['static'], $properties['returnType'], $properties['parameters'], + $properties['attributes'], ); } @@ -108,6 +122,7 @@ public function jsonSerialize() 'static' => $this->static, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } @@ -134,6 +149,12 @@ public static function decode(array $data): ExportedNode } return ExportedParameterNode::decode($parameterData['data']); }, $data['parameters']), + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedParameterNode.php b/src/Dependency/ExportedNode/ExportedParameterNode.php index ad36f20c42..ced2ac4c1e 100644 --- a/src/Dependency/ExportedNode/ExportedParameterNode.php +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -4,17 +4,24 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; +use function array_map; +use function count; class ExportedParameterNode implements ExportedNode, JsonSerializable { + /** + * @param ExportedAttributeNode[] $attributes + */ public function __construct( private string $name, private ?string $type, private bool $byRef, private bool $variadic, private bool $hasDefault, + private array $attributes, ) { } @@ -25,6 +32,17 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $ourAttribute) { + $theirAttribute = $node->attributes[$i]; + if (!$ourAttribute->equals($theirAttribute)) { + return false; + } + } + return $this->name === $node->name && $this->type === $node->type && $this->byRef === $node->byRef @@ -44,6 +62,7 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['variadic'], $properties['hasDefault'], + $properties['attributes'], ); } @@ -61,6 +80,7 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'variadic' => $this->variadic, 'hasDefault' => $this->hasDefault, + 'attributes' => $this->attributes, ], ]; } @@ -77,6 +97,12 @@ public static function decode(array $data): ExportedNode $data['byRef'], $data['variadic'], $data['hasDefault'], + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedPropertiesNode.php b/src/Dependency/ExportedNode/ExportedPropertiesNode.php index 2b61556824..363b50953e 100644 --- a/src/Dependency/ExportedNode/ExportedPropertiesNode.php +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -4,7 +4,9 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; +use function array_map; use function count; class ExportedPropertiesNode implements JsonSerializable, ExportedNode @@ -12,6 +14,7 @@ class ExportedPropertiesNode implements JsonSerializable, ExportedNode /** * @param string[] $names + * @param ExportedAttributeNode[] $attributes */ public function __construct( private array $names, @@ -21,6 +24,7 @@ public function __construct( private bool $private, private bool $static, private bool $readonly, + private array $attributes, ) { } @@ -53,6 +57,17 @@ public function equals(ExportedNode $node): bool } } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $ourAttribute) { + $theirAttribute = $node->attributes[$i]; + if (!$ourAttribute->equals($theirAttribute)) { + return false; + } + } + return $this->type === $node->type && $this->public === $node->public && $this->private === $node->private @@ -74,6 +89,7 @@ public static function __set_state(array $properties): ExportedNode $properties['private'], $properties['static'], $properties['readonly'], + $properties['attributes'], ); } @@ -91,6 +107,12 @@ public static function decode(array $data): ExportedNode $data['private'], $data['static'], $data['readonly'], + array_map(static function (array $parameterData): ExportedAttributeNode { + if ($parameterData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } @@ -110,6 +132,7 @@ public function jsonSerialize() 'private' => $this->private, 'static' => $this->static, 'readonly' => $this->readonly, + 'attributes' => $this->attributes, ], ]; } diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index f79883dea7..fb64727bc8 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -7,6 +7,8 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\Dependency\ExportedNode\ExportedAttributeArgumentNode; +use PHPStan\Dependency\ExportedNode\ExportedAttributeNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedClassNode; @@ -95,6 +97,7 @@ public function resolve(string $fileName, Node $node): ?ExportedNode throw new ShouldNotHappenException(); }, $adaptations), $this->exportClassStatements($node->stmts, $fileName, $className), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -138,6 +141,7 @@ public function resolve(string $fileName, Node $node): ?ExportedNode ), $implementsNames, $this->exportClassStatements($node->stmts, $fileName, $enumName), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -164,6 +168,7 @@ public function resolve(string $fileName, Node $node): ?ExportedNode $node->byRef, $this->printType($node->returnType), $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -243,6 +248,7 @@ private function exportParameterNodes(array $params): array $param->byRef, $param->variadic, $param->default !== null, + $this->exportAttributeNodes($param->attrGroups), ); } @@ -318,6 +324,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $node->isStatic(), $this->printType($node->returnType), $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), ); } } @@ -342,6 +349,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $node->isPrivate(), $node->isStatic(), $node->isReadonly(), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -357,6 +365,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $constants[] = new ExportedClassConstantNode( $const->name->toString(), $this->exprPrinter->printExpr($const->value), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -392,4 +401,32 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string return null; } + /** + * @param Node\AttributeGroup[] $attributeGroups + * @return ExportedAttributeNode[] + */ + private function exportAttributeNodes(array $attributeGroups): array + { + $nodes = []; + foreach ($attributeGroups as $attributeGroup) { + foreach ($attributeGroup->attrs as $attribute) { + $args = []; + foreach ($attribute->args as $arg) { + $args[] = new ExportedAttributeArgumentNode( + $arg->name !== null ? $arg->name->name : null, + $this->exprPrinter->printExpr($arg->value), + $arg->byRef, + ); + } + + $nodes[] = new ExportedAttributeNode( + $attribute->name->toString(), + $args, + ); + } + } + + return $nodes; + } + }