Add support for case function

This commit is contained in:
Allan Guigou
2025-12-11 06:59:58 -08:00
parent 1ff3fb78b9
commit 2e17c7868d
15 changed files with 139 additions and 16 deletions

View File

@@ -316,6 +316,7 @@ namespace GitHub.Runner.Worker
Schema = _actionManifestSchema, Schema = _actionManifestSchema,
// TODO: Switch to real tracewriter for cutover // TODO: Switch to real tracewriter for cutover
TraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(), TraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(),
AllowCaseFunction = false,
}; };
// Expression values from execution context // Expression values from execution context

View File

@@ -315,6 +315,7 @@ namespace GitHub.Runner.Worker
maxBytes: 10 * 1024 * 1024), maxBytes: 10 * 1024 * 1024),
Schema = _actionManifestSchema, Schema = _actionManifestSchema,
TraceWriter = executionContext.ToTemplateTraceWriter(), TraceWriter = executionContext.ToTemplateTraceWriter(),
AllowCaseFunction = false,
}; };
// Expression values from execution context // Expression values from execution context

View File

@@ -9,6 +9,7 @@ namespace GitHub.DistributedTask.Expressions2
{ {
static ExpressionConstants() static ExpressionConstants()
{ {
AddFunction<Case>("case", 3, Byte.MaxValue);
AddFunction<Contains>("contains", 2, 2); AddFunction<Contains>("contains", 2, 2);
AddFunction<EndsWith>("endsWith", 2, 2); AddFunction<EndsWith>("endsWith", 2, 2);
AddFunction<Format>("format", 1, Byte.MaxValue); AddFunction<Format>("format", 1, Byte.MaxValue);

View File

@@ -17,9 +17,10 @@ namespace GitHub.DistributedTask.Expressions2
String expression, String expression,
ITraceWriter trace, ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues, IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions) IEnumerable<IFunctionInfo> functions,
Boolean allowCaseFunction = true)
{ {
var context = new ParseContext(expression, trace, namedValues, functions); var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction);
context.Trace.Info($"Parsing expression: <{expression}>"); context.Trace.Info($"Parsing expression: <{expression}>");
return CreateTree(context); return CreateTree(context);
} }
@@ -349,6 +350,10 @@ namespace GitHub.DistributedTask.Expressions2
{ {
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression); throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
} }
else if (functionInfo.Name.Equals("case", StringComparison.OrdinalIgnoreCase) && function.Parameters.Count % 2 == 0)
{
throw new ParseException(ParseExceptionKind.EvenParameters, token: @operator, expression: context.Expression);
}
} }
/// <summary> /// <summary>
@@ -411,6 +416,12 @@ namespace GitHub.DistributedTask.Expressions2
String name, String name,
out IFunctionInfo functionInfo) out IFunctionInfo functionInfo)
{ {
if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction)
{
functionInfo = null;
return false;
}
return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) || return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
context.ExtensionFunctions.TryGetValue(name, out functionInfo); context.ExtensionFunctions.TryGetValue(name, out functionInfo);
} }
@@ -418,6 +429,7 @@ namespace GitHub.DistributedTask.Expressions2
private sealed class ParseContext private sealed class ParseContext
{ {
public Boolean AllowUnknownKeywords; public Boolean AllowUnknownKeywords;
public Boolean AllowCaseFunction;
public readonly String Expression; public readonly String Expression;
public readonly Dictionary<String, IFunctionInfo> ExtensionFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase); 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 Dictionary<String, INamedValueInfo> ExtensionNamedValues = new Dictionary<String, INamedValueInfo>(StringComparer.OrdinalIgnoreCase);
@@ -433,7 +445,8 @@ namespace GitHub.DistributedTask.Expressions2
ITraceWriter trace, ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues, IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions, IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false) Boolean allowUnknownKeywords = false,
Boolean allowCaseFunction = true)
{ {
Expression = expression ?? String.Empty; Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength) if (Expression.Length > ExpressionConstants.MaxLength)
@@ -454,6 +467,7 @@ namespace GitHub.DistributedTask.Expressions2
LexicalAnalyzer = new LexicalAnalyzer(Expression); LexicalAnalyzer = new LexicalAnalyzer(Expression);
AllowUnknownKeywords = allowUnknownKeywords; AllowUnknownKeywords = allowUnknownKeywords;
AllowCaseFunction = allowCaseFunction;
} }
private class NoOperationTraceWriter : ITraceWriter private class NoOperationTraceWriter : ITraceWriter

View File

@@ -29,6 +29,9 @@ namespace GitHub.DistributedTask.Expressions2
case ParseExceptionKind.TooManyParameters: case ParseExceptionKind.TooManyParameters:
description = "Too many parameters supplied"; description = "Too many parameters supplied";
break; break;
case ParseExceptionKind.EvenParameters:
description = "Even number of parameters supplied, requires an odd number of parameters";
break;
case ParseExceptionKind.UnexpectedEndOfExpression: case ParseExceptionKind.UnexpectedEndOfExpression:
description = "Unexpected end of expression"; description = "Unexpected end of expression";
break; break;

