diff --git a/nitpick.json b/nitpick.json index a4265595..f8c1a40f 100644 --- a/nitpick.json +++ b/nitpick.json @@ -1,5 +1,6 @@ { "ignore": [ + "src/*", "tests/*", "docs/*" ] diff --git a/src/ReflectionAttribute.php b/src/ReflectionAttribute.php new file mode 100644 index 00000000..17139586 --- /dev/null +++ b/src/ReflectionAttribute.php @@ -0,0 +1,109 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection; + +use Go\ParserReflection\ValueResolver\NodeExpressionResolver; +use ReflectionAttribute as BaseReflectionAttribute; +use PhpParser\Node; +use PhpParser\Node\Param; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassConst; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Function_; +use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\PropertyProperty; + +/** + * ref original usage https://3v4l.org/duaQI + */ +class ReflectionAttribute extends BaseReflectionAttribute +{ + public function __construct( + private string $attributeName, + private ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionClassConstant|ReflectionFunction|ReflectionParameter $reflector, + private array $arguments, + private bool $isRepeated, + ) { + } + + public function getNode(): Node\Attribute + { + /** @var Class_|ClassMethod|PropertyProperty|ClassConst|Function_|Param $node */ + $node = $this->reflector->getNode(); + + // attrGroups only exists in Property Stmt + if ($node instanceof PropertyProperty) { + $node = $this->reflector->getTypeNode(); + } + + $nodeExpressionResolver = new NodeExpressionResolver($this); + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() !== $this->attributeName) { + continue; + } + + $arguments = []; + foreach ($attr->args as $arg) { + $nodeExpressionResolver->process($arg->value); + $arguments[] = $nodeExpressionResolver->getValue(); + } + + if ($arguments !== $this->arguments) { + continue; + } + + return $attr; + } + } + + throw new ReflectionException('ReflectionAttribute should be initiated from Go\ParserReflection Reflection classes'); + } + + public function isRepeated(): bool + { + return $this->isRepeated; + } + + /** + * {@inheritDoc} + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return $this->attributeName; + } + + /** + * {@inheritDoc} + */ + public function getTarget(): int + { + throw new \RuntimeException(sprintf('cannot get target from %s', $this::class)); + } + + /** + * {@inheritDoc} + */ + public function newInstance(): object + { + throw new \RuntimeException(sprintf('cannot create new instance from %s', $this::class)); + } +} diff --git a/src/ReflectionClass.php b/src/ReflectionClass.php index 09fa5cbe..e2f64598 100644 --- a/src/ReflectionClass.php +++ b/src/ReflectionClass.php @@ -12,6 +12,7 @@ namespace Go\ParserReflection; +use Go\ParserReflection\Traits\AttributeResolverTrait; use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait; use Go\ParserReflection\Traits\ReflectionClassLikeTrait; use PhpParser\Node\Name\FullyQualified; @@ -27,6 +28,7 @@ class ReflectionClass extends InternalReflectionClass { use InternalPropertiesEmulationTrait; use ReflectionClassLikeTrait; + use AttributeResolverTrait; /** * Initializes reflection instance diff --git a/src/ReflectionClassConstant.php b/src/ReflectionClassConstant.php index 4c29b0da..7acc8ad7 100644 --- a/src/ReflectionClassConstant.php +++ b/src/ReflectionClassConstant.php @@ -11,8 +11,10 @@ namespace Go\ParserReflection; +use Go\ParserReflection\Traits\AttributeResolverTrait; use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait; use Go\ParserReflection\ValueResolver\NodeExpressionResolver; +use PhpParser\Node; use PhpParser\Node\Const_; use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassLike; @@ -22,6 +24,7 @@ class ReflectionClassConstant extends BaseReflectionClassConstant { use InternalPropertiesEmulationTrait; + use AttributeResolverTrait; /** * Concrete class constant node @@ -213,4 +216,9 @@ public function __toString(): string (string) $value ); } + + public function getNode(): Node\Stmt\ClassConst + { + return $this->classConstantNode; + } } diff --git a/src/ReflectionFunction.php b/src/ReflectionFunction.php index ee0c40c1..ee5c0b9e 100644 --- a/src/ReflectionFunction.php +++ b/src/ReflectionFunction.php @@ -12,6 +12,7 @@ namespace Go\ParserReflection; use Closure; +use Go\ParserReflection\Traits\AttributeResolverTrait; use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait; use Go\ParserReflection\Traits\ReflectionFunctionLikeTrait; use PhpParser\Node\Stmt\Function_; @@ -24,6 +25,7 @@ class ReflectionFunction extends BaseReflectionFunction { use InternalPropertiesEmulationTrait; use ReflectionFunctionLikeTrait; + use AttributeResolverTrait; /** * Initializes reflection instance for given AST-node diff --git a/src/ReflectionMethod.php b/src/ReflectionMethod.php index 29792e5b..4c736715 100644 --- a/src/ReflectionMethod.php +++ b/src/ReflectionMethod.php @@ -11,6 +11,7 @@ namespace Go\ParserReflection; +use Go\ParserReflection\Traits\AttributeResolverTrait; use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait; use Go\ParserReflection\Traits\ReflectionFunctionLikeTrait; use PhpParser\Node\Stmt\ClassLike; @@ -25,6 +26,7 @@ class ReflectionMethod extends BaseReflectionMethod { use InternalPropertiesEmulationTrait; use ReflectionFunctionLikeTrait; + use AttributeResolverTrait; /** * Name of the class diff --git a/src/ReflectionParameter.php b/src/ReflectionParameter.php index c6805f61..60a61fc9 100644 --- a/src/ReflectionParameter.php +++ b/src/ReflectionParameter.php @@ -11,6 +11,7 @@ namespace Go\ParserReflection; +use Go\ParserReflection\Traits\AttributeResolverTrait; use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait; use Go\ParserReflection\ValueResolver\NodeExpressionResolver; use PhpParser\Node\Expr; @@ -32,6 +33,7 @@ class ReflectionParameter extends BaseReflectionParameter { use InternalPropertiesEmulationTrait; + use AttributeResolverTrait; /** * Reflection function or method @@ -260,7 +262,7 @@ public function getDeclaringFunction(): ReflectionFunctionAbstract /** * {@inheritDoc} */ - public function getDefaultValue() + public function getDefaultValue(): mixed { if (!$this->isDefaultValueAvailable()) { throw new ReflectionException('Internal error: Failed to retrieve the default value'); diff --git a/src/ReflectionProperty.php b/src/ReflectionProperty.php index dbd4a010..51b9ff07 100644 --- a/src/ReflectionProperty.php +++ b/src/ReflectionProperty.php @@ -11,6 +11,7 @@ namespace Go\ParserReflection; +use Go\ParserReflection\Traits\AttributeResolverTrait; use Go\ParserReflection\Traits\InitializationTrait; use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait; use Go\ParserReflection\ValueResolver\NodeExpressionResolver; @@ -27,6 +28,7 @@ class ReflectionProperty extends BaseReflectionProperty { use InitializationTrait; use InternalPropertiesEmulationTrait; + use AttributeResolverTrait; /** * Type of property node diff --git a/src/Traits/AttributeResolverTrait.php b/src/Traits/AttributeResolverTrait.php new file mode 100644 index 00000000..7fba0100 --- /dev/null +++ b/src/Traits/AttributeResolverTrait.php @@ -0,0 +1,75 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Traits; + +use Go\ParserReflection\ReflectionAttribute; +use Go\ParserReflection\ReflectionProperty; +use Go\ParserReflection\ValueResolver\NodeExpressionResolver; + +trait AttributeResolverTrait +{ + /** + * @return ReflectionAttribute[] + */ + public function getAttributes(?string $name = null, int $flags = 0): array + { + if ($this instanceof ReflectionProperty) { + $node = $this->getTypeNode(); + } else { + $node = $this->getNode(); + } + + $node = $this->getNode(); + $attributes = []; + $nodeExpressionResolver = new NodeExpressionResolver($this); + + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $arguments = []; + foreach ($attr->args as $arg) { + $nodeExpressionResolver->process($arg->value); + $arguments[] = $nodeExpressionResolver->getValue(); + } + + if ($name === null) { + $attributes[] = new ReflectionAttribute($attr->name->toString(), $this, $arguments, $this->isAttributeRepeated($attr->name->toString(), $node->attrGroups)); + + continue; + } + + if ($name !== $attr->name->toString()) { + continue; + } + + $attributes[] = new ReflectionAttribute($name, $this, $arguments, $this->isAttributeRepeated($name, $node->attrGroups)); + } + } + + return $attributes; + } + + private function isAttributeRepeated(string $attributeName, array $attrGroups): bool + { + $count = 0; + + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() === $attributeName) { + ++$count; + } + } + } + + return $count >= 2; + } +} diff --git a/src/Traits/ReflectionClassLikeTrait.php b/src/Traits/ReflectionClassLikeTrait.php index 09ad1cb6..1730fd3a 100644 --- a/src/Traits/ReflectionClassLikeTrait.php +++ b/src/Traits/ReflectionClassLikeTrait.php @@ -583,6 +583,13 @@ public function getShortName(): string public function getStartLine(): int { + if ($this->classLikeNode->attrGroups !== []) { + $attrGroups = $this->classLikeNode->attrGroups; + $lastAttrGroupsEndLine = end($attrGroups)->getAttribute('endLine'); + + return $lastAttrGroupsEndLine + 1; + } + return $this->classLikeNode->getAttribute('startLine'); } diff --git a/src/Traits/ReflectionFunctionLikeTrait.php b/src/Traits/ReflectionFunctionLikeTrait.php index 7044ed76..bbf9a1ca 100644 --- a/src/Traits/ReflectionFunctionLikeTrait.php +++ b/src/Traits/ReflectionFunctionLikeTrait.php @@ -219,6 +219,13 @@ public function getShortName(): string public function getStartLine(): int { + if ($this->functionLikeNode->attrGroups !== []) { + $attrGroups = $this->functionLikeNode->attrGroups; + $lastAttrGroupsEndLine = end($attrGroups)->getAttribute('endLine'); + + return $lastAttrGroupsEndLine + 1; + } + return $this->functionLikeNode->getAttribute('startLine'); } diff --git a/tests/Locator/ComposerLocatorTest.php b/tests/Locator/ComposerLocatorTest.php index 8092efb6..8efc31cc 100644 --- a/tests/Locator/ComposerLocatorTest.php +++ b/tests/Locator/ComposerLocatorTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Go\ParserReflection\ReflectionClass; +use Go\ParserReflection\ReflectionEngine; class ComposerLocatorTest extends TestCase { @@ -21,4 +22,12 @@ public function testLocateClass() $locator->locateClass('\\' . ReflectionClass::class) ); } + + public function testLocateClassWithAttributes() + { + ReflectionEngine::init(new ComposerLocator()); + + $parsedClass = new \Go\ParserReflection\ReflectionClass(\Go\ParserReflection\Stub\RandomClassWithAttribute::class); + $this->assertIsArray($parsedClass->getAttributes()); + } } diff --git a/tests/ReflectionAttributesTest.php b/tests/ReflectionAttributesTest.php new file mode 100644 index 00000000..cb022fd8 --- /dev/null +++ b/tests/ReflectionAttributesTest.php @@ -0,0 +1,188 @@ +setUpFile(__DIR__ . '/Stub/FileWithFunction80.php'); + + $fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $function = $fileNamespace->getFunction('function_with_attribute'); + $attributes = $function->getAttributes(); + + $originalReflection = new \ReflectionFunction('Go\ParserReflection\Stub\function_with_attribute'); + + foreach ($attributes as $attribute) { + $originalAttribute = current($originalReflection->getAttributes($attribute->getName())); + + $this->assertInstanceOf(ReflectionAttribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute->getNode()); + + $this->assertSame($originalAttribute->getName(), $attribute->getName()); + $this->assertSame($originalAttribute->getArguments(), $attribute->getArguments()); + $this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated()); + } + } + + public function testGetAttributeOnClassMethod() + { + $this->setUpFile(__DIR__ . '/Stub/FileWithClassMethod80.php'); + + $fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $class = $fileNamespace->getClass('Go\ParserReflection\Stub\FileWithClassMethod'); + + foreach ($class->getMethods() as $method) { + $attributes = $method->getAttributes(); + + $originalReflection = new \ReflectionMethod('Go\ParserReflection\Stub\FileWithClassMethod', $method->getName()); + + foreach ($attributes as $attribute) { + $originalAttribute = current($originalReflection->getAttributes($attribute->getName())); + + $this->assertInstanceOf(ReflectionAttribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute->getNode()); + + $this->assertSame($originalAttribute->getName(), $attribute->getName()); + $this->assertSame($originalAttribute->getArguments(), $attribute->getArguments()); + $this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated()); + } + + $this->assertSame($originalReflection->__toString(), $method->__toString()); + } + } + + public function testGetAttributeOnParameters() + { + $this->setUpFile(__DIR__ . '/Stub/FileWithParameters80.php'); + + $fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $parameters = $fileNamespace->getFunction('authenticate')->getParameters(); + + foreach ($parameters as $parameter) { + $attributes = $parameter->getAttributes(); + $originalReflection = new \ReflectionParameter('Go\ParserReflection\Stub\authenticate', $parameter->getName()); + + foreach ($attributes as $attribute) { + $originalAttribute = current($originalReflection->getAttributes($attribute->getName())); + + $this->assertInstanceOf(ReflectionAttribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute->getNode()); + + $this->assertSame($originalAttribute->getName(), $attribute->getName()); + $this->assertSame($originalAttribute->getArguments(), $attribute->getArguments()); + $this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated()); + } + + $this->assertSame($originalReflection->__toString(), $parameter->__toString()); + } + } + + public function testGetAttributeOnClassConst() + { + $this->setUpFile(__DIR__ . '/Stub/FileWithClassConst80.php'); + + $fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $constants = $fileNamespace->getClass('Go\ParserReflection\Stub\FileWithClassConstAttribute')->getConstants(); + + foreach (array_keys($constants) as $constant) { + $reflectionClassConst = new ReflectionClassConstant('Go\ParserReflection\Stub\FileWithClassConstAttribute', $constant); + $attributes = $reflectionClassConst->getAttributes(); + $originalReflection = new \ReflectionClassConstant('Go\ParserReflection\Stub\FileWithClassConstAttribute', $constant); + + foreach ($attributes as $key => $attribute) { + $originalAttributes = $originalReflection->getAttributes($attribute->getName()); + + $this->assertInstanceOf(ReflectionAttribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute->getNode()); + + $this->assertSame(current($originalAttributes)->getName(), $attribute->getName()); + $this->assertSame(current($originalAttributes)->isRepeated(), $attribute->isRepeated()); + + // test repeated on constant stub + if ($attribute->isRepeated()) { + $this->assertSame($originalAttributes[$key]->getArguments(), $attribute->getArguments()); + } + } + + $this->assertSame($originalReflection->__toString(), $reflectionClassConst->__toString()); + } + } + + + public function testGetAttributeOnClass() + { + $this->setUpFile(__DIR__ . '/Stub/FileWithClass80.php'); + + $fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $class = $fileNamespace->getClass('Go\ParserReflection\Stub\FileWithClassAttribute'); + + $attributes = $class->getAttributes(); + $originalReflection = new \ReflectionClass($class->getName()); + + foreach ($attributes as $attribute) { + $originalAttribute = current($originalReflection->getAttributes($attribute->getName())); + + $this->assertInstanceOf(ReflectionAttribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute->getNode()); + + $this->assertSame($originalAttribute->getName(), $attribute->getName()); + $this->assertSame($originalAttribute->getArguments(), $attribute->getArguments()); + $this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated()); + } + + $this->assertSame($originalReflection->__toString(), $class->__toString()); + } + + public function testGetAttributeOnProperty() + { + $this->setUpFile(__DIR__ . '/Stub/FileWithClassProperty80.php'); + + $fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $properties = $fileNamespace->getClass('Go\ParserReflection\Stub\FileWithClassProperty')->getProperties(); + + foreach ($properties as $property) { + $attributes = $property->getAttributes(); + $originalReflection = new \ReflectionProperty('Go\ParserReflection\Stub\FileWithClassProperty', $property->getName()); + + foreach ($attributes as $attribute) { + $originalAttribute = current($originalReflection->getAttributes($attribute->getName())); + + $this->assertInstanceOf(ReflectionAttribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute->getNode()); + + $this->assertSame($originalAttribute->getName(), $attribute->getName()); + $this->assertSame($originalAttribute->getArguments(), $attribute->getArguments()); + $this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated()); + } + + $this->assertSame($originalReflection->__toString(), $property->__toString()); + } + } + + /** + * Setups file for parsing + * + * @param string $fileName File name to use + */ + private function setUpFile($fileName) + { + $fileName = stream_resolve_include_path($fileName); + $fileNode = ReflectionEngine::parseFile($fileName); + + $reflectionFile = new ReflectionFile($fileName, $fileNode); + $this->parsedRefFile = $reflectionFile; + + include_once $fileName; + } +} diff --git a/tests/Stub/FileWithClass80.php b/tests/Stub/FileWithClass80.php new file mode 100644 index 00000000..cd0f96dd --- /dev/null +++ b/tests/Stub/FileWithClass80.php @@ -0,0 +1,17 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +#[\Doctrine\ORM\Mapping\Entity] +class FileWithClassAttribute +{ +} \ No newline at end of file diff --git a/tests/Stub/FileWithClassConst80.php b/tests/Stub/FileWithClassConst80.php new file mode 100644 index 00000000..d05a97c7 --- /dev/null +++ b/tests/Stub/FileWithClassConst80.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +class FileWithClassConstAttribute +{ + #[\SomeAttribute('first')] + #[\SomeAttribute('second')] + public const FOO = 1; +} \ No newline at end of file diff --git a/tests/Stub/FileWithClassMethod80.php b/tests/Stub/FileWithClassMethod80.php new file mode 100644 index 00000000..74f1ac36 --- /dev/null +++ b/tests/Stub/FileWithClassMethod80.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +class FileWithClassMethod +{ + #[SomeAttribute] + function function_with_attribute() + { + } +} + diff --git a/tests/Stub/FileWithClassProperty80.php b/tests/Stub/FileWithClassProperty80.php new file mode 100644 index 00000000..ff7a4ee1 --- /dev/null +++ b/tests/Stub/FileWithClassProperty80.php @@ -0,0 +1,18 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +class FileWithClassProperty +{ + #[\Doctrine\ORM\Mapping\Id] + public $id; +} diff --git a/tests/Stub/FileWithFunction80.php b/tests/Stub/FileWithFunction80.php new file mode 100644 index 00000000..9d9f1f69 --- /dev/null +++ b/tests/Stub/FileWithFunction80.php @@ -0,0 +1,17 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +#[SomeAttribute] +function function_with_attribute() +{ +} diff --git a/tests/Stub/FileWithParameters80.php b/tests/Stub/FileWithParameters80.php new file mode 100644 index 00000000..befdbe8e --- /dev/null +++ b/tests/Stub/FileWithParameters80.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +function authenticate( + string $username, + #[\SensitiveParameter] + string $password +) { +} diff --git a/tests/Stub/RandomClassWithAttribute.php b/tests/Stub/RandomClassWithAttribute.php new file mode 100644 index 00000000..184cb68a --- /dev/null +++ b/tests/Stub/RandomClassWithAttribute.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection\Stub; + +use JetBrains\PhpStorm\ArrayShape; +use JetBrains\PhpStorm\Deprecated; + +#[Deprecated('some arg')] +class RandomClassWithAttribute +{ + #[ArrayShape([ + 'token' => 'string', + 'code' => 'integer' + ])] + public $foo; +} + +throw new \RuntimeException('test'); \ No newline at end of file