diff --git a/app/Cfnpp/Compiler.php b/app/Cfnpp/Compiler.php index 2c302f1..2e680eb 100644 --- a/app/Cfnpp/Compiler.php +++ b/app/Cfnpp/Compiler.php @@ -44,7 +44,7 @@ class Compiler implements \App\Engine\ICompile /** * Stores current state of the document so it can be mutated mid-pass. * - * @var Document + * @var ?Document */ protected $document; @@ -73,6 +73,15 @@ class Compiler implements \App\Engine\ICompile $this->merge_functions[$name] = $callback; } + /** + * Add a condition to the document currently being processed. + * + * The only appropriate place to call this is from the document functions + * + * @param string $name name of condition to create + * @param Node $node + * @return void + */ public function addCondition(string $name, Node $node): void { $conditions = $this->document->getChildByName('Conditions'); @@ -109,12 +118,11 @@ class Compiler implements \App\Engine\ICompile $cfnpp_functions = new Functions($this, $options); $cfnpp_functions->register($this); - $this->document = $this->pass_0($documents, $options); - $this->pass_1($this->document, $options); - $this->pass_2($this->document, $options); + $this->document = $document = $this->pass_0($documents, $options); + $this->pass_1($document, $options); + $this->pass_2($document, $options); return $this->document; - // Process each passed document /* foreach ($documents as $next_document) @@ -294,13 +302,14 @@ class Compiler implements \App\Engine\ICompile { $variables[] = $node->getValue(); } + /* + // TODO: Reimplement elseif ($node instanceof NodeFunctionValue && $node->getName() == 'expr') { - $parser = new \App\Cfnpp\Expression\Parser(); - $expression = $parser->parse($node->getValue()); + $expression = new \App\Cfnpp\Expression\Expression($node->getValue()); $variables = array_merge($variables, $expression->getReferencedVariables()); - } + }*/ } return $variables; diff --git a/app/Cfnpp/Expression/Expression.php b/app/Cfnpp/Expression/Expression.php index b0c4d97..1cb9791 100644 --- a/app/Cfnpp/Expression/Expression.php +++ b/app/Cfnpp/Expression/Expression.php @@ -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 + */ 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 $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 $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'); diff --git a/app/Cfnpp/Expression/ICloudformationNative.php b/app/Cfnpp/Expression/ICloudformationNative.php index 6b3dae1..338d907 100644 --- a/app/Cfnpp/Expression/ICloudformationNative.php +++ b/app/Cfnpp/Expression/ICloudformationNative.php @@ -4,7 +4,19 @@ declare(strict_types=1); namespace App\Cfnpp\Expression; +/** + * Token type that supports being converted directly to a CloudFormation intrinsic. + * + * @author Adam Pippin + */ interface ICloudformationNative { + /** + * Convert this token to a CloudFormation intrinsic, formatted as a simple + * PHP array. + * + * @param ?\App\Util\GraphNode[] $arguments requested token parameters + * @return mixed[] + */ public function toCloudformation(?array $arguments = null): array; } diff --git a/app/Cfnpp/Expression/Token.php b/app/Cfnpp/Expression/Token.php index 9bb13af..8164f08 100644 --- a/app/Cfnpp/Expression/Token.php +++ b/app/Cfnpp/Expression/Token.php @@ -5,13 +5,36 @@ declare(strict_types=1); namespace App\Cfnpp\Expression; /** - * Token parent class. + * Token parsed out of an expression. + * + * @author Adam Pippin */ abstract class Token { + /** + * Determine whether the passed stream contains an instance of this token. + * + * @param string $stream + * @return bool + */ abstract public static function isToken(string $stream): bool; + /** + * Consume a token from the passed stream. + * + * This method is expected to modify the stream to remove all + * consumed characters + * + * @param string $stream + * @return Token + */ abstract public static function getToken(string &$stream): Token; + /** + * Execute the token given the requested parameters. + * + * @param ?\App\Util\GraphNode[] $arguments + * @return \App\Util\GraphNode|Token|scalar|null + */ abstract public function execute(?array $arguments = null); } diff --git a/app/Cfnpp/Expression/Token/NumericLiteral.php b/app/Cfnpp/Expression/Token/NumericLiteral.php index d1bf00c..e27885c 100644 --- a/app/Cfnpp/Expression/Token/NumericLiteral.php +++ b/app/Cfnpp/Expression/Token/NumericLiteral.php @@ -7,15 +7,34 @@ namespace App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\TokenLiteral; +/** + * A number literal. + * + * @author Adam Pippin + */ class NumericLiteral extends TokenLiteral { + /** + * Value of this literal. + * @var int|float + */ protected $value; + /** + * New number literal. + * + * @param int|float $value + */ public function __construct($value) { $this->value = $value; } + /** + * Get the value of this literal. + * + * @return int|float + */ public function getValue() { return $this->value; @@ -55,6 +74,12 @@ class NumericLiteral extends TokenLiteral return new NumericLiteral($buffer); } + /** + * Get the value of this token. + * + * @param ?\App\Util\GraphNode[] $arguments + * @return \App\Util\GraphNode|Token|scalar|null + */ public function execute(?array $arguments = null) { return $this->getValue(); diff --git a/app/Cfnpp/Expression/Token/OperatorBinary.php b/app/Cfnpp/Expression/Token/OperatorBinary.php index 3715f2f..3510438 100644 --- a/app/Cfnpp/Expression/Token/OperatorBinary.php +++ b/app/Cfnpp/Expression/Token/OperatorBinary.php @@ -8,22 +8,45 @@ use App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\TokenBinary; use App\Cfnpp\Expression\ICloudformationNative; +/** + * Basic comparison operators that take two parameters. + * + * @author Adam Pippin + */ class OperatorBinary extends TokenBinary implements ICloudformationNative { + /** + * List of valid operators. + * @var string[] + */ public const OPERATORS = [ 'and', 'or', 'eq' ]; + /** + * The operator this instance represents. + * @var string + */ protected $operator; - public function __construct($operator) + /** + * New binary operator. + * + * @param string $operator + */ + public function __construct(string $operator) { $this->operator = $operator; } - public function getOperator() + /** + * Get the operator this instance represents. + * + * @return string + */ + public function getOperator(): string { return $this->operator; } @@ -56,6 +79,15 @@ class OperatorBinary extends TokenBinary implements ICloudformationNative throw new \Exception('Could not parse OperatorBinary'); } + /** + * Execute the operator. + * + * Suppressing accesses to arguments since this is guaranteed valid + * unless there are bugs in Expression. + * @suppress PhanTypeArraySuspiciousNullable + * @param ?\App\Util\GraphNode[] $arguments + * @return \App\Util\GraphNode|Token|scalar|null + */ public function execute(?array $arguments = null) { $value1 = $arguments[0]->getValue(); @@ -80,11 +112,9 @@ class OperatorBinary extends TokenBinary implements ICloudformationNative { return $value1; } - else - { + return null; - } - // no break + case 'or': if (is_scalar($value1) && is_scalar($value2)) { @@ -102,11 +132,9 @@ class OperatorBinary extends TokenBinary implements ICloudformationNative { return $value1; } - else - { + return null; - } - // no break + case 'eq': if (is_scalar($value1) && is_scalar($value2)) { @@ -116,16 +144,23 @@ class OperatorBinary extends TokenBinary implements ICloudformationNative { return true; } - else - { + return null; - } - // no break + default: throw new \Exception('Missing implementation for unary operator: '.$this->getOperator()); } } + /** + * Convert this token into a CloudFormation intrinsic. + * + * Suppressing accesses to arguments since this is guaranteed valid + * unless there are bugs in Expression. + * @suppress PhanTypeArraySuspiciousNullable + * @param ?\App\Util\GraphNode[] $arguments + * @return mixed[] + */ public function toCloudformation(?array $arguments = null): array { $value1 = $arguments[0]->getValue(); diff --git a/app/Cfnpp/Expression/Token/OperatorUnary.php b/app/Cfnpp/Expression/Token/OperatorUnary.php index 6fbc4a0..02acc1b 100644 --- a/app/Cfnpp/Expression/Token/OperatorUnary.php +++ b/app/Cfnpp/Expression/Token/OperatorUnary.php @@ -8,19 +8,42 @@ use App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\TokenUnary; use App\Cfnpp\Expression\ICloudformationNative; +/** + * Basic operators that take one parameter. + * + * @author Adam Pippin + */ class OperatorUnary extends TokenUnary implements ICloudformationNative { + /** + * List of valid operators. + * @var string[] + */ public const OPERATORS = [ 'not' ]; + /** + * The operator this instance represents. + * @var string + */ protected $operator; + /** + * New unary operator. + * + * @param string $operator + */ public function __construct($operator) { $this->operator = $operator; } + /** + * Get the operator this instance represents. + * + * @return string + */ public function getOperator() { return $this->operator; @@ -54,6 +77,15 @@ class OperatorUnary extends TokenUnary implements ICloudformationNative throw new \Exception('Could not parse OperatorUnary'); } + /** + * Execute the operator. + * + * Suppressing accesses to arguments since this is guaranteed valid + * unless there are bugs in Expression. + * @suppress PhanTypeArraySuspiciousNullable + * @param ?\App\Util\GraphNode[] $arguments + * @return \App\Util\GraphNode|Token|scalar|null + */ public function execute(?array $arguments = null) { $value = $arguments[0]->getValue(); @@ -66,6 +98,15 @@ class OperatorUnary extends TokenUnary implements ICloudformationNative } } + /** + * Convert this token into a CloudFormation intrinsic. + * + * Suppressing accesses to arguments since this is guaranteed valid + * unless there are bugs in Expression. + * @suppress PhanTypeArraySuspiciousNullable + * @param ?\App\Util\GraphNode[] $arguments + * @return mixed[] + */ public function toCloudformation(?array $arguments = null): array { $value = $arguments[0]->getValue(); diff --git a/app/Cfnpp/Expression/Token/Parameter.php b/app/Cfnpp/Expression/Token/Parameter.php index cd1b4be..cbbe37b 100644 --- a/app/Cfnpp/Expression/Token/Parameter.php +++ b/app/Cfnpp/Expression/Token/Parameter.php @@ -8,16 +8,35 @@ use App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\TokenLiteral; use App\Cfnpp\Expression\ICloudformationNative; +/** + * Represents a reference to a CloudFormation parameter. + * + * @author Adam Pippin + */ class Parameter extends TokenLiteral implements ICloudformationNative { + /** + * Name of the referenced parameter. + * @var string + */ protected $name; - public function __construct($name) + /** + * New parameter. + * @string $name + * @param string $name + */ + public function __construct(string $name) { $this->name = $name; } - public function getName() + /** + * Get the name of the parameter. + * + * @return string + */ + public function getName(): string { return $this->name; } @@ -48,11 +67,23 @@ class Parameter extends TokenLiteral implements ICloudformationNative return new Parameter($buffer); } + /** + * Get the value of this token. + * + * @param ?\App\Util\GraphNode[] $arguments + * @return \App\Util\GraphNode|Token|scalar|null + */ public function execute(?array $arguments = null) { throw new \Exception('Unreplaced parameter'); } + /** + * Return the cloudformation representation of a reference to a parameter. + * + * @param ?\App\Util\GraphNode[] $arguments + * @return mixed[] + */ public function toCloudformation(?array $arguments = null): array { return ['Ref' => $this->getName()]; diff --git a/app/Cfnpp/Expression/Token/StringLiteral.php b/app/Cfnpp/Expression/Token/StringLiteral.php index 9114e3d..7288831 100644 --- a/app/Cfnpp/Expression/Token/StringLiteral.php +++ b/app/Cfnpp/Expression/Token/StringLiteral.php @@ -7,16 +7,35 @@ namespace App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\TokenLiteral; +/** + * A string literal. + * + * @author Adam Pippin + */ class StringLiteral extends TokenLiteral { + /** + * Value of the string literal. + * @var string + */ protected $value; - public function __construct($value) + /** + * New string literal. + * + * @param string $value + */ + public function __construct(string $value) { $this->value = $value; } - public function getValue() + /** + * Get the value of this literal. + * + * @return string + */ + public function getValue(): string { return $this->value; } @@ -63,6 +82,12 @@ class StringLiteral extends TokenLiteral return new StringLiteral($buffer); } + /** + * Get the value of this token. + * + * @param ?\App\Util\GraphNode[] $arguments + * @return \App\Util\GraphNode|Token|scalar|null + */ public function execute(?array $arguments = null) { return $this->getValue(); diff --git a/app/Cfnpp/Expression/Token/Variable.php b/app/Cfnpp/Expression/Token/Variable.php index edb2b06..a1c94df 100644 --- a/app/Cfnpp/Expression/Token/Variable.php +++ b/app/Cfnpp/Expression/Token/Variable.php @@ -7,16 +7,35 @@ namespace App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\Token; use App\Cfnpp\Expression\TokenLiteral; +/** + * Reference to a variable. + * + * @author Adam Pippin + */ class Variable extends TokenLiteral { + /** + * Name of the variable. + * @var string + */ protected $name; - public function __construct($name) + /** + * New variable reference. + * + * @param string $name + */ + public function __construct(string $name) { $this->name = $name; } - public function getName() + /** + * Get the name of the variable. + * + * @return string + */ + public function getName(): string { return $this->name; } @@ -47,6 +66,13 @@ class Variable extends TokenLiteral return new Variable($buffer); } + /** + * Not valid. Variable replacement should be handled by the expression parser. + * + * @param ?\App\Util\GraphNode[] $arguments + * @throws \Exception if called + * @return \App\Util\GraphNode|Token|scalar|null + */ public function execute(?array $arguments = null) { throw new \Exception('Unreplaced variable'); diff --git a/app/Cfnpp/Functions.php b/app/Cfnpp/Functions.php index 79eb493..3ca96e8 100644 --- a/app/Cfnpp/Functions.php +++ b/app/Cfnpp/Functions.php @@ -119,6 +119,13 @@ class Functions return new NodeValue(null, $node->hasName() ? $node->getName() : null, $value); } + /** + * Create a reference to a CloudFormation parameter. + * + * @param Node $node + * @param NodeFunction $function + * @return ?Node + */ public function f_param(Node $node, NodeFunction $function): ?Node { if (!($function instanceof NodeFunctionValue)) @@ -201,6 +208,8 @@ class Functions * @param NodeFunction $function * @return ?Node */ + /* + * TODO: Reimplement public function f_expr(Node $node, NodeFunction $function): ?Node { if (!($function instanceof NodeFunctionValue)) @@ -217,4 +226,5 @@ class Functions return $result; } + */ } diff --git a/app/Util/GraphNode.php b/app/Util/GraphNode.php index e1eb212..1d08e00 100644 --- a/app/Util/GraphNode.php +++ b/app/Util/GraphNode.php @@ -4,71 +4,143 @@ declare(strict_types=1); namespace App\Util; +/** + * Represents a node in a tree. + * + * @author Adam Pippin + */ class GraphNode { + /** + * Parent of this node. + * @var GraphNode + */ protected $parent; + /** + * Value of this node. + * @var mixed + */ protected $value; + /** + * Children of this node. + * @var GraphNode[] + */ protected $children; + /** + * Create a new graph node. + * + * @param mixed $value + */ public function __construct($value = null) { $this->value = $value; $this->children = []; } - public function __invoke() - { - return $this->value; - } - + /** + * Set the value of this node. + * + * @param mixed $value + * @return void + */ public function setValue($value): void { $this->value = $value; } + /** + * Get the value of this node. + * + * @return mixed + */ public function getValue() { return $this->value; } + /** + * Set the parent node of this node. + * + * @param GraphNode $node + * @return void + */ public function setParent(GraphNode $node): void { $this->parent = $node; } + /** + * Determine whether this node has a parent. + * + * @return bool + */ public function hasParent(): bool { return isset($this->parent); } + /** + * Get this node's parent node. + * + * @return GraphNode + */ public function getParent(): GraphNode { return $this->parent; } + /** + * Count how many child nodes this node has. + * + * @return int + */ public function countChildren(): int { return sizeof($this->children); } + /** + * Determine whether this node has any children. + * + * @return bool + */ public function hasChildren(): bool { return sizeof($this->children) > 0; } + /** + * Fetch all of this node's children. + * + * @return GraphNode[] + */ public function getChildren(): array { return $this->children; } + /** + * Add a node to this child's list of children. + * + * @param GraphNode $child + * @return void + */ public function appendChild(GraphNode $child): void { $this->children[] = $child; $child->setParent($this); } + /** + * Replace a child node with another node. + * + * @param GraphNode $original + * @param GraphNode $new + * @return void + */ public function replaceChild(GraphNode $original, GraphNode $new): void { for ($i = 0; $i < sizeof($this->children); $i++) @@ -82,11 +154,22 @@ class GraphNode } } + /** + * Remove all children of this node. + * + * @return void + */ public function clearChildren(): void { $this->children = []; } + /** + * Add a child node by value. + * + * @param mixed $value value to add as a child + * @return GraphNode the node created as a child + */ public function add($value): GraphNode { $this->children[] = new GraphNode($value); @@ -94,11 +177,26 @@ class GraphNode return end($this->children); } + /** + * Walk through this node and all children, calling callback on each node. + * + * Callback is called on a node's children before a node + * + * @param callable $callback callback(GraphNode $node): void + * @return void + */ public function walk(callable $callback) { static::walkNodes([$this], $callback); } + /** + * Internal function for recursively visiting all nodes. + * + * @param GraphNode[] $nodes + * @param callable $callback + * @return void + */ protected static function walkNodes(array $nodes, callable $callback) { foreach ($nodes as $node) diff --git a/app/Util/Stack.php b/app/Util/Stack.php index f052a53..49926b9 100644 --- a/app/Util/Stack.php +++ b/app/Util/Stack.php @@ -4,25 +4,53 @@ declare(strict_types=1); namespace App\Util; +/** + * Basic stack data structure. + * + * @author Adam Pippin + */ class Stack { + /** + * Stack data. + * @var mixed[] + */ protected $stack; + /** + * Create a new stack. + */ public function __construct() { $this->stack = []; } + /** + * Count how many items this stack contains. + * + * @return int + */ public function count(): int { return sizeof($this->stack); } + /** + * Get all items in this stack. + * + * @return mixed[] + */ public function get(): array { return $this->stack; } + /** + * Pop an item off of this stack. + * + * @param int $offset offset from the end to pop if not the last element + * @return mixed + */ public function pop(int $offset = 0) { if (sizeof($this->stack) < $offset + 1) @@ -32,6 +60,12 @@ class Stack return array_splice($this->stack, -1 * ($offset + 1), 1)[0]; } + /** + * Push an item onto the end of the stack. + * + * @param mixed $value + * @return void + */ public function push($value): void { array_push($this->stack, $value);