|
|
@ -7,8 +7,18 @@ namespace App\Cfnpp\Expression; |
|
|
|
use App\Util\GraphNode; |
|
|
|
use App\Util\Stack; |
|
|
|
|
|
|
|
/** |
|
|
|
* Represents an expression and handles tokenizing, parsing, and executing |
|
|
|
* expressions. |
|
|
|
* |
|
|
|
* @author Adam Pippin <hello@adampippin.ca> |
|
|
|
*/ |
|
|
|
class Expression |
|
|
|
{ |
|
|
|
/** |
|
|
|
* References to all Token implementations this class can handle. |
|
|
|
* @var string[] |
|
|
|
*/ |
|
|
|
public const TOKEN_TYPES = [ |
|
|
|
Token\NumericLiteral::class, |
|
|
|
Token\OperatorUnary::class, |
|
|
@ -17,10 +27,26 @@ class Expression |
|
|
|
Token\Variable::class |
|
|
|
]; |
|
|
|
|
|
|
|
/** |
|
|
|
* Solved expression this represents. |
|
|
|
* @var GraphNode[] |
|
|
|
*/ |
|
|
|
protected $nodes; |
|
|
|
|
|
|
|
protected $solved = false; |
|
|
|
/** |
|
|
|
* Referencing to our compilation state for accessing variables/parameters. |
|
|
|
* @var \App\Engine\IOptions |
|
|
|
*/ |
|
|
|
protected $options; |
|
|
|
|
|
|
|
/** |
|
|
|
* Create a new expression. |
|
|
|
* |
|
|
|
* Expression is tokenized, parsed, and solved on creation. |
|
|
|
* |
|
|
|
* @param string $expression |
|
|
|
* @param \App\Engine\IOptions $options |
|
|
|
*/ |
|
|
|
public function __construct(string $expression, \App\Engine\IOptions $options) |
|
|
|
{ |
|
|
|
$tokens = static::tokenize($expression); |
|
|
@ -29,7 +55,16 @@ class Expression |
|
|
|
$this->options = $options; |
|
|
|
} |
|
|
|
|
|
|
|
public function isComplete() |
|
|
|
/** |
|
|
|
* Check whether this expression is 'complete'. |
|
|
|
* |
|
|
|
* Complete is defined as having been solved down to a single value, or array |
|
|
|
* of single values none of which are still tokens. That is, we have a set of |
|
|
|
* computed scalars and no unresolved var/param references. |
|
|
|
* |
|
|
|
* @return bool |
|
|
|
*/ |
|
|
|
public function isComplete(): bool |
|
|
|
{ |
|
|
|
// I think this works? If we resolved down to a flat set of values and |
|
|
|
// all are scalar, then we're done? |
|
|
@ -49,26 +84,55 @@ class Expression |
|
|
|
return $complete; |
|
|
|
} |
|
|
|
|
|
|
|
public function count() |
|
|
|
/** |
|
|
|
* Check how many values are contained in the solution. |
|
|
|
* |
|
|
|
* @return int |
|
|
|
*/ |
|
|
|
public function count(): int |
|
|
|
{ |
|
|
|
return sizeof($this->nodes); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Assuming the solution contains only a single value, fetch it. |
|
|
|
* |
|
|
|
* @return mixed |
|
|
|
*/ |
|
|
|
public function getValue() |
|
|
|
{ |
|
|
|
return $this->nodes[0]->getValue(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Fetch the solution as an array. |
|
|
|
* |
|
|
|
* Only valid if the solution is complete |
|
|
|
* |
|
|
|
* @return mixed[] |
|
|
|
*/ |
|
|
|
public function toArray(): array |
|
|
|
{ |
|
|
|
return static::unwrap($this->nodes); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Convert an incomplete solution into a CloudFormation condition using |
|
|
|
* CloudFormation intrinsic functions. |
|
|
|
* |
|
|
|
* @return mixed[] |
|
|
|
*/ |
|
|
|
public function toCloudformation(): array |
|
|
|
{ |
|
|
|
return static::cloudformation($this->nodes); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Convert an expression string into a series of tokens. |
|
|
|
* |
|
|
|
* @param string $expression |
|
|
|
* @return Token[] |
|
|
|
*/ |
|
|
|
protected static function tokenize(string $expression): array |
|
|
|
{ |
|
|
|
$tokens = []; |
|
|
@ -95,6 +159,12 @@ class Expression |
|
|
|
return $tokens; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Build a tree out of a series of tokens. |
|
|
|
* |
|
|
|
* @param Token[] $tokens |
|
|
|
* @return GraphNode[] |
|
|
|
*/ |
|
|
|
protected static function parse(array $tokens): array |
|
|
|
{ |
|
|
|
$stack = new Stack(); |
|
|
@ -115,6 +185,7 @@ class Expression |
|
|
|
{ |
|
|
|
$node = new GraphNode($token); |
|
|
|
$node->appendChild($stack->pop()); |
|
|
|
// @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement |
|
|
|
$node->appendChild($stack->pop()); |
|
|
|
$stack->push($node); |
|
|
|
} |
|
|
@ -122,6 +193,15 @@ class Expression |
|
|
|
return $stack->get(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Solve a tree of tokens by inserting all variable values and wherever possible |
|
|
|
* resolving all functions. |
|
|
|
* |
|
|
|
* @param GraphNode[] $nodes parsed tree |
|
|
|
* @param array<string,mixed> $variables variable names and values |
|
|
|
* @param string[] $parameters parameter names |
|
|
|
* @return GraphNode[] |
|
|
|
*/ |
|
|
|
protected static function solve(array $nodes, array $variables = [], array $parameters = []): array |
|
|
|
{ |
|
|
|
$root = new GraphNode(); |
|
|
@ -129,21 +209,24 @@ class Expression |
|
|
|
{ |
|
|
|
$root->appendChild($node); |
|
|
|
} |
|
|
|
$nodes = static::fillVariables($root->getChildren(), $variables, $parameters); |
|
|
|
$nodes = static::collapse($root->getChildren()); |
|
|
|
static::fillVariables($root->getChildren(), $variables, $parameters); |
|
|
|
static::collapse($root->getChildren()); |
|
|
|
return $root->getChildren(); |
|
|
|
} |
|
|
|
|
|
|
|
protected static function execute(array $nodes, array $variables = [], array $parameters = []) |
|
|
|
{ |
|
|
|
return static::unwrap(static::solve($nodes, $variables, $parameters)); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Convert a tree of of nodes into something that could be a valid CloudFormation |
|
|
|
* condition. |
|
|
|
* |
|
|
|
* @param GraphNode[] $nodes |
|
|
|
* @throws \Exception if the remaining nodes contain functions that cannot be expressed in cloudformation |
|
|
|
* @return mixed[] |
|
|
|
*/ |
|
|
|
protected static function cloudformation(array $nodes): array |
|
|
|
{ |
|
|
|
foreach ($nodes as $node) |
|
|
|
{ |
|
|
|
$node->walk(static function($node) { |
|
|
|
$node->walk(static function(GraphNode $node): void { |
|
|
|
if (is_scalar($node->getValue())) |
|
|
|
{ |
|
|
|
return; |
|
|
@ -164,11 +247,20 @@ class Expression |
|
|
|
return $nodes[0]->getValue(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Fill all variable values by replacing variable tokens with their actual values. |
|
|
|
* |
|
|
|
* @param GraphNode[] $nodes |
|
|
|
* @param array<string,mixed> $variables variable values |
|
|
|
* @param string[] $parameters parameter names |
|
|
|
* @throws \Exception if a reference is made to an undefined variable |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
protected static function fillVariables(array $nodes, array $variables, array $parameters): void |
|
|
|
{ |
|
|
|
foreach ($nodes as $node) |
|
|
|
{ |
|
|
|
$node->walk(static function($node) use ($variables, $parameters) { |
|
|
|
$node->walk(static function(GraphNode $node) use ($variables, $parameters): void { |
|
|
|
if ($node->getValue() instanceof Token\Variable) |
|
|
|
{ |
|
|
|
$var_name = $node->getValue()->getName(); |
|
|
@ -189,11 +281,17 @@ class Expression |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 'collapse' a node tree by executing nodes. |
|
|
|
* |
|
|
|
* @param GraphNode[] $nodes |
|
|
|
* @return void |
|
|
|
*/ |
|
|
|
protected static function collapse(array $nodes): void |
|
|
|
{ |
|
|
|
foreach ($nodes as $node) |
|
|
|
{ |
|
|
|
$node->walk(static function($node) { |
|
|
|
$node->walk(static function(GraphNode $node): void { |
|
|
|
if ($node->getValue() instanceof Token\Parameter) |
|
|
|
{ |
|
|
|
return; |
|
|
@ -221,9 +319,18 @@ class Expression |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Unwrap an array of nodes by returning their values. |
|
|
|
* |
|
|
|
* Not valid if any nodes still contain children, as the assumption is that |
|
|
|
* those nodes are unresolved. |
|
|
|
* |
|
|
|
* @param GraphNode[] $nodes |
|
|
|
* @return mixed[] |
|
|
|
*/ |
|
|
|
protected static function unwrap(array $nodes) |
|
|
|
{ |
|
|
|
return array_map(static function($node) { |
|
|
|
return array_map(/** @return mixed */ static function(GraphNode $node) { |
|
|
|
if ($node->hasChildren()) |
|
|
|
{ |
|
|
|
throw new \Exception('Cannot unwrap node: still has children'); |
|
|
|