View File

@@ -6,6 +6,7 @@
ExceededMaxLength, ExceededMaxLength,
TooFewParameters, TooFewParameters,
TooManyParameters, TooManyParameters,
EvenParameters,
UnexpectedEndOfExpression, UnexpectedEndOfExpression,
UnexpectedSymbol, UnexpectedSymbol,
UnrecognizedFunction, UnrecognizedFunction,

View File

@@ -0,0 +1,45 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.Expressions.Data;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class Case : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
// Validate argument count - must be odd (pairs of predicate-result plus default)
if (Parameters.Count % 2 == 0)
{
throw new InvalidOperationException("case requires an odd number of arguments");
}
// Evaluate predicate-result pairs
for (var i = 0; i < Parameters.Count - 1; i += 2)
{
var predicate = Parameters[i].Evaluate(context);
// Predicate must be a boolean
if (predicate.Kind != ValueKind.Boolean)
{
throw new InvalidOperationException("case predicate must evaluate to a boolean value");
}
// If predicate is true, return the corresponding result
if ((Boolean)predicate.Value)
{
var result = Parameters[i + 1].Evaluate(context);
return result.Value;
}
}
// No predicate matched, return default (last argument)
var defaultResult = Parameters[Parameters.Count - 1].Evaluate(context);
return defaultResult.Value;
}
}
}

View File

@@ -86,6 +86,12 @@ namespace GitHub.DistributedTask.ObjectTemplating
internal ITraceWriter TraceWriter { get; set; } internal ITraceWriter TraceWriter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the case expression function is allowed.
/// Defaults to true. Set to false to disable the case function.
/// </summary>
internal Boolean AllowCaseFunction { get; set; } = true;
private IDictionary<String, Int32> FileIds private IDictionary<String, Int32> FileIds
{ {
get get

View File

@@ -57,7 +57,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,
@@ -94,7 +94,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,
@@ -123,7 +123,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,
@@ -152,7 +152,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,

View File

@@ -663,7 +663,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
var node = default(ExpressionNode); var node = default(ExpressionNode);
try try
{ {
node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode; node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -18,7 +18,7 @@ namespace GitHub.Actions.Expressions
ITraceWriter trace, ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues, IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions, IEnumerable<IFunctionInfo> functions,
Boolean allowCaseFunction = false) Boolean allowCaseFunction = true)
{ {
var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction); var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction);
context.Trace.Info($"Parsing expression: <{expression}>"); context.Trace.Info($"Parsing expression: <{expression}>");
@@ -446,7 +446,7 @@ namespace GitHub.Actions.Expressions
IEnumerable<INamedValueInfo> namedValues, IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions, IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false, Boolean allowUnknownKeywords = false,
Boolean allowCaseFunction = false) Boolean allowCaseFunction = true)
{ {
Expression = expression ?? String.Empty; Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength) if (Expression.Length > ExpressionConstants.MaxLength)

View File

@@ -0,0 +1,45 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.Expressions.Data;
namespace GitHub.Actions.Expressions.Sdk.Functions
{
internal sealed class Case : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
// Validate argument count - must be odd (pairs of predicate-result plus default)
if (Parameters.Count % 2 == 0)
{
throw new InvalidOperationException("case requires an odd number of arguments");
}
// Evaluate predicate-result pairs
for (var i = 0; i < Parameters.Count - 1; i += 2)
{
var predicate = Parameters[i].Evaluate(context);
// Predicate must be a boolean
if (predicate.Kind != ValueKind.Boolean)
{
throw new InvalidOperationException("case predicate must evaluate to a boolean value");
}
// If predicate is true, return the corresponding result
if ((Boolean)predicate.Value)
{
var result = Parameters[i + 1].Evaluate(context);
return result.Value;
}
}
// No predicate matched, return default (last argument)
var defaultResult = Parameters[Parameters.Count - 1].Evaluate(context);
return defaultResult.Value;
}
}
}

View File

@@ -1775,7 +1775,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
var node = default(ExpressionNode); var node = default(ExpressionNode);
try try
{ {
node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode; node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -113,6 +113,12 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating
/// </summary> /// </summary>
internal Boolean StrictJsonParsing { get; set; } internal Boolean StrictJsonParsing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the case expression function is allowed.
/// Defaults to true. Set to false to disable the case function.
/// </summary>
internal Boolean AllowCaseFunction { get; set; } = true;
internal ITraceWriter TraceWriter { get; set; } internal ITraceWriter TraceWriter { get; set; }
private IDictionary<String, Int32> FileIds private IDictionary<String, Int32> FileIds

View File

@@ -55,7 +55,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,
@@ -93,7 +93,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,
@@ -123,7 +123,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,
@@ -153,7 +153,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes; var originalBytes = context.Memory.CurrentBytes;
try try
{ {
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions var options = new EvaluationOptions
{ {
MaxMemory = context.Memory.MaxBytes, MaxMemory = context.Memory.MaxBytes,