mirror of
https://github.com/actions/runner.git
synced 2025-12-20 06:29:53 +00:00
GitHub Actions Runner
This commit is contained in:
23
src/Sdk/DTExpressions2/Expressions2/EvaluationOptions.cs
Normal file
23
src/Sdk/DTExpressions2/Expressions2/EvaluationOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
453
src/Sdk/DTExpressions2/Expressions2/EvaluationResult.cs
Normal file
453
src/Sdk/DTExpressions2/Expressions2/EvaluationResult.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
60
src/Sdk/DTExpressions2/Expressions2/ExpressionConstants.cs
Normal file
60
src/Sdk/DTExpressions2/Expressions2/ExpressionConstants.cs
Normal 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 = "||";
|
||||
}
|
||||
}
|
||||
24
src/Sdk/DTExpressions2/Expressions2/ExpressionException.cs
Normal file
24
src/Sdk/DTExpressions2/Expressions2/ExpressionException.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
471
src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs
Normal file
471
src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Sdk/DTExpressions2/Expressions2/FunctionInfo.cs
Normal file
29
src/Sdk/DTExpressions2/Expressions2/FunctionInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Sdk/DTExpressions2/Expressions2/IExpressionNode.cs
Normal file
25
src/Sdk/DTExpressions2/Expressions2/IExpressionNode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
237
src/Sdk/DTExpressions2/Expressions2/IExpressionNodeExtensions.cs
Normal file
237
src/Sdk/DTExpressions2/Expressions2/IExpressionNodeExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Sdk/DTExpressions2/Expressions2/IFunctionInfo.cs
Normal file
15
src/Sdk/DTExpressions2/Expressions2/IFunctionInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
13
src/Sdk/DTExpressions2/Expressions2/INamedValueInfo.cs
Normal file
13
src/Sdk/DTExpressions2/Expressions2/INamedValueInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
12
src/Sdk/DTExpressions2/Expressions2/ITraceWriter.cs
Normal file
12
src/Sdk/DTExpressions2/Expressions2/ITraceWriter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/Sdk/DTExpressions2/Expressions2/NamedValueInfo.cs
Normal file
23
src/Sdk/DTExpressions2/Expressions2/NamedValueInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Sdk/DTExpressions2/Expressions2/ParseException.cs
Normal file
68
src/Sdk/DTExpressions2/Expressions2/ParseException.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
src/Sdk/DTExpressions2/Expressions2/ParseExceptionKind.cs
Normal file
14
src/Sdk/DTExpressions2/Expressions2/ParseExceptionKind.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace GitHub.DistributedTask.Expressions2
|
||||
{
|
||||
internal enum ParseExceptionKind
|
||||
{
|
||||
ExceededMaxDepth,
|
||||
ExceededMaxLength,
|
||||
TooFewParameters,
|
||||
TooManyParameters,
|
||||
UnexpectedEndOfExpression,
|
||||
UnexpectedSymbol,
|
||||
UnrecognizedFunction,
|
||||
UnrecognizedNamedValue,
|
||||
}
|
||||
}
|
||||
20
src/Sdk/DTExpressions2/Expressions2/Sdk/Container.cs
Normal file
20
src/Sdk/DTExpressions2/Expressions2/Sdk/Container.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
78
src/Sdk/DTExpressions2/Expressions2/Sdk/EvaluationContext.cs
Normal file
78
src/Sdk/DTExpressions2/Expressions2/Sdk/EvaluationContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
112
src/Sdk/DTExpressions2/Expressions2/Sdk/EvaluationMemory.cs
Normal file
112
src/Sdk/DTExpressions2/Expressions2/Sdk/EvaluationMemory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
192
src/Sdk/DTExpressions2/Expressions2/Sdk/ExpressionNode.cs
Normal file
192
src/Sdk/DTExpressions2/Expressions2/Sdk/ExpressionNode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
265
src/Sdk/DTExpressions2/Expressions2/Sdk/ExpressionUtility.cs
Normal file
265
src/Sdk/DTExpressions2/Expressions2/Sdk/ExpressionUtility.cs
Normal 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("'", "''");
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/Sdk/DTExpressions2/Expressions2/Sdk/Function.cs
Normal file
45
src/Sdk/DTExpressions2/Expressions2/Sdk/Function.cs
Normal 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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
298
src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/Format.cs
Normal file
298
src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/Format.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
74
src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/Join.cs
Normal file
74
src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/Join.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
390
src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/ToJson.cs
Normal file
390
src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/ToJson.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/Sdk/DTExpressions2/Expressions2/Sdk/IBoolean.cs
Normal file
11
src/Sdk/DTExpressions2/Expressions2/Sdk/IBoolean.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IBoolean
|
||||
{
|
||||
Boolean GetBoolean();
|
||||
}
|
||||
}
|
||||
9
src/Sdk/DTExpressions2/Expressions2/Sdk/INull.cs
Normal file
9
src/Sdk/DTExpressions2/Expressions2/Sdk/INull.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface INull
|
||||
{
|
||||
}
|
||||
}
|
||||
11
src/Sdk/DTExpressions2/Expressions2/Sdk/INumber.cs
Normal file
11
src/Sdk/DTExpressions2/Expressions2/Sdk/INumber.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface INumber
|
||||
{
|
||||
Double GetNumber();
|
||||
}
|
||||
}
|
||||
16
src/Sdk/DTExpressions2/Expressions2/Sdk/IReadOnlyArray.cs
Normal file
16
src/Sdk/DTExpressions2/Expressions2/Sdk/IReadOnlyArray.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
27
src/Sdk/DTExpressions2/Expressions2/Sdk/IReadOnlyObject.cs
Normal file
27
src/Sdk/DTExpressions2/Expressions2/Sdk/IReadOnlyObject.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
src/Sdk/DTExpressions2/Expressions2/Sdk/IString.cs
Normal file
11
src/Sdk/DTExpressions2/Expressions2/Sdk/IString.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IString
|
||||
{
|
||||
String GetString();
|
||||
}
|
||||
}
|
||||
43
src/Sdk/DTExpressions2/Expressions2/Sdk/Literal.cs
Normal file
43
src/Sdk/DTExpressions2/Expressions2/Sdk/Literal.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
94
src/Sdk/DTExpressions2/Expressions2/Sdk/MemoryCounter.cs
Normal file
94
src/Sdk/DTExpressions2/Expressions2/Sdk/MemoryCounter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
24
src/Sdk/DTExpressions2/Expressions2/Sdk/NamedValue.cs
Normal file
24
src/Sdk/DTExpressions2/Expressions2/Sdk/NamedValue.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/And.cs
Normal file
51
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/And.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Equal.cs
Normal file
44
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Equal.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
286
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Index.cs
Normal file
286
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Index.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Not.cs
Normal file
41
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Not.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Or.cs
Normal file
51
src/Sdk/DTExpressions2/Expressions2/Sdk/Operators/Or.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Sdk/DTExpressions2/Expressions2/Sdk/ResultMemory.cs
Normal file
58
src/Sdk/DTExpressions2/Expressions2/Sdk/ResultMemory.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
32
src/Sdk/DTExpressions2/Expressions2/Sdk/Wildcard.cs
Normal file
32
src/Sdk/DTExpressions2/Expressions2/Sdk/Wildcard.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace GitHub.DistributedTask.Expressions2.Tokens
|
||||
{
|
||||
internal enum Associativity
|
||||
{
|
||||
None,
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
}
|
||||
}
|
||||
491
src/Sdk/DTExpressions2/Expressions2/Tokens/LexicalAnalyzer.cs
Normal file
491
src/Sdk/DTExpressions2/Expressions2/Tokens/LexicalAnalyzer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
209
src/Sdk/DTExpressions2/Expressions2/Tokens/Token.cs
Normal file
209
src/Sdk/DTExpressions2/Expressions2/Tokens/Token.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Sdk/DTExpressions2/Expressions2/Tokens/TokenKind.cs
Normal file
28
src/Sdk/DTExpressions2/Expressions2/Tokens/TokenKind.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
15
src/Sdk/DTExpressions2/Expressions2/ValueKind.cs
Normal file
15
src/Sdk/DTExpressions2/Expressions2/ValueKind.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Expressions2
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public enum ValueKind
|
||||
{
|
||||
Array,
|
||||
Boolean,
|
||||
Null,
|
||||
Number,
|
||||
Object,
|
||||
String,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user