diff --git a/src/Dependency/ExportedNode/ExportedAttributeNode.php b/src/Dependency/ExportedNode/ExportedAttributeNode.php new file mode 100644 index 0000000000..02714f2f59 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedAttributeNode.php @@ -0,0 +1,85 @@ + $args argument name or index(string|int) => value expression (string) + */ + public function __construct( + private string $name, + private array $args, + ) + { + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + if ($this->name !== $node->name) { + return false; + } + + if (count($this->args) !== count($node->args)) { + return false; + } + + foreach ($this->args as $argName => $argValue) { + if (!isset($node->args[$argName]) || $argValue !== $node->args[$argName]) { + return false; + } + } + + return true; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): ExportedNode + { + return new self( + $properties['name'], + $properties['args'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + 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'], + $data['args'], + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index c24510b3cc..aab53925f1 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,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; + } + } + return $this->name === $node->name && $this->value === $node->value; } @@ -32,6 +52,7 @@ public static function __set_state(array $properties): ExportedNode return new self( $properties['name'], $properties['value'], + $properties['attributes'], ); } @@ -44,6 +65,12 @@ public static function decode(array $data): ExportedNode return new self( $data['name'], $data['value'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } @@ -58,6 +85,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..937fd9a7df 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 $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php index 831a823658..f01c1c2c97 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,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; + } + } + return $this->name === $node->name && $this->scalarType === $node->scalarType && $this->implements === $node->implements; @@ -66,6 +85,7 @@ public static function __set_state(array $properties): ExportedNode $properties['phpDoc'], $properties['implements'], $properties['statements'], + $properties['attributes'], ); } @@ -83,6 +103,7 @@ public function jsonSerialize() 'phpDoc' => $this->phpDoc, 'implements' => $this->implements, 'statements' => $this->statements, + 'attributes' => $this->attributes, ], ]; } @@ -103,6 +124,12 @@ public static function decode(array $data): ExportedNode return $nodeType::decode($node['data']); }, $data['statements']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedFunctionNode.php b/src/Dependency/ExportedNode/ExportedFunctionNode.php index 58f2704c79..795cdb2ed9 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,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; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->returnType === $node->returnType; @@ -71,6 +83,7 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['returnType'], $properties['parameters'], + $properties['attributes'], ); } @@ -88,6 +101,7 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } @@ -109,6 +123,12 @@ public static function decode(array $data): ExportedNode } return ExportedParameterNode::decode($parameterData['data']); }, $data['parameters']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedMethodNode.php b/src/Dependency/ExportedNode/ExportedMethodNode.php index 7cb3376fab..a60a6d205e 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,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; + } + } + return $this->name === $node->name && $this->byRef === $node->byRef && $this->public === $node->public @@ -86,6 +98,7 @@ public static function __set_state(array $properties): ExportedNode $properties['static'], $properties['returnType'], $properties['parameters'], + $properties['attributes'], ); } @@ -108,6 +121,7 @@ public function jsonSerialize() 'static' => $this->static, 'returnType' => $this->returnType, 'parameters' => $this->parameters, + 'attributes' => $this->attributes, ], ]; } @@ -134,6 +148,12 @@ public static function decode(array $data): ExportedNode } return ExportedParameterNode::decode($parameterData['data']); }, $data['parameters']), + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedParameterNode.php b/src/Dependency/ExportedNode/ExportedParameterNode.php index ad36f20c42..d205afafdb 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,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; + } + } + return $this->name === $node->name && $this->type === $node->type && $this->byRef === $node->byRef @@ -44,6 +61,7 @@ public static function __set_state(array $properties): ExportedNode $properties['byRef'], $properties['variadic'], $properties['hasDefault'], + $properties['attributes'], ); } @@ -61,6 +79,7 @@ public function jsonSerialize() 'byRef' => $this->byRef, 'variadic' => $this->variadic, 'hasDefault' => $this->hasDefault, + 'attributes' => $this->attributes, ], ]; } @@ -77,6 +96,12 @@ public static function decode(array $data): ExportedNode $data['byRef'], $data['variadic'], $data['hasDefault'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } diff --git a/src/Dependency/ExportedNode/ExportedPropertiesNode.php b/src/Dependency/ExportedNode/ExportedPropertiesNode.php index 2b61556824..c801f25642 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,16 @@ public function equals(ExportedNode $node): bool } } + if (count($this->attributes) !== count($node->attributes)) { + return false; + } + + foreach ($this->attributes as $i => $attribute) { + if (!$attribute->equals($node->attributes[$i])) { + return false; + } + } + return $this->type === $node->type && $this->public === $node->public && $this->private === $node->private @@ -74,6 +88,7 @@ public static function __set_state(array $properties): ExportedNode $properties['private'], $properties['static'], $properties['readonly'], + $properties['attributes'], ); } @@ -91,6 +106,12 @@ public static function decode(array $data): ExportedNode $data['private'], $data['static'], $data['readonly'], + array_map(static function (array $attributeData): ExportedAttributeNode { + if ($attributeData['type'] !== ExportedAttributeNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedAttributeNode::decode($attributeData['data']); + }, $data['attributes']), ); } @@ -110,6 +131,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..a03e883664 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\Dependency\ExportedNode\ExportedAttributeNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedClassNode; @@ -95,6 +96,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 +140,7 @@ public function resolve(string $fileName, Node $node): ?ExportedNode ), $implementsNames, $this->exportClassStatements($node->stmts, $fileName, $enumName), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -164,6 +167,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 +247,7 @@ private function exportParameterNodes(array $params): array $param->byRef, $param->variadic, $param->default !== null, + $this->exportAttributeNodes($param->attrGroups), ); } @@ -318,6 +323,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 +348,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string $node->isPrivate(), $node->isStatic(), $node->isReadonly(), + $this->exportAttributeNodes($node->attrGroups), ); } @@ -357,6 +364,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 +400,28 @@ 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 $i => $arg) { + $args[$arg->name->name ?? $i] = $this->exprPrinter->printExpr($arg->value); + } + + $nodes[] = new ExportedAttributeNode( + $attribute->name->toString(), + $args, + ); + } + } + + return $nodes; + } + }