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 { /// /// Returns the node and all descendant nodes /// public static IEnumerable 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; } } } } /// /// 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". /// public static Boolean[] CheckReferencesContext( this IExpressionNode tree, params String[] patterns) { var result = new Boolean[patterns.Length]; var segmentedPatterns = default(Stack[]); // Walk the tree var stack = new Stack(); 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 Sdk.Operators.Index) { // Lazy initialize the pattern segments if (segmentedPatterns is null) { segmentedPatterns = new Stack[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[] 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(originalNodeSegments.Reverse()); nodeSegments.Pop(); patternSegments = new Stack(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 GetMatchSegments(IExpressionNode node) { var result = new Stack(); // Node is a named-value if (node is NamedValue) { result.Push(node); } // Node is an index else if (node is Sdk.Operators.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 Sdk.Operators.Index index2) { index = index2; } // Otherwise clear else { result.Clear(); break; } } } return result; } } }