diff --git a/src/Aop/Pointcut/AndPointcut.php b/src/Aop/Pointcut/AndPointcut.php index c1502655..b5c91fef 100644 --- a/src/Aop/Pointcut/AndPointcut.php +++ b/src/Aop/Pointcut/AndPointcut.php @@ -39,8 +39,20 @@ /** * And constructor */ - public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts) + public function __construct(int $pointcutKind = null) { + $args = func_get_args(); + + if (is_array($args[1])) { + $pointcuts = $args[1]; + } else { + $pointcuts = array_slice($args, 1); + } + + if (count(array_filter($pointcuts, static fn ($pointcut) => $pointcut instanceof Pointcut)) !== count($pointcuts)) { + throw new \Exception('only pointcats allowed to be passed'); + } + // If we don't have specified kind, it will be calculated as intersection then if (!isset($pointcutKind)) { $pointcutKind = -1; diff --git a/src/Aop/Pointcut/ClassInheritancePointcut.php b/src/Aop/Pointcut/ClassInheritancePointcut.php index df2b1644..987f1454 100644 --- a/src/Aop/Pointcut/ClassInheritancePointcut.php +++ b/src/Aop/Pointcut/ClassInheritancePointcut.php @@ -13,6 +13,9 @@ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; +use Go\Aop\Pointcut\DNF\Parser\TokenizerParserInterface; +use Go\Aop\Pointcut\DNF\SemanticAnalyzerInterface; +use Go\Core\AspectKernel; use Go\ParserReflection\ReflectionFileNamespace; use ReflectionClass; use ReflectionFunction; @@ -25,11 +28,25 @@ */ final readonly class ClassInheritancePointcut implements Pointcut { + private const DNF_TOKENS = ['(', ')', '&', '|']; + + private TokenizerParserInterface $tokenizerParser; + private SemanticAnalyzerInterface $semanticAnalyzer; + /** * Inheritance class matcher constructor + * * @param (string&class-string) $parentClassOrInterfaceName Parent class or interface name to match in hierarchy */ - public function __construct(private string $parentClassOrInterfaceName) {} + public function __construct(private string $parentClassOrInterfaceName) + { + $this->tokenizerParser = AspectKernel::getInstance()->getContainer()->getService( + TokenizerParserInterface::class + ); + $this->semanticAnalyzer = AspectKernel::getInstance()->getContainer()->getService( + SemanticAnalyzerInterface::class + ); + } public function matches( ReflectionClass|ReflectionFileNamespace $context, @@ -42,12 +59,27 @@ public function matches( return false; } - // Otherwise, we match only if given context is child of given previously class name (either interface or class) - return $context->isSubclassOf($this->parentClassOrInterfaceName) || in_array($this->parentClassOrInterfaceName, $context->getInterfaceNames()); + if (!$this->isDNFType()) { + // Otherwise, we match only if given context is child of given previously class name (either interface or class) + return $context->isSubclassOf($this->parentClassOrInterfaceName) || in_array($this->parentClassOrInterfaceName, $context->getInterfaceNames()); + } + + return $this->checkDNFType($context); } public function getKind(): int { return self::KIND_CLASS; } + + private function isDNFType(): bool + { + return array_intersect(str_split($this->parentClassOrInterfaceName), self::DNF_TOKENS) !== []; + } + + private function checkDNFType(ReflectionClass|ReflectionFileNamespace $context): bool + { + $ast = $this->tokenizerParser->parse($this->parentClassOrInterfaceName); + return $this->semanticAnalyzer->verifyTree($ast, $context); + } } diff --git a/src/Aop/Pointcut/DNF/AST/Node.php b/src/Aop/Pointcut/DNF/AST/Node.php new file mode 100644 index 00000000..9293c7b0 --- /dev/null +++ b/src/Aop/Pointcut/DNF/AST/Node.php @@ -0,0 +1,14 @@ + $tokens + */ + public function __construct( + private readonly \ArrayIterator $tokens + ) { + } + + /** + * @inheritDoc + */ + public function expect(Token $token): void + { + /** @var Token $nextToken */ + [$nextToken] = $this->next(); + + if ($nextToken !== $token) { + throw new \Exception(sprintf('Expected %s, but got %s', $token->name, $nextToken->name)); + } + } + + /** + * @inheritDoc + */ + public function next(): array + { + if (! $this->tokens->valid()) { + return [Token::EOF, null]; + } + + $val = trim($this->tokens->current()->text); + + if ($val === '') { + return $this->next(); + } + + $return = [$this->getToken($val), $val]; + $this->tokens->next(); + + return $return; + } + + /** + * @inheritDoc + */ + public function peek(int $i): array + { + $offset = $this->tokens->key() + $i; + if (! $this->tokens->offsetExists($offset)) { + return [Token::EOF, null]; + } + + $val = trim($this->tokens->offsetGet($offset)->text); + + return [$this->getToken($val), $val]; + } + + private function getToken(string $val): Token + { + return match ($val) { + chr(26) => Token::EOF, + '(' => Token::LPAREN, + ')' => Token::RPAREN, + '|' => Token::OR, + '&' => Token::AND, + default => Token::IDENTIFIER + }; + } +} diff --git a/src/Aop/Pointcut/DNF/Parser/TokenCollectionInterface.php b/src/Aop/Pointcut/DNF/Parser/TokenCollectionInterface.php new file mode 100644 index 00000000..cc0c18e4 --- /dev/null +++ b/src/Aop/Pointcut/DNF/Parser/TokenCollectionInterface.php @@ -0,0 +1,21 @@ +parseExpression($this->tokenize($input)); + } + + private function tokenize(string $input): TokenCollection + { + $input = sprintf('next(); + + return $tokens; + } + + private function parseSubExpression( + TokenCollection $tokens, + int $bindingPower, + bool $insideParenthesis = false + ): ?Node { + [$token, $val] = $tokens->next(); + switch ($token) { + case Token::LPAREN: + $left = $this->parseSubexpression($tokens, 0, true); + $tokens->expect(Token::RPAREN); + break; + case Token::IDENTIFIER: + $left = new Node(NodeType::IDENTIFIER, $val); + break; + default: + throw new Exception('Bad Token'); + } + + while (true) { + [$token] = $tokens->peek(0); + + if ($token === Token::OR && $insideParenthesis) { + throw new \Exception('Only intersections allowed in the group'); + } + + switch ($token) { + case Token::OR: + case Token::AND: + [$leftBP, $rightBP] = $this->getBindingPower($token); + if ($leftBP < $bindingPower) { + break 2; + } + $tokens->next(); + $right = $this->parseSubexpression($tokens, $rightBP, $insideParenthesis); + $left = $this->operatorNode($token, $left, $right); + break; + case Token::RPAREN: + case Token::EOF: + default: + break 2; + } + } + + return $left; + } + + private function parseExpression(TokenCollection $tokens): ?Node + { + $sub = $this->parseSubExpression($tokens, 0); + $tokens->expect(Token::EOF); + + return $sub; + } + + private function operatorNode(Token $type, Node $left, ?Node $right): Node + { + return match ($type) { + Token::OR => new Node(NodeType::OR, left: $left, right: $right), + Token::AND => new Node(NodeType::AND, left: $left, right: $right), + default => throw new Exception('invalid op') + }; + } + + /** + * @param Token $type + * + * @return array{0: int, 1: int} + */ + private function getBindingPower(Token $type): array + { + return match ($type) { + Token::OR => [1, 2], + Token::AND => [3, 4], + default => throw new Exception('Invalid operator') + }; + } + +} \ No newline at end of file diff --git a/src/Aop/Pointcut/DNF/Parser/TokenizerParserInterface.php b/src/Aop/Pointcut/DNF/Parser/TokenizerParserInterface.php new file mode 100644 index 00000000..0c3b090b --- /dev/null +++ b/src/Aop/Pointcut/DNF/Parser/TokenizerParserInterface.php @@ -0,0 +1,8 @@ +type === NodeType::IDENTIFIER && $tree->identifier !== null) { + $parentClasses = $this->getParentClasses($val); + + return in_array($tree->identifier, $parentClasses, true) + || $val->implementsInterface($tree->identifier); + } + + if ($tree?->type === NodeType::AND) { + return $this->verifyTree($tree->left, $val) && $this->verifyTree($tree->right, $val); + } + + if ($tree->type === NodeType::OR) { + return $this->verifyTree($tree->left, $val) || $this->verifyTree($tree->right, $val); + } + } + + /** + * @param ReflectionFileNamespace|\ReflectionClass $val + * + * @return array + */ + public function getParentClasses(ReflectionFileNamespace|\ReflectionClass $val): array + { + $parentClasses = []; + while ($val->getParentClass()) { + $parentClasses[] = $val->getParentClass()->getName(); + $val = $val->getParentClass(); + } + + return $parentClasses; + } +} \ No newline at end of file diff --git a/src/Aop/Pointcut/DNF/SemanticAnalyzerInterface.php b/src/Aop/Pointcut/DNF/SemanticAnalyzerInterface.php new file mode 100644 index 00000000..bb8b6aa3 --- /dev/null +++ b/src/Aop/Pointcut/DNF/SemanticAnalyzerInterface.php @@ -0,0 +1,11 @@ +is('namespacePattern') ->call( function ($pattern) { - return $pattern === '**' ? new TruePointcut() : new NamePointcut(Pointcut::KIND_ALL, $pattern, true); } ) ->is('namespacePattern', '+') - ->call(fn($parentClassName) => new ClassInheritancePointcut($parentClassName)) + ->call(static function (...$args) { + return array_map( + static fn (string $class) => new ClassInheritancePointcut($class), + array_filter($args, 'is_string') + ); + }) ; $this('argumentList') @@ -301,6 +305,12 @@ function () { ->call($stringConverter) ->is('namespacePattern', 'nsSeparator', '**') ->call($stringConverter) + ->is('namespacePattern', '&', 'namespacePattern') + ->call($stringConverter) + ->is('(' , 'namespacePattern', '&', 'namespacePattern', ')') + ->call($stringConverter) + ->is('namespacePattern', '|', 'namespacePattern') + ->call($stringConverter) ; $this('namePatternPart') diff --git a/src/Core/Container.php b/src/Core/Container.php index 562783e0..8409310f 100644 --- a/src/Core/Container.php +++ b/src/Core/Container.php @@ -15,6 +15,10 @@ use Closure; use Go\Aop\Aspect; use Go\Aop\AspectException; +use Go\Aop\Pointcut\DNF\Parser\TokenizerParser; +use Go\Aop\Pointcut\DNF\Parser\TokenizerParserInterface; +use Go\Aop\Pointcut\DNF\SemanticAnalyzer; +use Go\Aop\Pointcut\DNF\SemanticAnalyzerInterface; use Go\Aop\Pointcut\PointcutGrammar; use Go\Aop\Pointcut\PointcutLexer; use Go\Aop\Pointcut\PointcutParser; @@ -96,6 +100,9 @@ public function __construct(array $resources = []) $this->addLazy(CachePathManager::class, fn(AspectContainer $container) => new CachePathManager( $container->getService(AspectKernel::class) )); + + $this->addLazy(TokenizerParserInterface::class, static fn(AspectContainer $c) => new TokenizerParser()); + $this->addLazy(SemanticAnalyzerInterface::class, static fn(AspectContainer $c) => new SemanticAnalyzer()); } final public function registerAspect(Aspect $aspect): void