From 10637e0b788eea131981f5599d76b0a03c0b6969 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 | 76 +++++++++++++++ .../ExportedNode/ExportedAttributeNode.php | 92 +++++++++++++++++++ .../ExportedClassConstantNode.php | 32 ++++++- .../ExportedNode/ExportedClassNode.php | 28 +++++- .../ExportedNode/ExportedEnumNode.php | 33 ++++++- .../ExportedNode/ExportedFunctionNode.php | 23 ++++- .../ExportedNode/ExportedMethodNode.php | 23 ++++- .../ExportedNode/ExportedParameterNode.php | 25 ++++- .../ExportedNode/ExportedPropertiesNode.php | 25 ++++- src/Dependency/ExportedNodeResolver.php | 47 +++++++++- 10 files changed, 385 insertions(+), 19 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 00000000000..11678bfda9a --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeArgumentNode.php @@ -0,0 +1,76 @@ +name = $name; + $this->value = $value; + $this->byRef = $byRef; + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + return $this->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 00000000000..3c2a851bd4b --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeNode.php @@ -0,0 +1,92 @@ +name = $name; + $this->args = $args; + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + if (count($this->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 \PHPStan\ShouldNotHappenException(); + } + return ExportedAttributeArgumentNode::decode($parameterData['data']); + }, $data['args']) + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index c24510b3cca..ee7f1277dc6 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -9,7 +9,14 @@ 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 +26,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 +50,7 @@ public static function __set_state(array $properties): ExportedNode return new self( $properties['name'], $properties['value'], + $properties['attributes'], ); } @@ -44,7 +63,13 @@ 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 \PHPStan\ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), + ); } /** @@ -58,7 +83,8 @@ 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 c46001f5151..7982e0c25bb 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -17,7 +17,8 @@ class ExportedClassNode implements ExportedNode, JsonSerializable * @param string[] $usedTraits * @param ExportedTraitUseAdaptation[] $traitUseAdaptations * @param ExportedNode[] $statements - */ + * @param ExportedAttributeNode[] $attributes + */ public function __construct( private string $name, private ?ExportedPhpDocNode $phpDoc, @@ -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; } @@ -96,8 +108,9 @@ public static function __set_state(array $properties): ExportedNode $properties['implements'], $properties['usedTraits'], $properties['traitUseAdaptations'], - $properties['statements'], - ); + $properties['statements'], + $properties['attributes'], + ); } /** @@ -118,7 +131,8 @@ 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 \PHPStan\ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($parameterData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php index 831a823658b..b4eb5288c8b 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; @@ -65,7 +86,8 @@ public static function __set_state(array $properties): ExportedNode $properties['scalarType'], $properties['phpDoc'], $properties['implements'], - $properties['statements'], + $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 58f2704c790..d0f476a067f 100644 --- a/src/Dependency/ExportedNode/ExportedFunctionNode.php +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -14,13 +14,15 @@ class ExportedFunctionNode implements ExportedNode, JsonSerializable /** * @param ExportedParameterNode[] $parameters - */ + * @param ExportedAttributeNode[] $attributes + */ public function __construct( private string $name, private ?ExportedPhpDocNode $phpDoc, 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 7cb3376fabe..b0e5725097e 100644 --- a/src/Dependency/ExportedNode/ExportedMethodNode.php +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -14,7 +14,8 @@ class ExportedMethodNode implements ExportedNode, JsonSerializable /** * @param ExportedParameterNode[] $parameters - */ + * @param ExportedAttributeNode[] + */ public function __construct( private string $name, private ?ExportedPhpDocNode $phpDoc, @@ -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 ad36f20c425..ab8f97c29c2 100644 --- a/src/Dependency/ExportedNode/ExportedParameterNode.php +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -9,12 +9,16 @@ 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 +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->type === $node->type && $this->byRef === $node->byRef @@ -44,7 +59,8 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['variadic'], $properties['hasDefault'], - ); + $properties['attributes'], + ); } /** @@ -61,6 +77,7 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'variadic' => $this->variadic, 'hasDefault' => $this->hasDefault, + 'attributes' => $this->attributes, ], ]; } @@ -77,6 +94,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 2b61556824d..9afca6d0ef5 100644 --- a/src/Dependency/ExportedNode/ExportedPropertiesNode.php +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -12,6 +12,7 @@ class ExportedPropertiesNode implements JsonSerializable, ExportedNode /** * @param string[] $names + * @param ExportedAttributeNode[] $attributes */ public function __construct( private array $names, @@ -21,6 +22,7 @@ public function __construct( private bool $private, private bool $static, private bool $readonly, + private array $attributes, ) { } @@ -53,6 +55,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,7 +87,8 @@ public static function __set_state(array $properties): ExportedNode $properties['private'], $properties['static'], $properties['readonly'], - ); + $properties['attributes'] + ); } /** @@ -91,6 +105,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,7 +130,8 @@ 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 f79883dea70..e18dd70a016 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,7 +97,8 @@ public function resolve(string $fileName, Node $node): ?ExportedNode throw new ShouldNotHappenException(); }, $adaptations), $this->exportClassStatements($node->stmts, $fileName, $className), - ); + $this->exportAttributeNodes($node->attrGroups), + ); } if ($node instanceof Node\Stmt\Interface_ && isset($node->namespacedName)) { @@ -138,7 +141,8 @@ public function resolve(string $fileName, Node $node): ?ExportedNode ), $implementsNames, $this->exportClassStatements($node->stmts, $fileName, $enumName), - ); + $this->exportAttributeNodes($node->attrGroups), + ); } if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { @@ -164,7 +168,8 @@ public function resolve(string $fileName, Node $node): ?ExportedNode $node->byRef, $this->printType($node->returnType), $this->exportParameterNodes($node->params), - ); + $this->exportAttributeNodes($node->attrGroups), + ); } return null; @@ -243,7 +248,8 @@ private function exportParameterNodes(array $params): array $param->byRef, $param->variadic, $param->default !== null, - ); + $this->exportAttributeNodes($param->attrGroups), + ); } return $nodes; @@ -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,7 +365,8 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $constants[] = new ExportedClassConstantNode( $const->name->toString(), $this->exprPrinter->printExpr($const->value), - ); + $this->exportAttributeNodes($node->attrGroups), + ); } return new ExportedClassConstantsNode( @@ -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; + } + }