Browse Source

Cleanup + comments

master
Adam Pippin 3 years ago
parent
commit
41b26006ac
  1. 25
      app/Cfnpp/Compiler.php
  2. 135
      app/Cfnpp/Expression/Expression.php
  3. 12
      app/Cfnpp/Expression/ICloudformationNative.php
  4. 25
      app/Cfnpp/Expression/Token.php
  5. 25
      app/Cfnpp/Expression/Token/NumericLiteral.php
  6. 63
      app/Cfnpp/Expression/Token/OperatorBinary.php
  7. 41
      app/Cfnpp/Expression/Token/OperatorUnary.php
  8. 35
      app/Cfnpp/Expression/Token/Parameter.php
  9. 29
      app/Cfnpp/Expression/Token/StringLiteral.php
  10. 30
      app/Cfnpp/Expression/Token/Variable.php
  11. 10
      app/Cfnpp/Functions.php
  12. 108
      app/Util/GraphNode.php
  13. 34
      app/Util/Stack.php

25
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;

135
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 <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');

12
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 <hello@adampippin.ca>
*/
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;
}

25
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 <hello@adampippin.ca>
*/
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);
}

25
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 <hello@adampippin.ca>
*/
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();

63
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 <hello@adampippin.ca>
*/
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();

41
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 <hello@adampippin.ca>
*/
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();

35
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 <hello@adampippin.ca>
*/
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()];

29
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 <hello@adampippin.ca>
*/
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();

30
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 <hello@adampippin.ca>
*/
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');

10
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;
}
*/
}

108
app/Util/GraphNode.php

@ -4,71 +4,143 @@ declare(strict_types=1);
namespace App\Util;
/**
* Represents a node in a tree.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
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)

34
app/Util/Stack.php

@ -4,25 +4,53 @@ declare(strict_types=1);
namespace App\Util;
/**
* Basic stack data structure.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
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);

Loading…
Cancel
Save