Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Runner.Worker/ActionManifestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ private TemplateContext CreateTemplateContext(
Schema = _actionManifestSchema,
// TODO: Switch to real tracewriter for cutover
TraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(),
AllowCaseFunction = false,
};

// Expression values from execution context
Expand Down
1 change: 1 addition & 0 deletions src/Runner.Worker/ActionManifestManagerLegacy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ private TemplateContext CreateTemplateContext(
maxBytes: 10 * 1024 * 1024),
Schema = _actionManifestSchema,
TraceWriter = executionContext.ToTemplateTraceWriter(),
AllowCaseFunction = false,
};

// Expression values from execution context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal static class ExpressionConstants
{
static ExpressionConstants()
{
AddFunction<Case>("case", 3, Byte.MaxValue);
AddFunction<Contains>("contains", 2, 2);
AddFunction<EndsWith>("endsWith", 2, 2);
AddFunction<Format>("format", 1, Byte.MaxValue);
Expand Down
20 changes: 17 additions & 3 deletions src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ public IExpressionNode CreateTree(
String expression,
ITraceWriter trace,
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}>");
return CreateTree(context);
}
Expand Down Expand Up @@ -349,6 +350,10 @@ private static void FlushTopEndParameters(ParseContext context)
{
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>
Expand Down Expand Up @@ -411,13 +416,20 @@ private static Boolean TryGetFunctionInfo(
String name,
out IFunctionInfo functionInfo)
{
if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction)
{
functionInfo = null;
return false;
}

return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
context.ExtensionFunctions.TryGetValue(name, out functionInfo);
}

private sealed class ParseContext
{
public Boolean AllowUnknownKeywords;
public Boolean AllowCaseFunction;
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);
Expand All @@ -433,7 +445,8 @@ public ParseContext(
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false)
Boolean allowUnknownKeywords = false,
Boolean allowCaseFunction = true)
{
Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength)
Expand All @@ -454,6 +467,7 @@ public ParseContext(

LexicalAnalyzer = new LexicalAnalyzer(Expression);
AllowUnknownKeywords = allowUnknownKeywords;
AllowCaseFunction = allowCaseFunction;
}

private class NoOperationTraceWriter : ITraceWriter
Expand Down
3 changes: 3 additions & 0 deletions src/Sdk/DTExpressions2/Expressions2/ParseException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal ParseException(ParseExceptionKind kind, Token token, String expression)
case ParseExceptionKind.TooManyParameters:
description = "Too many parameters supplied";
break;
case ParseExceptionKind.EvenParameters:
description = "Even number of parameters supplied, requires an odd number of parameters";
break;
case ParseExceptionKind.UnexpectedEndOfExpression:
description = "Unexpected end of expression";
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal enum ParseExceptionKind
ExceededMaxLength,
TooFewParameters,
TooManyParameters,
EvenParameters,
UnexpectedEndOfExpression,
UnexpectedSymbol,
UnrecognizedFunction,
Expand Down
45 changes: 45 additions & 0 deletions src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/Case.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ internal IDictionary<String, Object> State

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
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected StringToken EvaluateStringToken(
var originalBytes = context.Memory.CurrentBytes;
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);
Copy link
Copy Markdown
Collaborator

@ericsciple ericsciple Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this for the evaluator? Do we hardcode to true for the evaluator?

Basically I just want to make sure we set to true for evaluation. If evaluation works E2E then 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, this is defaulted to true but will allow the manifest manager code paths to disable it.

var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
Expand Down Expand Up @@ -94,7 +94,7 @@ protected SequenceToken EvaluateSequenceToken(
var originalBytes = context.Memory.CurrentBytes;
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
{
MaxMemory = context.Memory.MaxBytes,
Expand Down Expand Up @@ -123,7 +123,7 @@ protected MappingToken EvaluateMappingToken(
var originalBytes = context.Memory.CurrentBytes;
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
{
MaxMemory = context.Memory.MaxBytes,
Expand Down Expand Up @@ -152,7 +152,7 @@ protected TemplateToken EvaluateTemplateToken(
var originalBytes = context.Memory.CurrentBytes;
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
{
MaxMemory = context.Memory.MaxBytes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ private static String ConvertToIfCondition(
var node = default(ExpressionNode);
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)
{
Expand Down
1 change: 1 addition & 0 deletions src/Sdk/Expressions/ExpressionConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static class ExpressionConstants
{
static ExpressionConstants()
{
AddFunction<Case>("case", 3, Byte.MaxValue);
AddFunction<Contains>("contains", 2, 2);
AddFunction<EndsWith>("endsWith", 2, 2);
AddFunction<Format>("format", 1, Byte.MaxValue);
Expand Down
22 changes: 18 additions & 4 deletions src/Sdk/Expressions/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ public IExpressionNode CreateTree(
String expression,
ITraceWriter trace,
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: allowCaseFunction);
context.Trace.Info($"Parsing expression: <{expression}>");
return CreateTree(context);
}
Expand Down Expand Up @@ -349,6 +350,10 @@ private static void FlushTopEndParameters(ParseContext context)
{
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>
Expand Down Expand Up @@ -411,13 +416,20 @@ private static Boolean TryGetFunctionInfo(
String name,
out IFunctionInfo functionInfo)
{
if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction)
{
functionInfo = null;
return false;
}

return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
context.ExtensionFunctions.TryGetValue(name, out functionInfo);
}

private sealed class ParseContext
{
public Boolean AllowUnknownKeywords;
public Boolean AllowCaseFunction;
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);
Expand All @@ -433,7 +445,8 @@ public ParseContext(
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false)
Boolean allowUnknownKeywords = false,
Boolean allowCaseFunction = true)
{
Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength)
Expand All @@ -454,6 +467,7 @@ public ParseContext(

LexicalAnalyzer = new LexicalAnalyzer(Expression);
AllowUnknownKeywords = allowUnknownKeywords;
AllowCaseFunction = allowCaseFunction;
}

private class NoOperationTraceWriter : ITraceWriter
Expand All @@ -468,4 +482,4 @@ public void Verbose(String message)
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/Sdk/Expressions/ParseException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal ParseException(ParseExceptionKind kind, Token token, String expression)
case ParseExceptionKind.TooManyParameters:
description = "Too many parameters supplied";
break;
case ParseExceptionKind.EvenParameters:
description = "Even number of parameters supplied, requires an odd number of parameters";
break;
case ParseExceptionKind.UnexpectedEndOfExpression:
description = "Unexpected end of expression";
break;
Expand Down
1 change: 1 addition & 0 deletions src/Sdk/Expressions/ParseExceptionKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal enum ParseExceptionKind
ExceededMaxLength,
TooFewParameters,
TooManyParameters,
EvenParameters,
UnexpectedEndOfExpression,
UnexpectedSymbol,
UnrecognizedFunction,
Expand Down
45 changes: 45 additions & 0 deletions src/Sdk/Expressions/Sdk/Functions/Case.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1775,7 +1775,7 @@ private static BasicExpressionToken ConvertToIfCondition(
var node = default(ExpressionNode);
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)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Sdk/WorkflowParser/ObjectTemplating/TemplateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public IDictionary<String, Object> State
/// </summary>
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; }

private IDictionary<String, Int32> FileIds
Expand Down
Loading
Loading