GitHub Actions Runner

This commit is contained in:
Tingluo Huang
2019-10-10 00:52:42 -04:00
commit c8afc84840
1255 changed files with 198670 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class EvaluationOptions
{
public EvaluationOptions()
{
}
public EvaluationOptions(EvaluationOptions copy)
{
if (copy != null)
{
MaxMemory = copy.MaxMemory;
}
}
public Int32 MaxMemory { get; set; }
}
}

View File

@@ -0,0 +1,453 @@
using System;
using System.ComponentModel;
using System.Globalization;
using GitHub.DistributedTask.Expressions2.Sdk;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class EvaluationResult
{
internal EvaluationResult(
EvaluationContext context,
Int32 level,
Object val,
ValueKind kind,
Object raw)
: this(context, level, val, kind, raw, false)
{
}
internal EvaluationResult(
EvaluationContext context,
Int32 level,
Object val,
ValueKind kind,
Object raw,
Boolean omitTracing)
{
m_level = level;
Value = val;
Kind = kind;
Raw = raw;
m_omitTracing = omitTracing;
if (!omitTracing)
{
TraceValue(context);
}
}
public ValueKind Kind { get; }
/// <summary>
/// When an interface converter is applied to the node result, raw contains the original value
/// </summary>
public Object Raw { get; }
public Object Value { get; }
public Boolean IsFalsy
{
get
{
switch (Kind)
{
case ValueKind.Null:
return true;
case ValueKind.Boolean:
var boolean = (Boolean)Value;
return !boolean;
case ValueKind.Number:
var number = (Double)Value;
return number == 0d || Double.IsNaN(number);
case ValueKind.String:
var str = (String)Value;
return String.Equals(str, String.Empty, StringComparison.Ordinal);
default:
return false;
}
}
}
public Boolean IsPrimitive => ExpressionUtility.IsPrimitive(Kind);
public Boolean IsTruthy => !IsFalsy;
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
public Boolean AbstractEqual(EvaluationResult right)
{
return AbstractEqual(Value, right.Value);
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
public Boolean AbstractGreaterThan(EvaluationResult right)
{
return AbstractGreaterThan(Value, right.Value);
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
public Boolean AbstractGreaterThanOrEqual(EvaluationResult right)
{
return AbstractEqual(Value, right.Value) || AbstractGreaterThan(Value, right.Value);
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
public Boolean AbstractLessThan(EvaluationResult right)
{
return AbstractLessThan(Value, right.Value);
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
public Boolean AbstractLessThanOrEqual(EvaluationResult right)
{
return AbstractEqual(Value, right.Value) || AbstractLessThan(Value, right.Value);
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
public Boolean AbstractNotEqual(EvaluationResult right)
{
return !AbstractEqual(Value, right.Value);
}
public Double ConvertToNumber()
{
return ConvertToNumber(Value);
}
public String ConvertToString()
{
switch (Kind)
{
case ValueKind.Null:
return String.Empty;
case ValueKind.Boolean:
return ((Boolean)Value) ? ExpressionConstants.True : ExpressionConstants.False;
case ValueKind.Number:
return ((Double)Value).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
case ValueKind.String:
return Value as String;
default:
return Kind.ToString();
}
}
public Boolean TryGetCollectionInterface(out Object collection)
{
if ((Kind == ValueKind.Object || Kind == ValueKind.Array))
{
var obj = Value;
if (obj is IReadOnlyObject)
{
collection = obj;
return true;
}
else if (obj is IReadOnlyArray)
{
collection = obj;
return true;
}
}
collection = null;
return false;
}
/// <summary>
/// Useful for working with values that are not the direct evaluation result of a parameter.
/// This allows ExpressionNode authors to leverage the coercion and comparision functions
/// for any values.
///
/// Also note, the value will be canonicalized (for example numeric types converted to double) and any
/// matching interfaces applied.
/// </summary>
public static EvaluationResult CreateIntermediateResult(
EvaluationContext context,
Object obj)
{
var val = ExpressionUtility.ConvertToCanonicalValue(obj, out ValueKind kind, out Object raw);
return new EvaluationResult(context, 0, val, kind, raw, omitTracing: true);
}
private void TraceValue(EvaluationContext context)
{
if (!m_omitTracing)
{
TraceValue(context, Value, Kind);
}
}
private void TraceValue(
EvaluationContext context,
Object val,
ValueKind kind)
{
if (!m_omitTracing)
{
TraceVerbose(context, String.Concat("=> ", ExpressionUtility.FormatValue(context?.SecretMasker, val, kind)));
}
}
private void TraceVerbose(
EvaluationContext context,
String message)
{
if (!m_omitTracing)
{
context?.Trace.Verbose(String.Empty.PadLeft(m_level * 2, '.') + (message ?? String.Empty));
}
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
private static Boolean AbstractEqual(
Object canonicalLeftValue,
Object canonicalRightValue)
{
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out var leftKind, out var rightKind);
// Same kind
if (leftKind == rightKind)
{
switch (leftKind)
{
// Null, Null
case ValueKind.Null:
return true;
// Number, Number
case ValueKind.Number:
var leftDouble = (Double)canonicalLeftValue;
var rightDouble = (Double)canonicalRightValue;
if (Double.IsNaN(leftDouble) || Double.IsNaN(rightDouble))
{
return false;
}
return leftDouble == rightDouble;
// String, String
case ValueKind.String:
var leftString = (String)canonicalLeftValue;
var rightString = (String)canonicalRightValue;
return String.Equals(leftString, rightString, StringComparison.OrdinalIgnoreCase);
// Boolean, Boolean
case ValueKind.Boolean:
var leftBoolean = (Boolean)canonicalLeftValue;
var rightBoolean = (Boolean)canonicalRightValue;
return leftBoolean == rightBoolean;
// Object, Object
case ValueKind.Object:
case ValueKind.Array:
return Object.ReferenceEquals(canonicalLeftValue, canonicalRightValue);
}
}
return false;
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
private static Boolean AbstractGreaterThan(
Object canonicalLeftValue,
Object canonicalRightValue)
{
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out var leftKind, out var rightKind);
// Same kind
if (leftKind == rightKind)
{
switch (leftKind)
{
// Number, Number
case ValueKind.Number:
var leftDouble = (Double)canonicalLeftValue;
var rightDouble = (Double)canonicalRightValue;
if (Double.IsNaN(leftDouble) || Double.IsNaN(rightDouble))
{
return false;
}
return leftDouble > rightDouble;
// String, String
case ValueKind.String:
var leftString = (String)canonicalLeftValue;
var rightString = (String)canonicalRightValue;
return String.Compare(leftString, rightString, StringComparison.OrdinalIgnoreCase) > 0;
// Boolean, Boolean
case ValueKind.Boolean:
var leftBoolean = (Boolean)canonicalLeftValue;
var rightBoolean = (Boolean)canonicalRightValue;
return leftBoolean && !rightBoolean;
}
}
return false;
}
/// <summary>
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
/// </summary>
private static Boolean AbstractLessThan(
Object canonicalLeftValue,
Object canonicalRightValue)
{
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out var leftKind, out var rightKind);
// Same kind
if (leftKind == rightKind)
{
switch (leftKind)
{
// Number, Number
case ValueKind.Number:
var leftDouble = (Double)canonicalLeftValue;
var rightDouble = (Double)canonicalRightValue;
if (Double.IsNaN(leftDouble) || Double.IsNaN(rightDouble))
{
return false;
}
return leftDouble < rightDouble;
// String, String
case ValueKind.String:
var leftString = (String)canonicalLeftValue;
var rightString = (String)canonicalRightValue;
return String.Compare(leftString, rightString, StringComparison.OrdinalIgnoreCase) < 0;
// Boolean, Boolean
case ValueKind.Boolean:
var leftBoolean = (Boolean)canonicalLeftValue;
var rightBoolean = (Boolean)canonicalRightValue;
return !leftBoolean && rightBoolean;
}
}
return false;
}
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
/// Except objects are not coerced to primitives.
private static void CoerceTypes(
ref Object canonicalLeftValue,
ref Object canonicalRightValue,
out ValueKind leftKind,
out ValueKind rightKind)
{
leftKind = GetKind(canonicalLeftValue);
rightKind = GetKind(canonicalRightValue);
// Same kind
if (leftKind == rightKind)
{
}
// Number, String
else if (leftKind == ValueKind.Number && rightKind == ValueKind.String)
{
canonicalRightValue = ConvertToNumber(canonicalRightValue);
rightKind = ValueKind.Number;
}
// String, Number
else if (leftKind == ValueKind.String && rightKind == ValueKind.Number)
{
canonicalLeftValue = ConvertToNumber(canonicalLeftValue);
leftKind = ValueKind.Number;
}
// Boolean|Null, Any
else if (leftKind == ValueKind.Boolean || leftKind == ValueKind.Null)
{
canonicalLeftValue = ConvertToNumber(canonicalLeftValue);
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out leftKind, out rightKind);
}
// Any, Boolean|Null
else if (rightKind == ValueKind.Boolean || rightKind == ValueKind.Null)
{
canonicalRightValue = ConvertToNumber(canonicalRightValue);
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out leftKind, out rightKind);
}
}
/// <summary>
/// For primitives, follows the Javascript rules (the Number function in Javascript). Otherwise NaN.
/// </summary>
private static Double ConvertToNumber(Object canonicalValue)
{
var kind = GetKind(canonicalValue);
switch (kind)
{
case ValueKind.Null:
return 0d;
case ValueKind.Boolean:
return (Boolean)canonicalValue ? 1d : 0d;
case ValueKind.Number:
return (Double)canonicalValue;
case ValueKind.String:
return ExpressionUtility.ParseNumber(canonicalValue as String);
}
return Double.NaN;
}
private static ValueKind GetKind(Object canonicalValue)
{
if (Object.ReferenceEquals(canonicalValue, null))
{
return ValueKind.Null;
}
else if (canonicalValue is Boolean)
{
return ValueKind.Boolean;
}
else if (canonicalValue is Double)
{
return ValueKind.Number;
}
else if (canonicalValue is String)
{
return ValueKind.String;
}
else if (canonicalValue is IReadOnlyObject)
{
return ValueKind.Object;
}
else if (canonicalValue is IReadOnlyArray)
{
return ValueKind.Array;
}
return ValueKind.Object;
}
private readonly Int32 m_level;
private readonly Boolean m_omitTracing;
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using GitHub.DistributedTask.Expressions2.Sdk;
using GitHub.DistributedTask.Expressions2.Sdk.Functions;
namespace GitHub.DistributedTask.Expressions2
{
internal static class ExpressionConstants
{
static ExpressionConstants()
{
AddFunction<Contains>("contains", 2, 2);
AddFunction<EndsWith>("endsWith", 2, 2);
AddFunction<Format>("format", 1, Byte.MaxValue);
AddFunction<Join>("join", 1, 2);
AddFunction<StartsWith>("startsWith", 2, 2);
AddFunction<ToJson>("toJson", 1, 1);
AddFunction<HashFiles>("hashFiles", 1, 1);
}
private static void AddFunction<T>(String name, Int32 minParameters, Int32 maxParameters)
where T : Function, new()
{
WellKnownFunctions.Add(name, new FunctionInfo<T>(name, minParameters, maxParameters));
}
internal static readonly String False = "false";
internal static readonly String Infinity = "Infinity";
internal static readonly Int32 MaxDepth = 50;
internal static readonly Int32 MaxLength = 21000; // Under 85,000 large object heap threshold, even if .NET switches to UTF-32
internal static readonly String NaN = "NaN";
internal static readonly String NegativeInfinity = "-Infinity";
internal static readonly String Null = "null";
internal static readonly String NumberFormat = "G15";
internal static readonly String True = "true";
internal static readonly Dictionary<String, IFunctionInfo> WellKnownFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
// Punctuation
internal const Char StartGroup = '('; // logical grouping
internal const Char StartIndex = '[';
internal const Char StartParameter = '('; // function call
internal const Char EndGroup = ')'; // logical grouping
internal const Char EndIndex = ']';
internal const Char EndParameter = ')'; // function calll
internal const Char Separator = ',';
internal const Char Dereference = '.';
internal const Char Wildcard = '*';
// Operators
internal const String Not = "!";
internal const String NotEqual = "!=";
internal const String GreaterThan = ">";
internal const String GreaterThanOrEqual = ">=";
internal const String LessThan = "<";
internal const String LessThanOrEqual = "<=";
internal const String Equal = "==";
internal const String And = "&&";
internal const String Or = "||";
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.ComponentModel;
using GitHub.DistributedTask.Logging;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class ExpressionException : Exception
{
internal ExpressionException(ISecretMasker secretMasker, String message)
{
if (secretMasker != null)
{
message = secretMasker.MaskSecrets(message);
}
m_message = message;
}
public override String Message => m_message;
private readonly String m_message;
}
}

View File

@@ -0,0 +1,471 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using GitHub.DistributedTask.Expressions2.Sdk.Operators;
using GitHub.DistributedTask.Expressions2.Tokens;
namespace GitHub.DistributedTask.Expressions2
{
using GitHub.DistributedTask.Expressions2.Sdk;
using GitHub.DistributedTask.Expressions2.Sdk.Functions;
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class ExpressionParser
{
public IExpressionNode CreateTree(
String expression,
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions)
{
var context = new ParseContext(expression, trace, namedValues, functions);
context.Trace.Info($"Parsing expression: <{expression}>");
return CreateTree(context);
}
public IExpressionNode ValidateSyntax(
String expression,
ITraceWriter trace)
{
var context = new ParseContext(expression, trace, namedValues: null, functions: null, allowUnknownKeywords: true);
context.Trace.Info($"Validating expression syntax: <{expression}>");
return CreateTree(context);
}
private static IExpressionNode CreateTree(ParseContext context)
{
// Push the tokens
while (context.LexicalAnalyzer.TryGetNextToken(ref context.Token))
{
// Unexpected
if (context.Token.Kind == TokenKind.Unexpected)
{
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
}
// Operator
else if (context.Token.IsOperator)
{
PushOperator(context);
}
// Operand
else
{
PushOperand(context);
}
context.LastToken = context.Token;
}
// No tokens
if (context.LastToken == null)
{
return null;
}
// Check unexpected end of expression
if (context.Operators.Count > 0)
{
var unexpectedLastToken = false;
switch (context.LastToken.Kind)
{
case TokenKind.EndGroup: // ")" logical grouping
case TokenKind.EndIndex: // "]"
case TokenKind.EndParameters: // ")" function call
// Legal
break;
case TokenKind.Function:
// Illegal
unexpectedLastToken = true;
break;
default:
unexpectedLastToken = context.LastToken.IsOperator;
break;
}
if (unexpectedLastToken || context.LexicalAnalyzer.UnclosedTokens.Any())
{
throw new ParseException(ParseExceptionKind.UnexpectedEndOfExpression, context.LastToken, context.Expression);
}
}
// Flush operators
while (context.Operators.Count > 0)
{
FlushTopOperator(context);
}
// Check max depth
var result = context.Operands.Single();
CheckMaxDepth(context, result);
return result;
}
private static void PushOperand(ParseContext context)
{
// Create the node
var node = default(ExpressionNode);
switch (context.Token.Kind)
{
// Function
case TokenKind.Function:
var function = context.Token.RawValue;
if (TryGetFunctionInfo(context, function, out var functionInfo))
{
node = functionInfo.CreateNode();
node.Name = function;
}
else if (context.AllowUnknownKeywords)
{
node = new NoOperation();
node.Name = function;
}
else
{
throw new ParseException(ParseExceptionKind.UnrecognizedFunction, context.Token, context.Expression);
}
break;
// Named-value
case TokenKind.NamedValue:
var name = context.Token.RawValue;
if (context.ExtensionNamedValues.TryGetValue(name, out var namedValueInfo))
{
node = namedValueInfo.CreateNode();
node.Name = name;
}
else if (context.AllowUnknownKeywords)
{
node = new NoOperationNamedValue();
node.Name = name;
}
else
{
throw new ParseException(ParseExceptionKind.UnrecognizedNamedValue, context.Token, context.Expression);
}
break;
// Otherwise simple
default:
node = context.Token.ToNode();
break;
}
// Push the operand
context.Operands.Push(node);
}
private static void PushOperator(ParseContext context)
{
// Flush higher or equal precedence
if (context.Token.Associativity == Associativity.LeftToRight)
{
var precedence = context.Token.Precedence;
while (context.Operators.Count > 0)
{
var topOperator = context.Operators.Peek();
if (precedence <= topOperator.Precedence &&
topOperator.Kind != TokenKind.StartGroup && // Unless top is "(" logical grouping
topOperator.Kind != TokenKind.StartIndex && // or unless top is "["
topOperator.Kind != TokenKind.StartParameters &&// or unless top is "(" function call
topOperator.Kind != TokenKind.Separator) // or unless top is ","
{
FlushTopOperator(context);
continue;
}
break;
}
}
// Push the operator
context.Operators.Push(context.Token);
// Process closing operators now, since context.LastToken is required
// to accurately process TokenKind.EndParameters
switch (context.Token.Kind)
{
case TokenKind.EndGroup: // ")" logical grouping
case TokenKind.EndIndex: // "]"
case TokenKind.EndParameters: // ")" function call
FlushTopOperator(context);
break;
}
}
private static void FlushTopOperator(ParseContext context)
{
// Special handling for closing operators
switch (context.Operators.Peek().Kind)
{
case TokenKind.EndIndex: // "]"
FlushTopEndIndex(context);
return;
case TokenKind.EndGroup: // ")" logical grouping
FlushTopEndGroup(context);
return;
case TokenKind.EndParameters: // ")" function call
FlushTopEndParameters(context);
return;
}
// Pop the operator
var @operator = context.Operators.Pop();
// Create the node
var node = (Container)@operator.ToNode();
// Pop the operands, add to the node
var operands = PopOperands(context, @operator.OperandCount);
foreach (var operand in operands)
{
// Flatten nested And
if (node is And)
{
if (operand is And nestedAnd)
{
foreach (var nestedParameter in nestedAnd.Parameters)
{
node.AddParameter(nestedParameter);
}
continue;
}
}
// Flatten nested Or
else if (node is Or)
{
if (operand is Or nestedOr)
{
foreach (var nestedParameter in nestedOr.Parameters)
{
node.AddParameter(nestedParameter);
}
continue;
}
}
node.AddParameter(operand);
}
// Push the node to the operand stack
context.Operands.Push(node);
}
/// <summary>
/// Flushes the ")" logical grouping operator
/// </summary>
private static void FlushTopEndGroup(ParseContext context)
{
// Pop the operators
PopOperator(context, TokenKind.EndGroup); // ")" logical grouping
PopOperator(context, TokenKind.StartGroup); // "(" logical grouping
}
/// <summary>
/// Flushes the "]" operator
/// </summary>
private static void FlushTopEndIndex(ParseContext context)
{
// Pop the operators
PopOperator(context, TokenKind.EndIndex); // "]"
var @operator = PopOperator(context, TokenKind.StartIndex); // "["
// Create the node
var node = (Container)@operator.ToNode();
// Pop the operands, add to the node
var operands = PopOperands(context, @operator.OperandCount);
foreach (var operand in operands)
{
node.AddParameter(operand);
}
// Push the node to the operand stack
context.Operands.Push(node);
}
// ")" function call
private static void FlushTopEndParameters(ParseContext context)
{
// Pop the operator
var @operator = PopOperator(context, TokenKind.EndParameters); // ")" function call
// Sanity check top operator is the current token
if (!Object.ReferenceEquals(@operator, context.Token))
{
throw new InvalidOperationException("Expected the operator to be the current token");
}
var function = default(Function);
// No parameters
if (context.LastToken.Kind == TokenKind.StartParameters)
{
// Node already exists on the operand stack
function = (Function)context.Operands.Peek();
}
// Has parameters
else
{
// Pop the operands
var parameterCount = 1;
while (context.Operators.Peek().Kind == TokenKind.Separator)
{
parameterCount++;
context.Operators.Pop();
}
var functionOperands = PopOperands(context, parameterCount);
// Node already exists on the operand stack
function = (Function)context.Operands.Peek();
// Add the operands to the node
foreach (var operand in functionOperands)
{
function.AddParameter(operand);
}
}
// Pop the "(" operator too
@operator = PopOperator(context, TokenKind.StartParameters);
// Check min/max parameter count
TryGetFunctionInfo(context, function.Name, out var functionInfo);
if (functionInfo == null && context.AllowUnknownKeywords)
{
// Don't check min/max
}
else if (function.Parameters.Count < functionInfo.MinParameters)
{
throw new ParseException(ParseExceptionKind.TooFewParameters, token: @operator, expression: context.Expression);
}
else if (function.Parameters.Count > functionInfo.MaxParameters)
{
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
}
}
/// <summary>
/// Pops N operands from the operand stack. The operands are returned
/// in their natural listed order, i.e. not last-in-first-out.
/// </summary>
private static List<ExpressionNode> PopOperands(
ParseContext context,
Int32 count)
{
var result = new List<ExpressionNode>();
while (count-- > 0)
{
result.Add(context.Operands.Pop());
}
result.Reverse();
return result;
}
/// <summary>
/// Pops an operator and asserts it is the expected kind.
/// </summary>
private static Token PopOperator(
ParseContext context,
TokenKind expected)
{
var token = context.Operators.Pop();
if (token.Kind != expected)
{
throw new NotSupportedException($"Expected operator '{expected}' to be popped. Actual '{token.Kind}'.");
}
return token;
}
/// <summary>
/// Checks the max depth of the expression tree
/// </summary>
private static void CheckMaxDepth(
ParseContext context,
ExpressionNode node,
Int32 depth = 1)
{
if (depth > ExpressionConstants.MaxDepth)
{
throw new ParseException(ParseExceptionKind.ExceededMaxDepth, token: null, expression: context.Expression);
}
if (node is Container container)
{
foreach (var parameter in container.Parameters)
{
CheckMaxDepth(context, parameter, depth + 1);
}
}
}
private static Boolean TryGetFunctionInfo(
ParseContext context,
String name,
out IFunctionInfo functionInfo)
{
return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
context.ExtensionFunctions.TryGetValue(name, out functionInfo);
}
private sealed class ParseContext
{
public Boolean AllowUnknownKeywords;
public readonly String Expression;
public readonly Dictionary<String, IFunctionInfo> ExtensionFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
public readonly Dictionary<String, INamedValueInfo> ExtensionNamedValues = new Dictionary<String, INamedValueInfo>(StringComparer.OrdinalIgnoreCase);
public readonly LexicalAnalyzer LexicalAnalyzer;
public readonly Stack<ExpressionNode> Operands = new Stack<ExpressionNode>();
public readonly Stack<Token> Operators = new Stack<Token>();
public readonly ITraceWriter Trace;
public Token Token;
public Token LastToken;
public ParseContext(
String expression,
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false)
{
Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength)
{
throw new ParseException(ParseExceptionKind.ExceededMaxLength, token: null, expression: Expression);
}
Trace = trace ?? new NoOperationTraceWriter();
foreach (var namedValueInfo in (namedValues ?? new INamedValueInfo[0]))
{
ExtensionNamedValues.Add(namedValueInfo.Name, namedValueInfo);
}
foreach (var functionInfo in (functions ?? new IFunctionInfo[0]))
{
ExtensionFunctions.Add(functionInfo.Name, functionInfo);
}
LexicalAnalyzer = new LexicalAnalyzer(Expression);
AllowUnknownKeywords = allowUnknownKeywords;
}
private class NoOperationTraceWriter : ITraceWriter
{
public void Info(String message)
{
}
public void Verbose(String message)
{
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.ComponentModel;
using GitHub.DistributedTask.Expressions2.Sdk;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class FunctionInfo<T> : IFunctionInfo
where T : Function, new()
{
public FunctionInfo(String name, Int32 minParameters, Int32 maxParameters)
{
Name = name;
MinParameters = minParameters;
MaxParameters = maxParameters;
}
public String Name { get; }
public Int32 MinParameters { get; }
public Int32 MaxParameters { get; }
public Function CreateNode()
{
return new T();
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using GitHub.DistributedTask.Logging;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IExpressionNode
{
/// <summary>
/// Evaluates the expression and returns the result, wrapped in a helper
/// for converting, comparing, and traversing objects.
/// </summary>
/// <param name="trace">Optional trace writer</param>
/// <param name="secretMasker">Optional secret masker</param>
/// <param name="state">State object for custom evaluation function nodes and custom named-value nodes</param>
/// <param name="options">Evaluation options</param>
EvaluationResult Evaluate(
ITraceWriter trace,
ISecretMasker secretMasker,
Object state,
EvaluationOptions options);
}
}

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.DistributedTask.Logging;
using GitHub.DistributedTask.Expressions2.Sdk;
using GitHub.DistributedTask.Expressions2.Sdk.Operators;
namespace GitHub.DistributedTask.Expressions2
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static class IExpressionNodeExtensions
{
/// <summary>
/// Returns the node and all descendant nodes
/// </summary>
public static IEnumerable<IExpressionNode> Traverse(this IExpressionNode node)
{
yield return node;
if (node is Container container && container.Parameters.Count > 0)
{
foreach (var parameter in container.Parameters)
{
foreach (var descendant in parameter.Traverse())
{
yield return descendant;
}
}
}
}
/// <summary>
/// Checks whether specific contexts or sub-properties of contexts are referenced.
/// If a conclusive determination cannot be made, then the pattern is considered matched.
/// For example, the expression "toJson(github)" matches the pattern "github.event" because
/// the value is passed to a function. Not enough information is known to determine whether
/// the function requires the sub-property. Therefore, it is assumed that it may.
///
/// Wildcards are supported in the pattern, and are treated as matching any literal.
/// For example, the expression "needs.my-job.outputs.my-output" matches the pattern "needs.*.outputs".
/// </summary>
public static Boolean[] CheckReferencesContext(
this IExpressionNode tree,
params String[] patterns)
{
var result = new Boolean[patterns.Length];
var segmentedPatterns = default(Stack<IExpressionNode>[]);
// Walk the tree
var stack = new Stack<IExpressionNode>();
stack.Push(tree);
while (stack.Count > 0)
{
var node = stack.Pop();
// Attempt to match a named-value or index operator.
// Note, do not push children of the index operator.
if (node is NamedValue || node is Index)
{
// Lazy initialize the pattern segments
if (segmentedPatterns is null)
{
segmentedPatterns = new Stack<IExpressionNode>[patterns.Length];
var parser = new ExpressionParser();
for (var i = 0; i < patterns.Length; i++)
{
var pattern = patterns[i];
var patternTree = parser.ValidateSyntax(pattern, null);
var patternSegments = GetMatchSegments(patternTree);
if (patternSegments.Count == 0)
{
throw new InvalidOperationException($"Invalid context-match-pattern '{pattern}'");
}
segmentedPatterns[i] = patternSegments;
}
}
// Match
Match(node, segmentedPatterns, result);
}
// Push children of any other container node.
else if (node is Container container && container.Parameters.Count > 0)
{
foreach (var child in container.Parameters)
{
stack.Push(child);
}
}
}
return result;
}
private static void Match(
IExpressionNode node,
Stack<IExpressionNode>[] patterns,
Boolean[] result)
{
var nodeSegments = GetMatchSegments(node);
if (nodeSegments.Count == 0)
{
return;
}
var nodeNamedValue = nodeSegments.Peek() as NamedValue;
var originalNodeSegments = nodeSegments;
for (var i = 0; i < patterns.Length; i++)
{
var patternSegments = patterns[i];
var patternNamedValue = patternSegments.Peek() as NamedValue;
// Compare the named-value
if (String.Equals(nodeNamedValue.Name, patternNamedValue.Name, StringComparison.OrdinalIgnoreCase))
{
// Clone the stacks before mutating
nodeSegments = new Stack<IExpressionNode>(originalNodeSegments.Reverse());
nodeSegments.Pop();
patternSegments = new Stack<IExpressionNode>(patternSegments.Reverse());
patternSegments.Pop();
// Walk the stacks
while (true)
{
// Every pattern segment was matched
if (patternSegments.Count == 0)
{
result[i] = true;
break;
}
// Every node segment was matched. Treat the pattern as matched. There is not
// enough information to determine whether the property is required; assume it is.
// For example, consider the pattern "github.event" and the expression "toJson(github)".
// In this example the function requires the full structure of the named-value.
else if (nodeSegments.Count == 0)
{
result[i] = true;
break;
}
var nodeSegment = nodeSegments.Pop();
var patternSegment = patternSegments.Pop();
// The behavior of a wildcard varies depending on whether the left operand
// is an array or an object. For simplicity, treat the pattern as matched.
if (nodeSegment is Wildcard)
{
result[i] = true;
break;
}
// Treat a wildcard pattern segment as matching any literal segment
else if (patternSegment is Wildcard)
{
continue;
}
// Convert literals to string and compare
var nodeLiteral = nodeSegment as Literal;
var nodeEvaluationResult = EvaluationResult.CreateIntermediateResult(null, nodeLiteral.Value);
var nodeString = nodeEvaluationResult.ConvertToString();
var patternLiteral = patternSegment as Literal;
var patternEvaluationResult = EvaluationResult.CreateIntermediateResult(null, patternLiteral.Value);
var patternString = patternEvaluationResult.ConvertToString();
if (String.Equals(nodeString, patternString, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// Convert to number and compare
var nodeNumber = nodeEvaluationResult.ConvertToNumber();
if (!Double.IsNaN(nodeNumber) && nodeNumber >= 0d && nodeNumber <= (Double)Int32.MaxValue)
{
var patternNumber = patternEvaluationResult.ConvertToNumber();
if (!Double.IsNaN(patternNumber) && patternNumber >= 0 && patternNumber <= (Double)Int32.MaxValue)
{
nodeNumber = Math.Floor(nodeNumber);
patternNumber = Math.Floor(patternNumber);
if (nodeNumber == patternNumber)
{
continue;
}
}
}
// Not matched
break;
}
}
}
}
private static Stack<IExpressionNode> GetMatchSegments(IExpressionNode node)
{
var result = new Stack<IExpressionNode>();
// Node is a named-value
if (node is NamedValue)
{
result.Push(node);
}
// Node is an index
else if (node is Index index)
{
while (true)
{
// Push parameter 1. Treat anything other than literal as a wildcard.
var parameter1 = index.Parameters[1];
result.Push(parameter1 is Literal ? parameter1 : new Wildcard());
var parameter0 = index.Parameters[0];
// Parameter 0 is a named-value
if (parameter0 is NamedValue)
{
result.Push(parameter0);
break;
}
// Parameter 0 is an index
else if (parameter0 is Index index2)
{
index = index2;
}
// Otherwise clear
else
{
result.Clear();
break;
}
}
}
return result;
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.ComponentModel;
using GitHub.DistributedTask.Expressions2.Sdk;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IFunctionInfo
{
String Name { get; }
Int32 MinParameters { get; }
Int32 MaxParameters { get; }
Function CreateNode();
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.ComponentModel;
using GitHub.DistributedTask.Expressions2.Sdk;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface INamedValueInfo
{
String Name { get; }
NamedValue CreateNode();
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface ITraceWriter
{
void Info(String message);
void Verbose(String message);
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.ComponentModel;
using GitHub.DistributedTask.Expressions2.Sdk;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class NamedValueInfo<T> : INamedValueInfo
where T : NamedValue, new()
{
public NamedValueInfo(String name)
{
Name = name;
}
public String Name { get; }
public NamedValue CreateNode()
{
return new T();
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.ComponentModel;
using GitHub.DistributedTask.Expressions2.Tokens;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class ParseException : ExpressionException
{
internal ParseException(ParseExceptionKind kind, Token token, String expression)
: base(secretMasker: null, message: String.Empty)
{
Expression = expression;
Kind = kind;
RawToken = token?.RawValue;
TokenIndex = token?.Index ?? 0;
String description;
switch (kind)
{
case ParseExceptionKind.ExceededMaxDepth:
description = $"Exceeded max expression depth {ExpressionConstants.MaxDepth}";
break;
case ParseExceptionKind.ExceededMaxLength:
description = $"Exceeded max expression length {ExpressionConstants.MaxLength}";
break;
case ParseExceptionKind.TooFewParameters:
description = "Too few parameters supplied";
break;
case ParseExceptionKind.TooManyParameters:
description = "Too many parameters supplied";
break;
case ParseExceptionKind.UnexpectedEndOfExpression:
description = "Unexpected end of expression";
break;
case ParseExceptionKind.UnexpectedSymbol:
description = "Unexpected symbol";
break;
case ParseExceptionKind.UnrecognizedFunction:
description = "Unrecognized function";
break;
case ParseExceptionKind.UnrecognizedNamedValue:
description = "Unrecognized named-value";
break;
default: // Should never reach here.
throw new Exception($"Unexpected parse exception kind '{kind}'.");
}
if (token == null)
{
Message = description;
}
else
{
Message = $"{description}: '{RawToken}'. Located at position {TokenIndex + 1} within expression: {Expression}";
}
}
internal String Expression { get; }
internal ParseExceptionKind Kind { get; }
internal String RawToken { get; }
internal Int32 TokenIndex { get; }
public sealed override String Message { get; }
}
}

View File

@@ -0,0 +1,14 @@
namespace GitHub.DistributedTask.Expressions2
{
internal enum ParseExceptionKind
{
ExceededMaxDepth,
ExceededMaxLength,
TooFewParameters,
TooManyParameters,
UnexpectedEndOfExpression,
UnexpectedSymbol,
UnrecognizedFunction,
UnrecognizedNamedValue,
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class Container : ExpressionNode
{
public IReadOnlyList<ExpressionNode> Parameters => m_parameters.AsReadOnly();
public void AddParameter(ExpressionNode node)
{
m_parameters.Add(node);
node.Container = this;
}
private readonly List<ExpressionNode> m_parameters = new List<ExpressionNode>();
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using GitHub.DistributedTask.Logging;
using GitHub.Services.Common;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class EvaluationContext
{
internal EvaluationContext(
ITraceWriter trace,
ISecretMasker secretMasker,
Object state,
EvaluationOptions options,
ExpressionNode node)
{
ArgumentUtility.CheckForNull(trace, nameof(trace));
ArgumentUtility.CheckForNull(secretMasker, nameof(secretMasker));
Trace = trace;
SecretMasker = secretMasker;
State = state;
// Copy the options
options = new EvaluationOptions(copy: options);
if (options.MaxMemory == 0)
{
// Set a reasonable default max memory
options.MaxMemory = 1048576; // 1 mb
}
Options = options;
Memory = new EvaluationMemory(options.MaxMemory, node);
m_traceResults = new Dictionary<ExpressionNode, String>();
m_traceMemory = new MemoryCounter(null, options.MaxMemory);
}
public ITraceWriter Trace { get; }
public ISecretMasker SecretMasker { get; }
public Object State { get; }
internal EvaluationMemory Memory { get; }
internal EvaluationOptions Options { get; }
internal void SetTraceResult(
ExpressionNode node,
EvaluationResult result)
{
// Remove if previously added. This typically should not happen. This could happen
// due to a badly authored function. So we'll handle it and track memory correctly.
if (m_traceResults.TryGetValue(node, out String oldValue))
{
m_traceMemory.Remove(oldValue);
m_traceResults.Remove(node);
}
// Check max memory
String value = ExpressionUtility.FormatValue(SecretMasker, result);
if (m_traceMemory.TryAdd(value))
{
// Store the result
m_traceResults[node] = value;
}
}
internal Boolean TryGetTraceResult(ExpressionNode node, out String value)
{
return m_traceResults.TryGetValue(node, out value);
}
private readonly Dictionary<ExpressionNode, String> m_traceResults = new Dictionary<ExpressionNode, String>();
private readonly MemoryCounter m_traceMemory;
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using ExpressionResources = GitHub.DistributedTask.Expressions.ExpressionResources;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
/// <summary>
/// This is an internal class only.
///
/// This class is used to track current memory consumption
/// across the entire expression evaluation.
/// </summary>
internal sealed class EvaluationMemory
{
internal EvaluationMemory(
Int32 maxBytes,
ExpressionNode node)
{
m_maxAmount = maxBytes;
m_node = node;
}
internal void AddAmount(
Int32 depth,
Int32 bytes,
Boolean trimDepth = false)
{
// Trim deeper depths
if (trimDepth)
{
while (m_maxActiveDepth > depth)
{
var amount = m_depths[m_maxActiveDepth];
if (amount > 0)
{
// Sanity check
if (amount > m_totalAmount)
{
throw new InvalidOperationException("Bytes to subtract exceeds total bytes");
}
// Subtract from the total
checked
{
m_totalAmount -= amount;
}
// Reset the amount
m_depths[m_maxActiveDepth] = 0;
}
m_maxActiveDepth--;
}
}
// Grow the depths
if (depth > m_maxActiveDepth)
{
// Grow the list
while (m_depths.Count <= depth)
{
m_depths.Add(0);
}
// Adjust the max active depth
m_maxActiveDepth = depth;
}
checked
{
// Add to the depth
m_depths[depth] += bytes;
// Add to the total
m_totalAmount += bytes;
}
// Check max
if (m_totalAmount > m_maxAmount)
{
throw new InvalidOperationException(ExpressionResources.ExceededAllowedMemory(m_node?.ConvertToExpression()));
}
}
internal static Int32 CalculateBytes(Object obj)
{
if (obj is String str)
{
// This measurement doesn't have to be perfect
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
checked
{
return c_stringBaseOverhead + ((str?.Length ?? 0) * sizeof(Char));
}
}
else
{
return c_minObjectSize;
}
}
private const Int32 c_minObjectSize = 24;
private const Int32 c_stringBaseOverhead = 26;
private readonly List<Int32> m_depths = new List<Int32>();
private readonly Int32 m_maxAmount;
private readonly ExpressionNode m_node;
private Int32 m_maxActiveDepth = -1;
private Int32 m_totalAmount;
}
}

View File

@@ -0,0 +1,37 @@
using System;
using GitHub.DistributedTask.Logging;
using GitHub.Services.Common;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
internal sealed class EvaluationTraceWriter : ITraceWriter
{
public EvaluationTraceWriter(ITraceWriter trace, ISecretMasker secretMasker)
{
ArgumentUtility.CheckForNull(secretMasker, nameof(secretMasker));
m_trace = trace;
m_secretMasker = secretMasker;
}
public void Info(String message)
{
if (m_trace != null)
{
message = m_secretMasker.MaskSecrets(message);
m_trace.Info(message);
}
}
public void Verbose(String message)
{
if (m_trace != null)
{
message = m_secretMasker.MaskSecrets(message);
m_trace.Verbose(message);
}
}
private readonly ISecretMasker m_secretMasker;
private readonly ITraceWriter m_trace;
}
}

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using GitHub.DistributedTask.Logging;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class ExpressionNode : IExpressionNode
{
internal Container Container { get; set; }
internal Int32 Level { get; private set; }
/// <summary>
/// The name is used for tracing. Normally the parser will set the name. However if a node
/// is added manually, then the name may not be set and will fallback to the type name.
/// </summary>
protected internal String Name
{
get
{
return !String.IsNullOrEmpty(m_name) ? m_name : this.GetType().Name;
}
set
{
m_name = value;
}
}
/// <summary>
/// Indicates whether the evalation result should be stored on the context and used
/// when the realized result is traced.
/// </summary>
protected abstract Boolean TraceFullyRealized { get; }
/// <summary>
/// IExpressionNode entry point.
/// </summary>
EvaluationResult IExpressionNode.Evaluate(
ITraceWriter trace,
ISecretMasker secretMasker,
Object state,
EvaluationOptions options)
{
if (Container != null)
{
// Do not localize. This is an SDK consumer error.
throw new NotSupportedException($"Expected {nameof(IExpressionNode)}.{nameof(Evaluate)} to be called on root node only.");
}
var originalSecretMasker = secretMasker;
try
{
// Evaluate
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
trace = new EvaluationTraceWriter(trace, secretMasker);
var context = new EvaluationContext(trace, secretMasker, state, options, this);
trace.Info($"Evaluating: {ConvertToExpression()}");
var result = Evaluate(context);
// Trace the result
TraceTreeResult(context, result.Value, result.Kind);
return result;
}
finally
{
if (secretMasker != null && secretMasker != originalSecretMasker)
{
(secretMasker as IDisposable)?.Dispose();
secretMasker = null;
}
}
}
/// <summary>
/// This function is intended only for ExpressionNode authors to call. The EvaluationContext
/// caches result-state specific to the evaluation instance.
/// </summary>
public EvaluationResult Evaluate(EvaluationContext context)
{
// Evaluate
Level = Container == null ? 0 : Container.Level + 1;
TraceVerbose(context, Level, $"Evaluating {Name}:");
var coreResult = EvaluateCore(context, out ResultMemory coreMemory);
if (coreMemory == null)
{
coreMemory = new ResultMemory();
}
// Convert to canonical value
var val = ExpressionUtility.ConvertToCanonicalValue(coreResult, out ValueKind kind, out Object raw);
// The depth can be safely trimmed when the total size of the core result is known,
// or when the total size of the core result can easily be determined.
var trimDepth = coreMemory.IsTotal || (Object.ReferenceEquals(raw, null) && ExpressionUtility.IsPrimitive(kind));
// Account for the memory overhead of the core result
var coreBytes = coreMemory.Bytes ?? EvaluationMemory.CalculateBytes(raw ?? val);
context.Memory.AddAmount(Level, coreBytes, trimDepth);
// Account for the memory overhead of the conversion result
if (!Object.ReferenceEquals(raw, null))
{
var conversionBytes = EvaluationMemory.CalculateBytes(val);
context.Memory.AddAmount(Level, conversionBytes);
}
var result = new EvaluationResult(context, Level, val, kind, raw);
// Store the trace result
if (this.TraceFullyRealized)
{
context.SetTraceResult(this, result);
}
return result;
}
internal abstract String ConvertToExpression();
internal abstract String ConvertToRealizedExpression(EvaluationContext context);
/// <summary>
/// Evaluates the node
/// </summary>
/// <param name="context">The current expression context</param>
/// <param name="resultMemory">
/// Helps determine how much memory is being consumed across the evaluation of the expression.
/// </param>
protected abstract Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory);
protected MemoryCounter CreateMemoryCounter(EvaluationContext context)
{
return new MemoryCounter(this, context.Options.MaxMemory);
}
private void TraceTreeResult(
EvaluationContext context,
Object result,
ValueKind kind)
{
// Get the realized expression
String realizedExpression = ConvertToRealizedExpression(context);
// Format the result
String traceValue = ExpressionUtility.FormatValue(context.SecretMasker, result, kind);
// Only trace the realized expression if it is meaningfully different
if (!String.Equals(realizedExpression, traceValue, StringComparison.Ordinal))
{
if (kind == ValueKind.Number &&
String.Equals(realizedExpression, $"'{traceValue}'", StringComparison.Ordinal))
{
// Don't bother tracing the realized expression when the result is a number and the
// realized expresion is a precisely matching string.
}
else
{
context.Trace.Info($"Expanded: {realizedExpression}");
}
}
// Always trace the result
context.Trace.Info($"Result: {traceValue}");
}
private static void TraceVerbose(
EvaluationContext context,
Int32 level,
String message)
{
context.Trace.Verbose(String.Empty.PadLeft(level * 2, '.') + (message ?? String.Empty));
}
private static readonly ValueKind[] s_simpleKinds = new[]
{
ValueKind.Boolean,
ValueKind.Null,
ValueKind.Number,
ValueKind.String,
};
private String m_name;
}
}

View File

@@ -0,0 +1,265 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using GitHub.DistributedTask.Logging;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class ExpressionUtility
{
internal static Object ConvertToCanonicalValue(
Object val,
out ValueKind kind,
out Object raw)
{
raw = null;
if (Object.ReferenceEquals(val, null))
{
kind = ValueKind.Null;
return null;
}
else if (val is Boolean)
{
kind = ValueKind.Boolean;
return val;
}
else if (val is Double)
{
kind = ValueKind.Number;
return val;
}
else if (val is String)
{
kind = ValueKind.String;
return val;
}
else if (val is INull n)
{
kind = ValueKind.Null;
raw = val;
return null;
}
else if (val is IBoolean boolean)
{
kind = ValueKind.Boolean;
raw = val;
return boolean.GetBoolean();
}
else if (val is INumber number)
{
kind = ValueKind.Number;
raw = val;
return number.GetNumber();
}
else if (val is IString str)
{
kind = ValueKind.String;
raw = val;
return str.GetString();
}
else if (val is IReadOnlyObject)
{
kind = ValueKind.Object;
return val;
}
else if (val is IReadOnlyArray)
{
kind = ValueKind.Array;
return val;
}
else if (!val.GetType().GetTypeInfo().IsClass)
{
if (val is Decimal || val is Byte || val is SByte || val is Int16 || val is UInt16 || val is Int32 || val is UInt32 || val is Int64 || val is UInt64 || val is Single)
{
kind = ValueKind.Number;
return Convert.ToDouble(val);
}
else if (val is Enum)
{
var strVal = String.Format(CultureInfo.InvariantCulture, "{0:G}", val);
if (Double.TryParse(strVal, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out Double doubleValue))
{
kind = ValueKind.Number;
return doubleValue;
}
kind = ValueKind.String;
return strVal;
}
}
kind = ValueKind.Object;
return val;
}
internal static String FormatValue(
ISecretMasker secretMasker,
EvaluationResult evaluationResult)
{
return FormatValue(secretMasker, evaluationResult.Value, evaluationResult.Kind);
}
internal static String FormatValue(
ISecretMasker secretMasker,
Object value,
ValueKind kind)
{
switch (kind)
{
case ValueKind.Null:
return ExpressionConstants.Null;
case ValueKind.Boolean:
return ((Boolean)value) ? ExpressionConstants.True : ExpressionConstants.False;
case ValueKind.Number:
var strNumber = ((Double)value).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
return secretMasker != null ? secretMasker.MaskSecrets(strNumber) : strNumber;
case ValueKind.String:
// Mask secrets before string-escaping.
var strValue = secretMasker != null ? secretMasker.MaskSecrets(value as String) : value as String;
return $"'{StringEscape(strValue)}'";
case ValueKind.Array:
case ValueKind.Object:
return kind.ToString();
default: // Should never reach here.
throw new NotSupportedException($"Unable to convert to realized expression. Unexpected value kind: {kind}");
}
}
internal static bool IsLegalKeyword(String str)
{
if (String.IsNullOrEmpty(str))
{
return false;
}
var first = str[0];
if ((first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
first == '_')
{
for (var i = 1; i < str.Length; i++)
{
var c = str[i];
if ((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '-')
{
// OK
}
else
{
return false;
}
}
return true;
}
else
{
return false;
}
}
internal static Boolean IsPrimitive(ValueKind kind)
{
switch (kind)
{
case ValueKind.Null:
case ValueKind.Boolean:
case ValueKind.Number:
case ValueKind.String:
return true;
default:
return false;
}
}
/// <summary>
/// The rules here attempt to follow Javascript rules for coercing a string into a number
/// for comparison. That is, the Number() function in Javascript.
/// </summary>
internal static Double ParseNumber(String str)
{
// Trim
str = str?.Trim() ?? String.Empty;
// Empty
if (String.IsNullOrEmpty(str))
{
return 0d;
}
// Try parse
else if (Double.TryParse(str, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var value))
{
return value;
}
// Check for 0x[0-9a-fA-F]+
else if (str[0] == '0' &&
str.Length > 2 &&
str[1] == 'x' &&
str.Skip(2).All(x => (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F')))
{
// Try parse
if (Int32.TryParse(str.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var integer))
{
return (Double)integer;
}
// Otherwise exceeds range
}
// Check for 0o[0-9]+
else if (str[0] == '0' &&
str.Length > 2 &&
str[1] == 'o' &&
str.Skip(2).All(x => x >= '0' && x <= '7'))
{
// Try parse
var integer = default(Int32?);
try
{
integer = Convert.ToInt32(str.Substring(2), 8);
}
// Otherwise exceeds range
catch (Exception)
{
}
// Success
if (integer != null)
{
return (Double)integer.Value;
}
}
// Infinity
else if (String.Equals(str, ExpressionConstants.Infinity, StringComparison.Ordinal))
{
return Double.PositiveInfinity;
}
// -Infinity
else if (String.Equals(str, ExpressionConstants.NegativeInfinity, StringComparison.Ordinal))
{
return Double.NegativeInfinity;
}
// Otherwise NaN
return Double.NaN;
}
internal static String StringEscape(String value)
{
return String.IsNullOrEmpty(value) ? String.Empty : value.Replace("'", "''");
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class Function : Container
{
/// <summary>
/// Generally this should not be overridden. True indicates the result of the node is traced as part of the "expanded"
/// (i.e. "realized") trace information. Otherwise the node expression is printed, and parameters to the node may or
/// may not be fully realized - depending on each respective parameter's trace-fully-realized setting.
///
/// The purpose is so the end user can understand how their expression expanded at run time. For example, consider
/// the expression: eq(variables.publish, 'true'). The runtime-expanded expression may be: eq('true', 'true')
/// </summary>
protected override Boolean TraceFullyRealized => true;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"{0}({1})",
Name,
String.Join(", ", Parameters.Select(x => x.ConvertToExpression())));
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"{0}({1})",
Name,
String.Join(", ", Parameters.Select(x => x.ConvertToRealizedExpression(context))));
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class Contains : Function
{
protected sealed override Boolean TraceFullyRealized => false;
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
if (left.IsPrimitive)
{
var leftString = left.ConvertToString();
var right = Parameters[1].Evaluate(context);
if (right.IsPrimitive)
{
var rightString = right.ConvertToString();
return leftString.IndexOf(rightString, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
else if (left.TryGetCollectionInterface(out var collection) &&
collection is IReadOnlyArray array &&
array.Count > 0)
{
var right = Parameters[1].Evaluate(context);
foreach (var item in array)
{
var itemResult = EvaluationResult.CreateIntermediateResult(context, item);
if (right.AbstractEqual(itemResult))
{
return true;
}
}
}
return false;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class EndsWith : Function
{
protected sealed override Boolean TraceFullyRealized => false;
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
if (left.IsPrimitive)
{
var leftString = left.ConvertToString();
var right = Parameters[1].Evaluate(context);
if (right.IsPrimitive)
{
var rightString = right.ConvertToString();
return leftString.EndsWith(rightString, StringComparison.OrdinalIgnoreCase);
}
}
return false;
}
}
}

View File

@@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using ExpressionResources = GitHub.DistributedTask.Expressions.ExpressionResources;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class Format : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var format = Parameters[0].Evaluate(context).ConvertToString();
var index = 0;
var result = new FormatResultBuilder(this, context, CreateMemoryCounter(context));
while (index < format.Length)
{
var lbrace = format.IndexOf('{', index);
var rbrace = format.IndexOf('}', index);
// Left brace
if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace))
{
// Escaped left brace
if (SafeCharAt(format, lbrace + 1) == '{')
{
result.Append(format.Substring(index, lbrace - index + 1));
index = lbrace + 2;
}
// Left brace, number, optional format specifiers, right brace
else if (rbrace > lbrace + 1 &&
ReadArgIndex(format, lbrace + 1, out Byte argIndex, out Int32 endArgIndex) &&
ReadFormatSpecifiers(format, endArgIndex + 1, out String formatSpecifiers, out rbrace))
{
// Check parameter count
if (argIndex > Parameters.Count - 2)
{
throw new FormatException(ExpressionResources.InvalidFormatArgIndex(format));
}
// Append the portion before the left brace
if (lbrace > index)
{
result.Append(format.Substring(index, lbrace - index));
}
// Append the arg
result.Append(argIndex, formatSpecifiers);
index = rbrace + 1;
}
else
{
throw new FormatException(ExpressionResources.InvalidFormatString(format));
}
}
// Right brace
else if (rbrace >= 0)
{
// Escaped right brace
if (SafeCharAt(format, rbrace + 1) == '}')
{
result.Append(format.Substring(index, rbrace - index + 1));
index = rbrace + 2;
}
else
{
throw new FormatException(ExpressionResources.InvalidFormatString(format));
}
}
// Last segment
else
{
result.Append(format.Substring(index));
break;
}
}
return result.ToString();
}
private Boolean ReadArgIndex(
String str,
Int32 startIndex,
out Byte result,
out Int32 endIndex)
{
// Count the number of digits
var length = 0;
while (Char.IsDigit(SafeCharAt(str, startIndex + length)))
{
length++;
}
// Validate at least one digit
if (length < 1)
{
result = default;
endIndex = default;
return false;
}
// Parse the number
endIndex = startIndex + length - 1;
return Byte.TryParse(str.Substring(startIndex, length), NumberStyles.None, CultureInfo.InvariantCulture, out result);
}
private Boolean ReadFormatSpecifiers(
String str,
Int32 startIndex,
out String result,
out Int32 rbrace)
{
// No format specifiers
var c = SafeCharAt(str, startIndex);
if (c == '}')
{
result = String.Empty;
rbrace = startIndex;
return true;
}
// Validate starts with ":"
if (c != ':')
{
result = default;
rbrace = default;
return false;
}
// Read the specifiers
var specifiers = new StringBuilder();
var index = startIndex + 1;
while (true)
{
// Validate not the end of the string
if (index >= str.Length)
{
result = default;
rbrace = default;
return false;
}
c = str[index];
// Not right-brace
if (c != '}')
{
specifiers.Append(c);
index++;
}
// Escaped right-brace
else if (SafeCharAt(str, index + 1) == '}')
{
specifiers.Append('}');
index += 2;
}
// Closing right-brace
else
{
result = specifiers.ToString();
rbrace = index;
return true;
}
}
}
private Char SafeCharAt(
String str,
Int32 index)
{
if (str.Length > index)
{
return str[index];
}
return '\0';
}
private sealed class FormatResultBuilder
{
internal FormatResultBuilder(
Format node,
EvaluationContext context,
MemoryCounter counter)
{
m_node = node;
m_context = context;
m_counter = counter;
m_cache = new ArgValue[node.Parameters.Count - 1];
}
// Build the final string. This is when lazy segments are evaluated.
public override String ToString()
{
return String.Join(
String.Empty,
m_segments.Select(obj =>
{
if (obj is Lazy<String> lazy)
{
return lazy.Value;
}
else
{
return obj as String;
}
}));
}
// Append a static value
internal void Append(String value)
{
if (value?.Length > 0)
{
// Track memory
m_counter.Add(value);
// Append the segment
m_segments.Add(value);
}
}
// Append an argument
internal void Append(
Int32 argIndex,
String formatSpecifiers)
{
// Delay execution until the final ToString
m_segments.Add(new Lazy<String>(() =>
{
String result;
// Get the arg from the cache
var argValue = m_cache[argIndex];
// Evaluate the arg and cache the result
if (argValue == null)
{
// The evaluation result is required when format specifiers are used. Otherwise the string
// result is required. Go ahead and store both values. Since ConvertToString produces tracing,
// we need to run that now so the tracing appears in order in the log.
var evaluationResult = m_node.Parameters[argIndex + 1].Evaluate(m_context);
var stringResult = evaluationResult.ConvertToString();
argValue = new ArgValue(evaluationResult, stringResult);
m_cache[argIndex] = argValue;
}
// No format specifiers
if (String.IsNullOrEmpty(formatSpecifiers))
{
result = argValue.StringResult;
}
// Invalid
else
{
throw new FormatException(ExpressionResources.InvalidFormatSpecifiers(formatSpecifiers, argValue.EvaluationResult.Kind));
}
// Track memory
if (!String.IsNullOrEmpty(result))
{
m_counter.Add(result);
}
return result;
}));
}
private readonly ArgValue[] m_cache;
private readonly EvaluationContext m_context;
private readonly MemoryCounter m_counter;
private readonly Format m_node;
private readonly List<Object> m_segments = new List<Object>();
}
/// <summary>
/// Stores an EvaluateResult and the value converted to a String.
/// </summary>
private sealed class ArgValue
{
public ArgValue(
EvaluationResult evaluationResult,
String stringResult)
{
EvaluationResult = evaluationResult;
StringResult = stringResult;
}
public EvaluationResult EvaluationResult { get; }
public String StringResult { get; }
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Minimatch;
using System.IO;
using System.Security.Cryptography;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class HashFiles : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
// hashFiles() only works on the runner and only works with files under GITHUB_WORKSPACE
// Since GITHUB_WORKSPACE is set by runner, I am using that as the fact of this code runs on server or runner.
if (context.State is ObjectTemplating.TemplateContext templateContext &&
templateContext.ExpressionValues.TryGetValue(PipelineTemplateConstants.GitHub, out var githubContextData) &&
githubContextData is DictionaryContextData githubContext &&
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out var workspace) == true &&
workspace is StringContextData workspaceData)
{
string searchRoot = workspaceData.Value;
string pattern = Parameters[0].Evaluate(context).ConvertToString();
context.Trace.Info($"Search root directory: '{searchRoot}'");
context.Trace.Info($"Search pattern: '{pattern}'");
var files = Directory.GetFiles(searchRoot, "*", SearchOption.AllDirectories).OrderBy(x => x).ToList();
if (files.Count == 0)
{
throw new ArgumentException($"'hashFiles({pattern})' failed. Directory '{searchRoot}' is empty");
}
else
{
context.Trace.Info($"Found {files.Count} files");
}
var matcher = new Minimatcher(pattern, s_minimatchOptions);
files = matcher.Filter(files).ToList();
if (files.Count == 0)
{
throw new ArgumentException($"'hashFiles({pattern})' failed. Search pattern '{pattern}' doesn't match any file under '{searchRoot}'");
}
else
{
context.Trace.Info($"{files.Count} matches to hash");
}
List<byte> filesSha256 = new List<byte>();
foreach (var file in files)
{
context.Trace.Info($"Hash {file}");
using (SHA256 sha256hash = SHA256.Create())
{
using (var fileStream = File.OpenRead(file))
{
filesSha256.AddRange(sha256hash.ComputeHash(fileStream));
}
}
}
using (SHA256 sha256hash = SHA256.Create())
{
var hashBytes = sha256hash.ComputeHash(filesSha256.ToArray());
StringBuilder hashString = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
hashString.Append(hashBytes[i].ToString("x2"));
}
var result = hashString.ToString();
context.Trace.Info($"Final hash result: '{result}'");
return result;
}
}
else
{
throw new InvalidOperationException("'hashfiles' expression function is only supported under runner context.");
}
}
private static readonly Options s_minimatchOptions = new Options
{
Dot = true,
NoBrace = true,
NoCase = Environment.OSVersion.Platform != PlatformID.Unix && Environment.OSVersion.Platform != PlatformID.MacOSX
};
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Text;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class Join : Function
{
protected sealed override Boolean TraceFullyRealized => true;
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var items = Parameters[0].Evaluate(context);
// Array
if (items.TryGetCollectionInterface(out var collection) &&
collection is IReadOnlyArray array &&
array.Count > 0)
{
var result = new StringBuilder();
var memory = new MemoryCounter(this, context.Options.MaxMemory);
// Append the first item
var item = array[0];
var itemResult = EvaluationResult.CreateIntermediateResult(context, item);
var itemString = itemResult.ConvertToString();
memory.Add(itemString);
result.Append(itemString);
// More items?
if (array.Count > 1)
{
var separator = ",";
if (Parameters.Count > 1)
{
var separatorResult = Parameters[1].Evaluate(context);
if (separatorResult.IsPrimitive)
{
separator = separatorResult.ConvertToString();
}
}
for (var i = 1; i < array.Count; i++)
{
// Append the separator
memory.Add(separator);
result.Append(separator);
// Append the next item
var nextItem = array[i];
var nextItemResult = EvaluationResult.CreateIntermediateResult(context, nextItem);
var nextItemString = nextItemResult.ConvertToString();
memory.Add(nextItemString);
result.Append(nextItemString);
}
}
return result.ToString();
}
// Primitive
else if (items.IsPrimitive)
{
return items.ConvertToString();
}
// Otherwise return empty string
else
{
return String.Empty;
}
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
/// <summary>
/// Used for building expression parse trees.
/// </summary>
internal sealed class NoOperation : Function
{
protected override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
return null;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class StartsWith : Function
{
protected sealed override Boolean TraceFullyRealized => false;
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
if (left.IsPrimitive)
{
var leftString = left.ConvertToString();
var right = Parameters[1].Evaluate(context);
if (right.IsPrimitive)
{
var rightString = right.ConvertToString();
return leftString.StartsWith(rightString, StringComparison.OrdinalIgnoreCase);
}
}
return false;
}
}
}

View File

@@ -0,0 +1,390 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using GitHub.Services.WebApi;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class ToJson : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var result = new StringBuilder();
var memory = new MemoryCounter(this, context.Options.MaxMemory);
var current = Parameters[0].Evaluate(context);
var ancestors = new Stack<ICollectionEnumerator>();
do
{
// Descend as much as possible
while (true)
{
// Collection
if (current.TryGetCollectionInterface(out Object collection))
{
// Array
if (collection is IReadOnlyArray array)
{
if (array.Count > 0)
{
// Write array start
WriteArrayStart(result, memory, ancestors);
// Move to first item
var enumerator = new ArrayEnumerator(context, current, array);
enumerator.MoveNext();
ancestors.Push(enumerator);
current = enumerator.Current;
}
else
{
// Write empty array
WriteEmptyArray(result, memory, ancestors);
break;
}
}
// Mapping
else if (collection is IReadOnlyObject obj)
{
if (obj.Count > 0)
{
// Write mapping start
WriteMappingStart(result, memory, ancestors);
// Move to first pair
var enumerator = new ObjectEnumerator(context, current, obj);
enumerator.MoveNext();
ancestors.Push(enumerator);
// Write mapping key
WriteMappingKey(context, result, memory, enumerator.Current.Key, ancestors);
// Move to mapping value
current = enumerator.Current.Value;
}
else
{
// Write empty mapping
WriteEmptyMapping(result, memory, ancestors);
break;
}
}
else
{
throw new NotSupportedException($"Unexpected type '{collection?.GetType().FullName}'");
}
}
// Not a collection
else
{
// Write value
WriteValue(context, result, memory, current, ancestors);
break;
}
}
// Next sibling or ancestor sibling
do
{
if (ancestors.Count > 0)
{
var parent = ancestors.Peek();
// Parent array
if (parent is ArrayEnumerator arrayEnumerator)
{
// Move to next item
if (arrayEnumerator.MoveNext())
{
current = arrayEnumerator.Current;
break;
}
// Move to parent
else
{
ancestors.Pop();
current = arrayEnumerator.Array;
// Write array end
WriteArrayEnd(result, memory, ancestors);
}
}
// Parent mapping
else if (parent is ObjectEnumerator objectEnumerator)
{
// Move to next pair
if (objectEnumerator.MoveNext())
{
// Write mapping key
WriteMappingKey(context, result, memory, objectEnumerator.Current.Key, ancestors);
// Move to mapping value
current = objectEnumerator.Current.Value;
break;
}
// Move to parent
else
{
ancestors.Pop();
current = objectEnumerator.Object;
// Write mapping end
WriteMappingEnd(result, memory, ancestors);
}
}
else
{
throw new NotSupportedException($"Unexpected type '{parent?.GetType().FullName}'");
}
}
else
{
current = null;
}
} while (current != null);
} while (current != null);
return result.ToString();
}
private void WriteArrayStart(
StringBuilder writer,
MemoryCounter memory,
Stack<ICollectionEnumerator> ancestors)
{
var str = PrefixValue("[", ancestors);
memory.Add(str);
writer.Append(str);
}
private void WriteMappingStart(
StringBuilder writer,
MemoryCounter memory,
Stack<ICollectionEnumerator> ancestors)
{
var str = PrefixValue("{", ancestors);
memory.Add(str);
writer.Append(str);
}
private void WriteArrayEnd(
StringBuilder writer,
MemoryCounter memory,
Stack<ICollectionEnumerator> ancestors)
{
var str = $"\n{new String(' ', ancestors.Count * 2)}]";
memory.Add(str);
writer.Append(str);
}
private void WriteMappingEnd(
StringBuilder writer,
MemoryCounter memory,
Stack<ICollectionEnumerator> ancestors)
{
var str = $"\n{new String(' ', ancestors.Count * 2)}}}";
memory.Add(str);
writer.Append(str);
}
private void WriteEmptyArray(
StringBuilder writer,
MemoryCounter memory,
Stack<ICollectionEnumerator> ancestors)
{
var str = PrefixValue("[]", ancestors);
memory.Add(str);
writer.Append(str);
}
private void WriteEmptyMapping(
StringBuilder writer,
MemoryCounter memory,
Stack<ICollectionEnumerator> ancestors)
{
var str = PrefixValue("{}", ancestors);
memory.Add(str);
writer.Append(str);
}
private void WriteMappingKey(
EvaluationContext context,
StringBuilder writer,
MemoryCounter memory,
EvaluationResult key,
Stack<ICollectionEnumerator> ancestors)
{
var str = PrefixValue(JsonUtility.ToString(key.ConvertToString()), ancestors, isMappingKey: true);
memory.Add(str);
writer.Append(str);
}
private void WriteValue(
EvaluationContext context,
StringBuilder writer,
MemoryCounter memory,
EvaluationResult value,
Stack<ICollectionEnumerator> ancestors)
{
String str;
switch (value.Kind)
{
case ValueKind.Null:
str = "null";
break;
case ValueKind.Boolean:
str = (Boolean)value.Value ? "true" : "false";
break;
case ValueKind.Number:
str = value.ConvertToString();
break;
case ValueKind.String:
str = JsonUtility.ToString(value.Value);
break;
default:
str = "{}"; // The value is an object we don't know how to traverse
break;
}
str = PrefixValue(str, ancestors);
memory.Add(str);
writer.Append(str);
}
private String PrefixValue(
String value,
Stack<ICollectionEnumerator> ancestors,
Boolean isMappingKey = false)
{
var level = ancestors.Count;
var parent = level > 0 ? ancestors.Peek() : null;
if (!isMappingKey && parent is ObjectEnumerator)
{
return $": {value}";
}
else if (level > 0)
{
return $"{(parent.IsFirst ? String.Empty : ",")}\n{new String(' ', level * 2)}{value}";
}
else
{
return value;
}
}
private interface ICollectionEnumerator : IEnumerator
{
Boolean IsFirst { get; }
}
private sealed class ArrayEnumerator : ICollectionEnumerator
{
public ArrayEnumerator(
EvaluationContext context,
EvaluationResult result,
IReadOnlyArray array)
{
m_context = context;
m_result = result;
m_enumerator = array.GetEnumerator();
}
public EvaluationResult Array => m_result;
public EvaluationResult Current => m_current;
Object IEnumerator.Current => m_current;
public Boolean IsFirst => m_index == 0;
public Boolean MoveNext()
{
if (m_enumerator.MoveNext())
{
m_current = EvaluationResult.CreateIntermediateResult(m_context, m_enumerator.Current);
m_index++;
return true;
}
else
{
m_current = null;
return false;
}
}
public void Reset()
{
throw new NotSupportedException(nameof(Reset));
}
private readonly EvaluationContext m_context;
private readonly IEnumerator m_enumerator;
private readonly EvaluationResult m_result;
private EvaluationResult m_current;
private Int32 m_index = -1;
}
private sealed class ObjectEnumerator : ICollectionEnumerator
{
public ObjectEnumerator(
EvaluationContext context,
EvaluationResult result,
IReadOnlyObject obj)
{
m_context = context;
m_result = result;
m_enumerator = obj.GetEnumerator();
}
public KeyValuePair<EvaluationResult, EvaluationResult> Current => m_current;
Object IEnumerator.Current => m_current;
public Boolean IsFirst => m_index == 0;
public EvaluationResult Object => m_result;
public Boolean MoveNext()
{
if (m_enumerator.MoveNext())
{
var current = (KeyValuePair<String, Object>)m_enumerator.Current;
var key = EvaluationResult.CreateIntermediateResult(m_context, current.Key);
var value = EvaluationResult.CreateIntermediateResult(m_context, current.Value);
m_current = new KeyValuePair<EvaluationResult, EvaluationResult>(key, value);
m_index++;
return true;
}
else
{
m_current = default(KeyValuePair<EvaluationResult, EvaluationResult>);
return false;
}
}
public void Reset()
{
throw new NotSupportedException(nameof(Reset));
}
private readonly EvaluationContext m_context;
private readonly IEnumerator m_enumerator;
private readonly EvaluationResult m_result;
private KeyValuePair<EvaluationResult, EvaluationResult> m_current;
private Int32 m_index = -1;
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IBoolean
{
Boolean GetBoolean();
}
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface INull
{
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface INumber
{
Double GetNumber();
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IReadOnlyArray
{
Int32 Count { get; }
Object this[Int32 index] { get; }
IEnumerator GetEnumerator();
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IReadOnlyObject
{
Int32 Count { get; }
IEnumerable<String> Keys { get; }
IEnumerable<Object> Values { get; }
Object this[String key] { get; }
Boolean ContainsKey(String key);
IEnumerator GetEnumerator();
Boolean TryGetValue(
String key,
out Object value);
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IString
{
String GetString();
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Literal : ExpressionNode
{
public Literal(Object val)
{
Value = ExpressionUtility.ConvertToCanonicalValue(val, out var kind, out _);
Kind = kind;
Name = kind.ToString();
}
public ValueKind Kind { get; }
public Object Value { get; }
// Prevent the value from being stored on the evaluation context.
// This avoids unneccessarily duplicating the value in memory.
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return ExpressionUtility.FormatValue(null, Value, Kind);
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
return ExpressionUtility.FormatValue(null, Value, Kind);
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
return Value;
}
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.ComponentModel;
using Newtonsoft.Json.Linq;
using ExpressionResources = GitHub.DistributedTask.Expressions.ExpressionResources;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
/// <summary>
/// Helper class for ExpressionNode authors. This class helps calculate memory overhead for a result object.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class MemoryCounter
{
internal MemoryCounter(
ExpressionNode node,
Int32? maxBytes)
{
m_node = node;
m_maxBytes = (maxBytes ?? 0) > 0 ? maxBytes.Value : Int32.MaxValue;
}
public Int32 CurrentBytes => m_currentBytes;
public void Add(Int32 amount)
{
if (!TryAdd(amount))
{
throw new InvalidOperationException(ExpressionResources.ExceededAllowedMemory(m_node?.ConvertToExpression()));
}
}
public void Add(String value)
{
Add(CalculateSize(value));
}
public void AddMinObjectSize()
{
Add(MinObjectSize);
}
public void Remove(String value)
{
m_currentBytes -= CalculateSize(value);
}
public static Int32 CalculateSize(String value)
{
// This measurement doesn't have to be perfect.
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
Int32 bytes;
checked
{
bytes = StringBaseOverhead + ((value?.Length ?? 0) * 2);
}
return bytes;
}
internal Boolean TryAdd(Int32 amount)
{
try
{
checked
{
amount += m_currentBytes;
}
if (amount > m_maxBytes)
{
return false;
}
m_currentBytes = amount;
return true;
}
catch (OverflowException)
{
return false;
}
}
internal Boolean TryAdd(String value)
{
return TryAdd(CalculateSize(value));
}
internal const Int32 MinObjectSize = 24;
internal const Int32 StringBaseOverhead = 26;
private readonly Int32 m_maxBytes;
private readonly ExpressionNode m_node;
private Int32 m_currentBytes;
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class NamedValue : ExpressionNode
{
internal sealed override string ConvertToExpression() => Name;
protected sealed override Boolean TraceFullyRealized => true;
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return Name;
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
internal sealed class NoOperationNamedValue : NamedValue
{
protected override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
return null;
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Globalization;
using System.Linq;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class And : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0})",
String.Join(" && ", Parameters.Select(x => x.ConvertToExpression())));
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0})",
String.Join(" && ", Parameters.Select(x => x.ConvertToRealizedExpression(context))));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var result = default(EvaluationResult);
foreach (var parameter in Parameters)
{
result = parameter.Evaluate(context);
if (result.IsFalsy)
{
return result.Value;
}
}
return result?.Value;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class Equal : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0} == {1})",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0} == {1})",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
var right = Parameters[1].Evaluate(context);
return left.AbstractEqual(right);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class GreaterThan : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0} > {1})",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0} > {1})",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
var right = Parameters[1].Evaluate(context);
return left.AbstractGreaterThan(right);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class GreaterThanOrEqual : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0} >= {1})",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0} >= {1})",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
var right = Parameters[1].Evaluate(context);
return left.AbstractGreaterThanOrEqual(right);
}
}
}

View File

@@ -0,0 +1,286 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class Index : Container
{
protected sealed override Boolean TraceFullyRealized => true;
internal sealed override String ConvertToExpression()
{
// Verify if we can simplify the expression, we would rather return
// github.sha then github['sha'] so we check if this is a simple case.
if (Parameters[1] is Literal literal &&
literal.Value is String literalString &&
ExpressionUtility.IsLegalKeyword(literalString))
{
return String.Format(
CultureInfo.InvariantCulture,
"{0}.{1}",
Parameters[0].ConvertToExpression(),
literalString);
}
else
{
return String.Format(
CultureInfo.InvariantCulture,
"{0}[{1}]",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"{0}[{1}]",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
var left = Parameters[0].Evaluate(context);
// Not a collection
if (!left.TryGetCollectionInterface(out Object collection))
{
resultMemory = null;
return Parameters[1] is Wildcard ? new FilteredArray() : null;
}
// Filtered array
else if (collection is FilteredArray filteredArray)
{
return HandleFilteredArray(context, filteredArray, out resultMemory);
}
// Object
else if (collection is IReadOnlyObject obj)
{
return HandleObject(context, obj, out resultMemory);
}
// Array
else if (collection is IReadOnlyArray array)
{
return HandleArray(context, array, out resultMemory);
}
resultMemory = null;
return null;
}
private Object HandleFilteredArray(
EvaluationContext context,
FilteredArray filteredArray,
out ResultMemory resultMemory)
{
var result = new FilteredArray();
var counter = new MemoryCounter(this, context.Options.MaxMemory);
var index = new IndexHelper(context, Parameters[1]);
foreach (var item in filteredArray)
{
// Leverage the expression SDK to traverse the object
var itemResult = EvaluationResult.CreateIntermediateResult(context, item);
if (itemResult.TryGetCollectionInterface(out var nestedCollection))
{
// Apply the index to each child object
if (nestedCollection is IReadOnlyObject nestedObject)
{
// Wildcard
if (index.IsWildcard)
{
foreach (var val in nestedObject.Values)
{
result.Add(val);
counter.Add(IntPtr.Size);
}
}
// String
else if (index.HasStringIndex)
{
if (nestedObject.TryGetValue(index.StringIndex, out var nestedObjectValue))
{
result.Add(nestedObjectValue);
counter.Add(IntPtr.Size);
}
}
}
// Apply the index to each child array
else if (nestedCollection is IReadOnlyArray nestedArray)
{
// Wildcard
if (index.IsWildcard)
{
foreach (var val in nestedArray)
{
result.Add(val);
counter.Add(IntPtr.Size);
}
}
// String
else if (index.HasIntegerIndex &&
index.IntegerIndex < nestedArray.Count)
{
result.Add(nestedArray[index.IntegerIndex]);
counter.Add(IntPtr.Size);
}
}
}
}
resultMemory = new ResultMemory { Bytes = counter.CurrentBytes };
return result;
}
private Object HandleObject(
EvaluationContext context,
IReadOnlyObject obj,
out ResultMemory resultMemory)
{
var index = new IndexHelper(context, Parameters[1]);
// Wildcard
if (index.IsWildcard)
{
var filteredArray = new FilteredArray();
var counter = new MemoryCounter(this, context.Options.MaxMemory);
counter.AddMinObjectSize();
foreach (var val in obj.Values)
{
filteredArray.Add(val);
counter.Add(IntPtr.Size);
}
resultMemory = new ResultMemory { Bytes = counter.CurrentBytes };
return filteredArray;
}
// String
else if (index.HasStringIndex &&
obj.TryGetValue(index.StringIndex, out var result))
{
resultMemory = null;
return result;
}
resultMemory = null;
return null;
}
private Object HandleArray(
EvaluationContext context,
IReadOnlyArray array,
out ResultMemory resultMemory)
{
var index = new IndexHelper(context, Parameters[1]);
// Wildcard
if (index.IsWildcard)
{
var filtered = new FilteredArray();
var counter = new MemoryCounter(this, context.Options.MaxMemory);
counter.AddMinObjectSize();
foreach (var item in array)
{
filtered.Add(item);
counter.Add(IntPtr.Size);
}
resultMemory = new ResultMemory { Bytes = counter.CurrentBytes };
return filtered;
}
// Integer
else if (index.HasIntegerIndex && index.IntegerIndex < array.Count)
{
resultMemory = null;
return array[index.IntegerIndex];
}
resultMemory = null;
return null;
}
private class FilteredArray : IReadOnlyArray
{
public FilteredArray()
{
m_list = new List<Object>();
}
public void Add(Object o)
{
m_list.Add(o);
}
public Int32 Count => m_list.Count;
public Object this[Int32 index] => m_list[index];
public IEnumerator GetEnumerator() => m_list.GetEnumerator();
private readonly IList<Object> m_list;
}
private class IndexHelper
{
public IndexHelper(
EvaluationContext context,
ExpressionNode parameter)
{
m_parameter = parameter;
m_result = parameter.Evaluate(context);
m_integerIndex = new Lazy<Int32?>(() =>
{
var doubleIndex = m_result.ConvertToNumber();
if (Double.IsNaN(doubleIndex) || doubleIndex < 0d)
{
return null;
}
doubleIndex = Math.Floor(doubleIndex);
if (doubleIndex > (Double)Int32.MaxValue)
{
return null;
}
return (Int32)doubleIndex;
});
m_stringIndex = new Lazy<String>(() =>
{
return m_result.IsPrimitive ? m_result.ConvertToString() : null;
});
}
public Boolean HasIntegerIndex => m_integerIndex.Value != null;
public Boolean HasStringIndex => m_stringIndex.Value != null;
public Boolean IsWildcard => m_parameter is Wildcard;
public Int32 IntegerIndex => m_integerIndex.Value ?? default(Int32);
public String StringIndex => m_stringIndex.Value;
private readonly ExpressionNode m_parameter;
private readonly EvaluationResult m_result;
private readonly Lazy<Int32?> m_integerIndex;
private readonly Lazy<String> m_stringIndex;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class LessThan : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0} < {1})",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0} < {1})",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
var right = Parameters[1].Evaluate(context);
return left.AbstractLessThan(right);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class LessThanOrEqual : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0} <= {1})",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0} <= {1})",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
var right = Parameters[1].Evaluate(context);
return left.AbstractLessThanOrEqual(right);
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class Not : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"!{0}",
Parameters[0].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"!{0}",
Parameters[0].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var result = Parameters[0].Evaluate(context);
return result.IsFalsy;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class NotEqual : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0} != {1})",
Parameters[0].ConvertToExpression(),
Parameters[1].ConvertToExpression());
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0} != {1})",
Parameters[0].ConvertToRealizedExpression(context),
Parameters[1].ConvertToRealizedExpression(context));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var left = Parameters[0].Evaluate(context);
var right = Parameters[1].Evaluate(context);
return left.AbstractNotEqual(right);
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Globalization;
using System.Linq;
namespace GitHub.DistributedTask.Expressions2.Sdk.Operators
{
internal sealed class Or : Container
{
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return String.Format(
CultureInfo.InvariantCulture,
"({0})",
String.Join(" || ", Parameters.Select(x => x.ConvertToExpression())));
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
// Check if the result was stored
if (context.TryGetTraceResult(this, out String result))
{
return result;
}
return String.Format(
CultureInfo.InvariantCulture,
"({0})",
String.Join(" || ", Parameters.Select(x => x.ConvertToRealizedExpression(context))));
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
var result = default(EvaluationResult);
foreach (var parameter in Parameters)
{
result = parameter.Evaluate(context);
if (result.IsTruthy)
{
break;
}
}
return result?.Value;
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class ResultMemory
{
/// <summary>
/// Only set a non-null value when both of the following conditions are met:
/// 1) The result is a complex object. In other words, the result is
/// not a simple type: string, boolean, number, or null.
/// 2) The result is a newly created object.
///
/// <para>
/// For example, consider a function jsonParse() which takes a string parameter,
/// and returns a JToken object. The JToken object is newly created and a rough
/// measurement should be returned for the number of bytes it consumes in memory.
/// </para>
///
/// <para>
/// For another example, consider a function which returns a sub-object from a
/// complex parameter value. From the perspective of an individual function,
/// the size of the complex parameter value is unknown. In this situation, set the
/// value to IntPtr.Size.
/// </para>
///
/// <para>
/// When you are unsure, set the value to null. Null indicates the overhead of a
/// new pointer should be accounted for.
/// </para>
/// </summary>
public Int32? Bytes { get; set; }
/// <summary>
/// Indicates whether <c ref="Bytes" /> represents the total size of the result.
/// True indicates the accounting-overhead of downstream parameters can be discarded.
///
/// For <c ref="EvaluationOptions.Converters" />, this value is currently ignored.
///
/// <para>
/// For example, consider a funciton jsonParse() which takes a string paramter,
/// and returns a JToken object. The JToken object is newly created and a rough
/// measurement should be returned for the amount of bytes it consumes in memory.
/// Set the <c ref="IsTotal" /> to true, since new object contains no references
/// to previously allocated memory.
/// </para>
///
/// <para>
/// For another example, consider a function which wraps a complex parameter result.
/// <c ref="Bytes" /> should be set to the amount of newly allocated memory.
/// However since the object references previously allocated memory, set <c ref="IsTotal" />
/// to false.
/// </para>
/// </summary>
public Boolean IsTotal { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2.Sdk
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Wildcard : ExpressionNode
{
// Prevent the value from being stored on the evaluation context.
// This avoids unneccessarily duplicating the value in memory.
protected sealed override Boolean TraceFullyRealized => false;
internal sealed override String ConvertToExpression()
{
return ExpressionConstants.Wildcard.ToString();
}
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
{
return ExpressionConstants.Wildcard.ToString();
}
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
return ExpressionConstants.Wildcard.ToString();
}
}
}

View File

@@ -0,0 +1,9 @@
namespace GitHub.DistributedTask.Expressions2.Tokens
{
internal enum Associativity
{
None,
LeftToRight,
RightToLeft,
}
}

View File

@@ -0,0 +1,491 @@
using GitHub.DistributedTask.Expressions2.Sdk;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace GitHub.DistributedTask.Expressions2.Tokens
{
internal sealed class LexicalAnalyzer
{
public LexicalAnalyzer(String expression)
{
m_expression = expression;
}
public IEnumerable<Token> UnclosedTokens => m_unclosedTokens;
public Boolean TryGetNextToken(ref Token token)
{
// Skip whitespace
while (m_index < m_expression.Length && Char.IsWhiteSpace(m_expression[m_index]))
{
m_index++;
}
// Test end of string
if (m_index >= m_expression.Length)
{
token = null;
return false;
}
// Read the first character to determine the type of token.
var c = m_expression[m_index];
switch (c)
{
case ExpressionConstants.StartGroup: // "("
// Function call
if (m_lastToken?.Kind == TokenKind.Function)
{
token = CreateToken(TokenKind.StartParameters, c, m_index++);
}
// Logical grouping
else
{
token = CreateToken(TokenKind.StartGroup, c, m_index++);
}
break;
case ExpressionConstants.StartIndex: // "["
token = CreateToken(TokenKind.StartIndex, c, m_index++);
break;
case ExpressionConstants.EndGroup: // ")"
// Function call
if (m_unclosedTokens.FirstOrDefault()?.Kind == TokenKind.StartParameters) // "(" function call
{
token = CreateToken(TokenKind.EndParameters, c, m_index++);
}
// Logical grouping
else
{
token = CreateToken(TokenKind.EndGroup, c, m_index++);
}
break;
case ExpressionConstants.EndIndex: // "]"
token = CreateToken(TokenKind.EndIndex, c, m_index++);
break;
case ExpressionConstants.Separator: // ","
token = CreateToken(TokenKind.Separator, c, m_index++);
break;
case ExpressionConstants.Wildcard: // "*"
token = CreateToken(TokenKind.Wildcard, c, m_index++);
break;
case '\'':
token = ReadStringToken();
break;
case '!': // "!" and "!="
case '>': // ">" and ">="
case '<': // "<" and "<="
case '=': // "=="
case '&': // "&&"
case '|': // "||"
token = ReadOperator();
break;
default:
if (c == '.')
{
// Number
if (m_lastToken == null ||
m_lastToken.Kind == TokenKind.Separator || // ","
m_lastToken.Kind == TokenKind.StartGroup || // "(" logical grouping
m_lastToken.Kind == TokenKind.StartIndex || // "["
m_lastToken.Kind == TokenKind.StartParameters || // "(" function call
m_lastToken.Kind == TokenKind.LogicalOperator) // "!", "==", etc
{
token = ReadNumberToken();
}
// "."
else
{
token = CreateToken(TokenKind.Dereference, c, m_index++);
}
}
else if (c == '-' || c == '+' || (c >= '0' && c <= '9'))
{
token = ReadNumberToken();
}
else
{
token = ReadKeywordToken();
}
break;
}
m_lastToken = token;
return true;
}
private Token ReadNumberToken()
{
var startIndex = m_index;
do
{
m_index++;
}
while (m_index < m_expression.Length && (!TestTokenBoundary(m_expression[m_index]) || m_expression[m_index] == '.'));
var length = m_index - startIndex;
var str = m_expression.Substring(startIndex, length);
var d = ExpressionUtility.ParseNumber(str);
if (Double.IsNaN(d))
{
return CreateToken(TokenKind.Unexpected, str, startIndex);
}
return CreateToken(TokenKind.Number, str, startIndex, d);
}
private Token ReadKeywordToken()
{
// Read to the end of the keyword.
var startIndex = m_index;
m_index++; // Skip the first char. It is already known to be the start of the keyword.
while (m_index < m_expression.Length && !TestTokenBoundary(m_expression[m_index]))
{
m_index++;
}
// Test if valid keyword character sequence.
var length = m_index - startIndex;
var str = m_expression.Substring(startIndex, length);
if (ExpressionUtility.IsLegalKeyword(str))
{
// Test if follows property dereference operator.
if (m_lastToken != null && m_lastToken.Kind == TokenKind.Dereference)
{
return CreateToken(TokenKind.PropertyName, str, startIndex);
}
// Null
if (str.Equals(ExpressionConstants.Null, StringComparison.Ordinal))
{
return CreateToken(TokenKind.Null, str, startIndex);
}
// Boolean
else if (str.Equals(ExpressionConstants.True, StringComparison.Ordinal))
{
return CreateToken(TokenKind.Boolean, str, startIndex, true);
}
else if (str.Equals(ExpressionConstants.False, StringComparison.Ordinal))
{
return CreateToken(TokenKind.Boolean, str, startIndex, false);
}
// NaN
else if (str.Equals(ExpressionConstants.NaN, StringComparison.Ordinal))
{
return CreateToken(TokenKind.Number, str, startIndex, Double.NaN);
}
// Infinity
else if (str.Equals(ExpressionConstants.Infinity, StringComparison.Ordinal))
{
return CreateToken(TokenKind.Number, str, startIndex, Double.PositiveInfinity);
}
// Lookahead
var tempIndex = m_index;
while (tempIndex < m_expression.Length && Char.IsWhiteSpace(m_expression[tempIndex]))
{
tempIndex++;
}
// Function
if (tempIndex < m_expression.Length && m_expression[tempIndex] == ExpressionConstants.StartGroup) // "("
{
return CreateToken(TokenKind.Function, str, startIndex);
}
// Named-value
else
{
return CreateToken(TokenKind.NamedValue, str, startIndex);
}
}
else
{
// Invalid keyword
return CreateToken(TokenKind.Unexpected, str, startIndex);
}
}
private Token ReadStringToken()
{
var startIndex = m_index;
var c = default(Char);
var closed = false;
var str = new StringBuilder();
m_index++; // Skip the leading single-quote.
while (m_index < m_expression.Length)
{
c = m_expression[m_index++];
if (c == '\'')
{
// End of string.
if (m_index >= m_expression.Length || m_expression[m_index] != '\'')
{
closed = true;
break;
}
// Escaped single quote.
m_index++;
}
str.Append(c);
}
var length = m_index - startIndex;
var rawValue = m_expression.Substring(startIndex, length);
if (closed)
{
return CreateToken(TokenKind.String, rawValue, startIndex, str.ToString());
}
return CreateToken(TokenKind.Unexpected, rawValue, startIndex);
}
private Token ReadOperator()
{
var startIndex = m_index;
var raw = default(String);
m_index++;
// Check for a two-character operator
if (m_index < m_expression.Length)
{
m_index++;
raw = m_expression.Substring(startIndex, 2);
switch (raw)
{
case ExpressionConstants.NotEqual:
case ExpressionConstants.GreaterThanOrEqual:
case ExpressionConstants.LessThanOrEqual:
case ExpressionConstants.Equal:
case ExpressionConstants.And:
case ExpressionConstants.Or:
return CreateToken(TokenKind.LogicalOperator, raw, startIndex);
}
// Backup
m_index--;
}
// Check for one-character operator
raw = m_expression.Substring(startIndex, 1);
switch (raw)
{
case ExpressionConstants.Not:
case ExpressionConstants.GreaterThan:
case ExpressionConstants.LessThan:
return CreateToken(TokenKind.LogicalOperator, raw, startIndex);
}
// Unexpected
while (m_index < m_expression.Length && !TestTokenBoundary(m_expression[m_index]))
{
m_index++;
}
var length = m_index - startIndex;
raw = m_expression.Substring(startIndex, length);
return CreateToken(TokenKind.Unexpected, raw, startIndex);
}
private static Boolean TestTokenBoundary(Char c)
{
switch (c)
{
case ExpressionConstants.StartGroup: // "("
case ExpressionConstants.StartIndex: // "["
case ExpressionConstants.EndGroup: // ")"
case ExpressionConstants.EndIndex: // "]"
case ExpressionConstants.Separator: // ","
case ExpressionConstants.Dereference: // "."
case '!': // "!" and "!="
case '>': // ">" and ">="
case '<': // "<" and "<="
case '=': // "=="
case '&': // "&&"
case '|': // "||"
return true;
default:
return char.IsWhiteSpace(c);
}
}
private Token CreateToken(
TokenKind kind,
Char rawValue,
Int32 index,
Object parsedValue = null)
{
return CreateToken(kind, rawValue.ToString(), index, parsedValue);
}
private Token CreateToken(
TokenKind kind,
String rawValue,
Int32 index,
Object parsedValue = null)
{
// Check whether the current token is legal based on the last token
var legal = false;
switch (kind)
{
case TokenKind.StartGroup: // "(" logical grouping
// Is first or follows "," or "(" or "[" or a logical operator
legal = CheckLastToken(null, TokenKind.Separator, TokenKind.StartGroup, TokenKind.StartParameters, TokenKind.StartIndex, TokenKind.LogicalOperator);
break;
case TokenKind.StartIndex: // "["
// Follows ")", "]", "*", a property name, or a named-value
legal = CheckLastToken(TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.PropertyName, TokenKind.NamedValue);
break;
case TokenKind.StartParameters: // "(" function call
// Follows a function
legal = CheckLastToken(TokenKind.Function);
break;
case TokenKind.EndGroup: // ")" logical grouping
// Follows ")", "]", "*", a literal, a property name, or a named-value
legal = CheckLastToken(TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.Null, TokenKind.Boolean, TokenKind.Number, TokenKind.String, TokenKind.PropertyName, TokenKind.NamedValue);
break;
case TokenKind.EndIndex: // "]"
// Follows ")", "]", "*", a literal, a property name, or a named-value
legal = CheckLastToken(TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.Null, TokenKind.Boolean, TokenKind.Number, TokenKind.String, TokenKind.PropertyName, TokenKind.NamedValue);
break;
case TokenKind.EndParameters: // ")" function call
// Follows "(" function call, ")", "]", "*", a literal, a property name, or a named-value
legal = CheckLastToken(TokenKind.StartParameters, TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.Null, TokenKind.Boolean, TokenKind.Number, TokenKind.String, TokenKind.PropertyName, TokenKind.NamedValue);
break;
case TokenKind.Separator: // ","
// Follows ")", "]", "*", a literal, a property name, or a named-value
legal = CheckLastToken(TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.Null, TokenKind.Boolean, TokenKind.Number, TokenKind.String, TokenKind.PropertyName, TokenKind.NamedValue);
break;
case TokenKind.Dereference: // "."
// Follows ")", "]", "*", a property name, or a named-value
legal = CheckLastToken(TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.PropertyName, TokenKind.NamedValue);
break;
case TokenKind.Wildcard: // "*"
// Follows "[" or "."
legal = CheckLastToken(TokenKind.StartIndex, TokenKind.Dereference);
break;
case TokenKind.LogicalOperator: // "!", "==", etc
switch (rawValue)
{
case ExpressionConstants.Not:
// Is first or follows "," or "(" or "[" or a logical operator
legal = CheckLastToken(null, TokenKind.Separator, TokenKind.StartGroup, TokenKind.StartParameters, TokenKind.StartIndex, TokenKind.LogicalOperator);
break;
default:
// Follows ")", "]", "*", a literal, a property name, or a named-value
legal = CheckLastToken(TokenKind.EndGroup, TokenKind.EndParameters, TokenKind.EndIndex, TokenKind.Wildcard, TokenKind.Null, TokenKind.Boolean, TokenKind.Number, TokenKind.String, TokenKind.PropertyName, TokenKind.NamedValue);
break;
}
break;
case TokenKind.Null:
case TokenKind.Boolean:
case TokenKind.Number:
case TokenKind.String:
// Is first or follows "," or "[" or "(" or a logical operator (e.g. "!" or "==" etc)
legal = CheckLastToken(null, TokenKind.Separator, TokenKind.StartIndex, TokenKind.StartGroup, TokenKind.StartParameters, TokenKind.LogicalOperator);
break;
case TokenKind.PropertyName:
// Follows "."
legal = CheckLastToken(TokenKind.Dereference);
break;
case TokenKind.Function:
// Is first or follows "," or "[" or "(" or a logical operator (e.g. "!" or "==" etc)
legal = CheckLastToken(null, TokenKind.Separator, TokenKind.StartIndex, TokenKind.StartGroup, TokenKind.StartParameters, TokenKind.LogicalOperator);
break;
case TokenKind.NamedValue:
// Is first or follows "," or "[" or "(" or a logical operator (e.g. "!" or "==" etc)
legal = CheckLastToken(null, TokenKind.Separator, TokenKind.StartIndex, TokenKind.StartGroup, TokenKind.StartParameters, TokenKind.LogicalOperator);
break;
}
// Illegal
if (!legal)
{
return new Token(TokenKind.Unexpected, rawValue, index);
}
// Legal so far
var token = new Token(kind, rawValue, index, parsedValue);
switch (kind)
{
case TokenKind.StartGroup: // "(" logical grouping
case TokenKind.StartIndex: // "["
case TokenKind.StartParameters: // "(" function call
// Track start token
m_unclosedTokens.Push(token);
break;
case TokenKind.EndGroup: // ")" logical grouping
// Check inside logical grouping
if (m_unclosedTokens.FirstOrDefault()?.Kind != TokenKind.StartGroup)
{
return new Token(TokenKind.Unexpected, rawValue, index);
}
// Pop start token
m_unclosedTokens.Pop();
break;
case TokenKind.EndIndex: // "]"
// Check inside indexer
if (m_unclosedTokens.FirstOrDefault()?.Kind != TokenKind.StartIndex)
{
return new Token(TokenKind.Unexpected, rawValue, index);
}
// Pop start token
m_unclosedTokens.Pop();
break;
case TokenKind.EndParameters: // ")" function call
// Check inside function call
if (m_unclosedTokens.FirstOrDefault()?.Kind != TokenKind.StartParameters)
{
return new Token(TokenKind.Unexpected, rawValue, index);
}
// Pop start token
m_unclosedTokens.Pop();
break;
case TokenKind.Separator: // ","
// Check inside function call
if (m_unclosedTokens.FirstOrDefault()?.Kind != TokenKind.StartParameters)
{
return new Token(TokenKind.Unexpected, rawValue, index);
}
break;
}
return token;
}
/// <summary>
/// Checks whether the last token kind is in the array of allowed kinds.
/// </summary>
private Boolean CheckLastToken(params TokenKind?[] allowed)
{
var lastKind = m_lastToken?.Kind;
foreach (var kind in allowed)
{
if (kind == lastKind)
{
return true;
}
}
return false;
}
private readonly String m_expression; // Raw expression string
private readonly Stack<Token> m_unclosedTokens = new Stack<Token>(); // Unclosed start tokens
private Int32 m_index; // Index of raw expression string
private Token m_lastToken;
}
}

View File

@@ -0,0 +1,209 @@
using System;
using GitHub.DistributedTask.Expressions2.Sdk;
using GitHub.DistributedTask.Expressions2.Sdk.Operators;
namespace GitHub.DistributedTask.Expressions2.Tokens
{
internal sealed class Token
{
public Token(
TokenKind kind,
String rawValue,
Int32 index,
Object parsedValue = null)
{
Kind = kind;
RawValue = rawValue;
Index = index;
ParsedValue = parsedValue;
}
public TokenKind Kind { get; }
public String RawValue { get; }
public Int32 Index { get; }
public Object ParsedValue { get; }
public Associativity Associativity
{
get
{
switch (Kind)
{
case TokenKind.StartGroup:
return Associativity.None;
case TokenKind.LogicalOperator:
switch (RawValue)
{
case ExpressionConstants.Not: // "!"
return Associativity.RightToLeft;
}
break;
}
return IsOperator ? Associativity.LeftToRight : Associativity.None;
}
}
public Boolean IsOperator
{
get
{
switch (Kind)
{
case TokenKind.StartGroup: // "(" logical grouping
case TokenKind.StartIndex: // "["
case TokenKind.StartParameters: // "(" function call
case TokenKind.EndGroup: // ")" logical grouping
case TokenKind.EndIndex: // "]"
case TokenKind.EndParameters: // ")" function call
case TokenKind.Separator: // ","
case TokenKind.Dereference: // "."
case TokenKind.LogicalOperator: // "!", "==", etc
return true;
default:
return false;
}
}
}
/// <summary>
/// Operator precedence. The value is only meaningful for operator tokens.
/// </summary>
public Int32 Precedence
{
get
{
switch (Kind)
{
case TokenKind.StartGroup: // "(" logical grouping
return 20;
case TokenKind.StartIndex: // "["
case TokenKind.StartParameters: // "(" function call
case TokenKind.Dereference: // "."
return 19;
case TokenKind.LogicalOperator:
switch (RawValue)
{
case ExpressionConstants.Not: // "!"
return 16;
case ExpressionConstants.GreaterThan: // ">"
case ExpressionConstants.GreaterThanOrEqual:// ">="
case ExpressionConstants.LessThan: // "<"
case ExpressionConstants.LessThanOrEqual: // "<="
return 11;
case ExpressionConstants.Equal: // "=="
case ExpressionConstants.NotEqual: // "!="
return 10;
case ExpressionConstants.And: // "&&"
return 6;
case ExpressionConstants.Or: // "||"
return 5;
}
break;
case TokenKind.EndGroup: // ")" logical grouping
case TokenKind.EndIndex: // "]"
case TokenKind.EndParameters: // ")" function call
case TokenKind.Separator: // ","
return 1;
}
return 0;
}
}
/// <summary>
/// Expected number of operands. The value is only meaningful for standalone unary operators and binary operators.
/// </summary>
public Int32 OperandCount
{
get
{
switch (Kind)
{
case TokenKind.StartIndex: // "["
case TokenKind.Dereference: // "."
return 2;
case TokenKind.LogicalOperator:
switch (RawValue)
{
case ExpressionConstants.Not: // "!"
return 1;
case ExpressionConstants.GreaterThan: // ">"
case ExpressionConstants.GreaterThanOrEqual:// ">="
case ExpressionConstants.LessThan: // "<"
case ExpressionConstants.LessThanOrEqual: // "<="
case ExpressionConstants.Equal: // "=="
case ExpressionConstants.NotEqual: // "!="
case ExpressionConstants.And: // "&&"
case ExpressionConstants.Or: // "|"
return 2;
}
break;
}
return 0;
}
}
public ExpressionNode ToNode()
{
switch (Kind)
{
case TokenKind.StartIndex: // "["
case TokenKind.Dereference: // "."
return new Index();
case TokenKind.LogicalOperator:
switch (RawValue)
{
case ExpressionConstants.Not: // "!"
return new Not();
case ExpressionConstants.NotEqual: // "!="
return new NotEqual();
case ExpressionConstants.GreaterThan: // ">"
return new GreaterThan();
case ExpressionConstants.GreaterThanOrEqual:// ">="
return new GreaterThanOrEqual();
case ExpressionConstants.LessThan: // "<"
return new LessThan();
case ExpressionConstants.LessThanOrEqual: // "<="
return new LessThanOrEqual();
case ExpressionConstants.Equal: // "=="
return new Equal();
case ExpressionConstants.And: // "&&"
return new And();
case ExpressionConstants.Or: // "||"
return new Or();
default:
throw new NotSupportedException($"Unexpected logical operator '{RawValue}' when creating node");
}
case TokenKind.Null:
case TokenKind.Boolean:
case TokenKind.Number:
case TokenKind.String:
return new Literal(ParsedValue);
case TokenKind.PropertyName:
return new Literal(RawValue);
case TokenKind.Wildcard: // "*"
return new Wildcard();
}
throw new NotSupportedException($"Unexpected kind '{Kind}' when creating node");
}
}
}

View File

@@ -0,0 +1,28 @@
namespace GitHub.DistributedTask.Expressions2.Tokens
{
internal enum TokenKind
{
// Punctuation
StartGroup, // "(" logical grouping
StartIndex, // "["
StartParameters, // "(" function call
EndGroup, // ")" logical grouping
EndIndex, // "]"
EndParameters, // ")" function call
Separator, // ","
Dereference, // "."
Wildcard, // "*"
LogicalOperator, // "!", "==", etc
// Values
Null,
Boolean,
Number,
String,
PropertyName,
Function,
NamedValue,
Unexpected,
}
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel;
namespace GitHub.DistributedTask.Expressions2
{
[EditorBrowsable(EditorBrowsableState.Never)]
public enum ValueKind
{
Array,
Boolean,
Null,
Number,
Object,
String,
}
}