Skip to content

Added DNF type support #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Added DNF types support
krzysztof-ciszewski committed May 19, 2024
commit bb52e474f32751b22fb3f396e463e464e07ebe22
14 changes: 13 additions & 1 deletion src/Aop/Pointcut/AndPointcut.php
Original file line number Diff line number Diff line change
@@ -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)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space before opening parenthesis of function call prohibited

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;
38 changes: 35 additions & 3 deletions src/Aop/Pointcut/ClassInheritancePointcut.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
14 changes: 14 additions & 0 deletions src/Aop/Pointcut/DNF/AST/Node.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\AST;

readonly class Node
{
public function __construct(
public NodeType $type,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 4 spaces, found 8

public ?string $identifier = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 4 spaces, found 8

public ?Node $left = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 4 spaces, found 8

public ?Node $right = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 4 spaces, found 8

) {
}
}
10 changes: 10 additions & 0 deletions src/Aop/Pointcut/DNF/AST/NodeType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\AST;

enum NodeType
{
case IDENTIFIER;
case AND;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

case OR;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

}
13 changes: 13 additions & 0 deletions src/Aop/Pointcut/DNF/Parser/Token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\Parser;

enum Token
{
case EOF;
case IDENTIFIER;
case LPAREN;
case RPAREN;
case AND;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

case OR;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

}
74 changes: 74 additions & 0 deletions src/Aop/Pointcut/DNF/Parser/TokenCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\Parser;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be one blank line after the namespace declaration

class TokenCollection implements TokenCollectionInterface
{
/**
* @param \ArrayIterator<\PhpToken> $tokens
*/
public function __construct(
private readonly \ArrayIterator $tokens

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 4 spaces, found 8

) {
}

/**
* @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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space before opening parenthesis of function call prohibited

chr(26) => Token::EOF,
'(' => Token::LPAREN,
')' => Token::RPAREN,
'|' => Token::OR,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

'&' => Token::AND,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

default => Token::IDENTIFIER

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 8 spaces, found 12

};
}
}
21 changes: 21 additions & 0 deletions src/Aop/Pointcut/DNF/Parser/TokenCollectionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\Parser;

interface TokenCollectionInterface
{
/**
* @throws \Exception
*/
public function expect(Token $token): void;

/**
* @return array{0: Token, 1: string|null}
*/
public function next(): array;

/**
* @return array{0: Token, 1: string|null}
*/
public function peek(int $i): array;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline at end of file; 0 found

113 changes: 113 additions & 0 deletions src/Aop/Pointcut/DNF/Parser/TokenizerParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\Parser;

use Exception;
use Go\Aop\Pointcut\DNF\AST\Node;
use Go\Aop\Pointcut\DNF\AST\NodeType;
use PhpToken;

class TokenizerParser implements TokenizerParserInterface
{
public function parse(string $input): Node
{
return $this->parseExpression($this->tokenize($input));
}

private function tokenize(string $input): TokenCollection
{
$input = sprintf('<?php %s%s', $input, chr(/* EOF */26));
$tokens = new \ArrayIterator(PhpToken::tokenize($input));
$arrayIntersect = array_intersect(
(array)$tokens,
['string', 'bool', 'array', 'float', 'int', 'integer', 'null', 'resource']
);
if ($arrayIntersect !== []) {
throw new Exception(sprintf('Tokens [%s] not allowed', implode(', ', $arrayIntersect)));
}

$tokens = new TokenCollection($tokens);
//skip '<?php'
$tokens->next();

return $tokens;
}

private function parseSubExpression(
TokenCollection $tokens,
int $bindingPower,
bool $insideParenthesis = false
): ?Node {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration; found 0 spaces

[$token, $val] = $tokens->next();
switch ($token) {
case Token::LPAREN:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CASE statements must be defined using a colon

$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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

throw new \Exception('Only intersections allowed in the group');
}

switch ($token) {
case Token::OR:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

case Token::AND:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found 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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space before opening parenthesis of function call prohibited

Token::OR => new Node(NodeType::OR, left: $left, right: $right),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

Token::AND => new Node(NodeType::AND, left: $left, right: $right),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

default => throw new Exception('invalid op')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 8 spaces, found 12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 8 spaces, found 12

};
}

/**
* @param Token $type
*
* @return array{0: int, 1: int}
*/
private function getBindingPower(Token $type): array
{
return match ($type) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space before opening parenthesis of function call prohibited

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space before opening parenthesis of function call prohibited

Token::OR => [1, 2],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found OR

Token::AND => [3, 4],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

default => throw new Exception('Invalid operator')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 8 spaces, found 12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 8 spaces, found 12

};
}

}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Expected 1 newline at end of file; 0 found
  • The closing brace for the class must go on the next line after the body

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Expected 1 newline at end of file; 0 found
  • The closing brace for the class must go on the next line after the body

8 changes: 8 additions & 0 deletions src/Aop/Pointcut/DNF/Parser/TokenizerParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF\Parser;

interface TokenizerParserInterface
{
public function parse(string $input);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline at end of file; 0 found

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline at end of file; 0 found

44 changes: 44 additions & 0 deletions src/Aop/Pointcut/DNF/SemanticAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF;

use Go\Aop\Pointcut\DNF\AST\Node;
use Go\Aop\Pointcut\DNF\AST\NodeType;
use Go\ParserReflection\ReflectionFileNamespace;

class SemanticAnalyzer implements SemanticAnalyzerInterface
{
public function verifyTree(?Node $tree, \ReflectionClass|ReflectionFileNamespace $val)
{
if ($tree !== null && $tree->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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected and but found AND

return $this->verifyTree($tree->left, $val) && $this->verifyTree($tree->right, $val);
}

if ($tree->type === NodeType::OR) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP keywords must be lowercase; expected or but found 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;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline at end of file; 0 found

11 changes: 11 additions & 0 deletions src/Aop/Pointcut/DNF/SemanticAnalyzerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of line character is invalid; expected \n but found \r\n


namespace Go\Aop\Pointcut\DNF;

use Go\Aop\Pointcut\DNF\AST\Node;
use Go\ParserReflection\ReflectionFileNamespace;

interface SemanticAnalyzerInterface
{
public function verifyTree(Node $tree, \ReflectionClass|ReflectionFileNamespace $val);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline at end of file; 0 found

14 changes: 12 additions & 2 deletions src/Aop/Pointcut/PointcutGrammar.php
Original file line number Diff line number Diff line change
@@ -266,14 +266,18 @@ function (
->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),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Line indented incorrectly; expected 16 spaces, found 20
  • Space before opening parenthesis of function call prohibited

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', ')')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space found before comma in function call

->call($stringConverter)
->is('namespacePattern', '|', 'namespacePattern')
->call($stringConverter)
;

$this('namePatternPart')
7 changes: 7 additions & 0 deletions src/Core/Container.php
Original file line number Diff line number Diff line change
@@ -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