private LanguageExpression InlineVariablesRecursive(LanguageExpression original) { if (original is not FunctionExpression functionExpression) { return(original); } if (functionExpression.Function == "variables" && functionExpression.Parameters.Length == 1 && functionExpression.Parameters[0] is JTokenExpression variableNameExpression) { var variableVal = template["variables"]?[variableNameExpression.Value.ToString()]; if (variableVal == null) { throw new ArgumentException($"Unable to resolve variable {variableNameExpression.Value}"); } if (variableVal.Type == JTokenType.String && variableVal.ToObject <string>() is string stringValue) { var variableExpression = ExpressionHelpers.ParseExpression(stringValue); return(InlineVariablesRecursive(variableExpression)); } } var inlinedParameters = functionExpression.Parameters.Select(p => InlineVariablesRecursive(p)); return(new FunctionExpression( functionExpression.Function, inlinedParameters.ToArray(), functionExpression.Properties)); }
public void EmitLanguageExpression(SyntaxBase syntax) { var symbol = context.SemanticModel.GetSymbolInfo(syntax); if (symbol is VariableSymbol variableSymbol && context.VariablesToInline.Contains(variableSymbol)) { EmitExpression(variableSymbol.Value); return; } LanguageExpression converted = converter.ConvertExpression(syntax); if (converted is JTokenExpression valueExpression && valueExpression.Value.Type == JTokenType.Integer) { // the converted expression is an integer literal JToken value = valueExpression.Value; // for integer literals the expression will look like "[42]" or "[-12]" // while it's still a valid template expression that works in ARM, it looks weird // and is also not recognized by the template language service in VS code // let's serialize it as a proper integer instead writer.WriteValue(value); return; } // strings literals and other expressions must be processed with the serializer to ensure correct conversion and escaping var serialized = ExpressionSerializer.SerializeExpression(converted); writer.WriteValue(serialized); }
private LanguageExpression ConvertString(StringSyntax syntax) { if (syntax.TryGetLiteralValue() is string literalStringValue) { // no need to build a format string return(new JTokenExpression(literalStringValue));; } if (syntax.Expressions.Length == 1) { const string emptyStringOpen = LanguageConstants.StringDelimiter + LanguageConstants.StringHoleOpen; // '${ const string emptyStringClose = LanguageConstants.StringHoleClose + LanguageConstants.StringDelimiter; // }' // Special-case interpolation of format '${myValue}' because it's a common pattern for userAssignedIdentities. // There's no need for a 'format' function because we just have a single expression with no outer formatting. if (syntax.StringTokens[0].Text == emptyStringOpen && syntax.StringTokens[1].Text == emptyStringClose) { return(ConvertExpression(syntax.Expressions[0])); } } var formatArgs = new LanguageExpression[syntax.Expressions.Length + 1]; var formatString = StringFormatConverter.BuildFormatString(syntax); formatArgs[0] = new JTokenExpression(formatString); for (var i = 0; i < syntax.Expressions.Length; i++) { formatArgs[i + 1] = ConvertExpression(syntax.Expressions[i]); } return(CreateFunction("format", formatArgs)); }
private LanguageExpression ConvertBinary(BinaryOperationSyntax syntax) { LanguageExpression operand1 = ConvertExpression(syntax.LeftExpression); LanguageExpression operand2 = ConvertExpression(syntax.RightExpression); return(syntax.Operator switch { BinaryOperator.LogicalOr => CreateFunction("or", operand1, operand2), BinaryOperator.LogicalAnd => CreateFunction("and", operand1, operand2), BinaryOperator.Equals => CreateFunction("equals", operand1, operand2), BinaryOperator.NotEquals => CreateFunction("not", CreateFunction("equals", operand1, operand2)), BinaryOperator.EqualsInsensitive => CreateFunction("equals", CreateFunction("toLower", operand1), CreateFunction("toLower", operand2)), BinaryOperator.NotEqualsInsensitive => CreateFunction("not", CreateFunction("equals", CreateFunction("toLower", operand1), CreateFunction("toLower", operand2))), BinaryOperator.LessThan => CreateFunction("less", operand1, operand2), BinaryOperator.LessThanOrEqual => CreateFunction("lessOrEquals", operand1, operand2), BinaryOperator.GreaterThan => CreateFunction("greater", operand1, operand2), BinaryOperator.GreaterThanOrEqual => CreateFunction("greaterOrEquals", operand1, operand2), BinaryOperator.Add => CreateFunction("add", operand1, operand2), BinaryOperator.Subtract => CreateFunction("sub", operand1, operand2), BinaryOperator.Multiply => CreateFunction("mul", operand1, operand2), BinaryOperator.Divide => CreateFunction("div", operand1, operand2), BinaryOperator.Modulo => CreateFunction("mod", operand1, operand2), BinaryOperator.Coalesce => CreateFunction("coalesce", operand1, operand2), _ => throw new NotImplementedException($"Cannot emit unexpected binary operator '{syntax.Operator}'."), });
public static LanguageExpression GenerateResourceGroupScope(LanguageExpression subscriptionId, LanguageExpression resourceGroup) => new FunctionExpression("format", new LanguageExpression[] { new JTokenExpression("/subscriptions/{0}/resourceGroups/{1}"), subscriptionId, resourceGroup, }, new LanguageExpression[0]);
/// <summary> /// Writes the serialized expression to the buffer. /// </summary> /// <param name="buffer">The buffer</param> /// <param name="expression">The expression</param> /// <param name="prefix">The optional prefix</param> /// <param name="suffix">The optional suffix</param> private static void WriteExpressionToBuffer(StringBuilder buffer, LanguageExpression expression, string prefix = null, string suffix = null) { ExpressionSerializer.ValidateExpression(expression: expression); if (prefix != null) { buffer.Append(value: prefix); } switch (expression) { case FunctionExpression functionExpression: ExpressionSerializer.WriteFunctionExpressionToBuffer(buffer: buffer, functionExpression: functionExpression); break; case JTokenExpression valueExpression: ExpressionSerializer.WriteValueExpressionToBuffer(buffer: buffer, valueExpression: valueExpression); break; default: throw new InvalidOperationException($"Unexpected expression type '{expression.GetType().Name}'"); } if (suffix != null) { buffer.Append(value: suffix); } }
/// <summary> /// Validates that the specified expression is not null. Does not perform recursive validation. /// </summary> /// <param name="expression">The expression</param> private static void ValidateExpression(LanguageExpression expression) { if (expression == null) { throw new ArgumentNullException(nameof(expression)); } }
private LanguageExpression ConvertUnary(UnaryOperationSyntax syntax) { LanguageExpression convertedOperand = ConvertExpression(syntax.Expression); switch (syntax.Operator) { case UnaryOperator.Not: return(CreateFunction("not", convertedOperand)); case UnaryOperator.Minus: if (convertedOperand is JTokenExpression literal && literal.Value.Type == JTokenType.Integer) { // invert the integer literal int literalValue = literal.Value.Value <int>(); return(new JTokenExpression(-literalValue)); } return(CreateFunction( "sub", new JTokenExpression(0), convertedOperand)); default: throw new NotImplementedException($"Cannot emit unexpected unary operator '{syntax.Operator}."); } }
public string?TryRequestResourceName(string typeString, LanguageExpression nameExpression) { // it's valid to include a trailing slash, so we need to normalize it typeString = typeString.TrimEnd('/'); var assignedResourceKey = GetResourceNameKey(typeString, nameExpression); var nameString = GetNameRecursive(nameExpression); // try to get a shorter name first if possible // if we've got two resources of different types with the same name, we may be forced to qualify it var unqualifiedName = TryRequestName(NameType.Resource, nameString); if (unqualifiedName != null) { assignedResourceNames[assignedResourceKey] = unqualifiedName; return(unqualifiedName); } var qualifiedName = TryRequestName(NameType.Resource, $"{typeString}_{nameString}"); if (qualifiedName != null) { assignedResourceNames[assignedResourceKey] = qualifiedName; return(qualifiedName); } return(null); }
public string?TryLookupResourceName(string?typeString, LanguageExpression nameExpression) { if (typeString is null) { var nameString = ExpressionsEngine.SerializeExpression(nameExpression); var resourceKeySuffix = EscapeIdentifier($"_{nameString}"); var matchingResources = assignedResourceNames.Where(kvp => kvp.Key.EndsWith(resourceKeySuffix, StringComparison.OrdinalIgnoreCase)); if (matchingResources.Count() == 1) { // only return a value if we're sure about the match return(matchingResources.First().Value); } return(null); } // it's valid to include a trailing slash, so we need to normalize it typeString = typeString.TrimEnd('/'); var assignedResourceKey = GetResourceNameKey(typeString, nameExpression); if (!assignedResourceNames.TryGetValue(assignedResourceKey, out var name)) { return(null); } return(name); }
public static string? TryGetLocalFilePathForTemplateLink(LanguageExpression templateLinkExpression) { LanguageExpression? relativePath = null; if (templateLinkExpression is FunctionExpression uriExpression && uriExpression.Function == "uri") { // it's common to format references to files using the uri function. the second param is the relative path (which should match the file system path) relativePath = uriExpression.Parameters[1]; }
public static LanguageExpression GetManagementGroupScopeExpression(LanguageExpression managementGroupName) => new FunctionExpression( "tenantResourceId", new LanguageExpression[] { new JTokenExpression("Microsoft.Management/managementGroups"), managementGroupName, }, Array.Empty <LanguageExpression>());
private void EmitPropertyInternal(LanguageExpression expressionKey, Action valueFunc) { var serializedName = ExpressionSerializer.SerializeExpression(expressionKey); writer.WritePropertyName(serializedName); valueFunc(); }
private static LanguageExpression GetManagementGroupScopeExpression(LanguageExpression managementGroupName) => new FunctionExpression( "format", new LanguageExpression[] { new JTokenExpression("Microsoft.Management/managementGroups/{0}"), managementGroupName, }, Array.Empty <LanguageExpression>());
private string?TryLookupResource(LanguageExpression expression) { var normalizedForm = ExpressionHelpers.TryGetResourceNormalizedForm(expression); if (normalizedForm is null) { // try to look it up without the type string, using the expression as-is as the name // it's fairly common to refer to another resource by name only return(nameResolver.TryLookupResourceName(null, expression)); } return(nameResolver.TryLookupResourceName(normalizedForm.Value.typeString, normalizedForm.Value.nameExpression)); }
/// <summary> /// Writes the specified property expression to the buffer. /// </summary> /// <param name="buffer">The buffer</param> /// <param name="propertyExpression">The property expression</param> private static void WritePropertyExpressionToBuffer(StringBuilder buffer, LanguageExpression propertyExpression) { ExpressionSerializer.ValidateExpression(expression: propertyExpression); switch (propertyExpression) { case FunctionExpression functionExpression: // Note(majastrz): Functions in properties should be enclosed in brackets ExpressionSerializer.WriteExpressionToBuffer(buffer: buffer, expression: functionExpression, prefix: "[", suffix: "]"); return; case JTokenExpression valueExpression: var value = ExpressionSerializer.GetValueFromValueExpression(valueExpression: valueExpression); switch (value.Type) { case JTokenType.String: var valueStr = value.ToString(); if (ExpressionSerializer.IsIdentifier(value: valueStr)) { // Note(majastrz): The property expression is an identifier. We can serialize it with a leading dot and without any enclosing quotes. buffer.Append(value: '.'); buffer.Append(value: valueStr); } else { // Note(majastrz): The property expression has to be enclosed in brackets because it's not an identifier. ExpressionSerializer.WriteExpressionToBuffer(buffer: buffer, expression: valueExpression, prefix: "[", suffix: "]"); } return; case JTokenType.Integer: // Note(majastrz): Indexes should be enclosed in brackets. buffer.Append(value: '['); buffer.Append(value: value); buffer.Append(value: ']'); return; default: // Note(majastrz): JTokenValue can only be created with string or int value. throw new InvalidOperationException(message: $"JTokenExpression has a value of unexpected type '{value.Type}'."); } default: throw new InvalidOperationException($"Unexpected expression type '{propertyExpression.GetType().Name}'"); } }
private static string GetNameRecursive(LanguageExpression expression) { if (expression is JTokenExpression jTokenExpression) { return(jTokenExpression.Value.ToString()); } if (expression is FunctionExpression functionExpression) { var subExpressions = functionExpression.Parameters.Concat(functionExpression.Properties); return(string.Join('_', subExpressions.Select(GetNameRecursive))); } return("_"); }
private static (bool hasChanges, LanguageExpression[] output) RewriteInternal(LanguageExpression[] input, Func <LanguageExpression, LanguageExpression> rewriteFunc) { var output = new LanguageExpression[input.Length]; var hasChanges = false; for (var i = 0; i < input.Length; i++) { var(entryHasChanges, entryOutput) = RewriteInternal(input[i], rewriteFunc); hasChanges |= entryHasChanges; output[i] = entryOutput; } return(hasChanges, output); }
private static (bool hasChanges, LanguageExpression output) RewriteInternal(LanguageExpression input, Func <LanguageExpression, LanguageExpression> rewriteFunc) { switch (input) { case FunctionExpression function: { var hasChanges = false; var newParameters = function.Parameters; if (function.Parameters is not null) { var(hasChangesInner, output) = RewriteInternal(function.Parameters, rewriteFunc); hasChanges |= hasChangesInner; newParameters = output; } var newProperties = function.Properties; if (function.Properties is not null) { var(hasChangesInner, output) = RewriteInternal(function.Properties, rewriteFunc); hasChanges |= hasChangesInner; newProperties = output; } if (hasChanges) { function = new FunctionExpression(function.Function, newParameters, newProperties); } var rewritten = rewriteFunc(function); hasChanges |= !object.ReferenceEquals(function, rewritten); return(hasChanges, rewritten); } case JTokenExpression jtoken: { var rewritten = rewriteFunc(jtoken); var hasChanges = !object.ReferenceEquals(jtoken, rewritten); return(hasChanges, rewritten); } default: throw new NotImplementedException($"Unrecognized expression type {input.GetType()}"); } }
/// <summary> /// Serializes a language expression into a string. /// </summary> /// <param name="expression">The expression</param> public string SerializeExpression(LanguageExpression expression) { var buffer = new StringBuilder(); if (!ExpressionSerializer.TryWriteSingleStringToBuffer(buffer: buffer, expression: expression, settings: this.settings)) { // Note(majastrz): The expression is not a single string expression or the single string serialization is disabled // Note(majastrz): Include outer brackets when serializing the expression if feature is enabled ExpressionSerializer.WriteExpressionToBuffer( buffer: buffer, expression: expression, prefix: this.settings.IncludeOuterSquareBrackets ? "[" : null, suffix: this.settings.IncludeOuterSquareBrackets ? "]" : null); } return(buffer.ToString()); }
public void EmitLanguageExpression(SyntaxBase syntax) { var symbol = context.SemanticModel.GetSymbolInfo(syntax); if (symbol is VariableSymbol variableSymbol && context.VariablesToInline.Contains(variableSymbol)) { EmitExpression(variableSymbol.Value); return; } if (syntax is FunctionCallSyntax functionCall && symbol is FunctionSymbol functionSymbol && string.Equals(functionSymbol.Name, "any", LanguageConstants.IdentifierComparison)) { // the outermost function in the current syntax node is the "any" function // we should emit its argument directly // otherwise, they'd get wrapped in a json() template function call in the converted expression // we have checks for function parameter count mismatch, which should prevent an exception from being thrown EmitExpression(functionCall.Arguments.Single().Expression); return; } LanguageExpression converted = converter.ConvertExpression(syntax); if (converted is JTokenExpression valueExpression && valueExpression.Value.Type == JTokenType.Integer) { // the converted expression is an integer literal JToken value = valueExpression.Value; // for integer literals the expression will look like "[42]" or "[-12]" // while it's still a valid template expression that works in ARM, it looks weird // and is also not recognized by the template language service in VS code // let's serialize it as a proper integer instead writer.WriteValue(value); return; } // strings literals and other expressions must be processed with the serializer to ensure correct conversion and escaping var serialized = ExpressionSerializer.SerializeExpression(converted); writer.WriteValue(serialized); }
private FunctionExpression ConvertObject(ObjectSyntax syntax) { // need keys and values in one array of parameters var parameters = new LanguageExpression[syntax.Properties.Count() * 2]; int index = 0; foreach (var propertySyntax in syntax.Properties) { parameters[index] = new JTokenExpression(propertySyntax.TryGetKeyText()); index++; parameters[index] = ConvertExpression(propertySyntax.Value); index++; } // we are using the createObject() funciton as a proy for an object literal return(GetCreateObjectExpression(parameters)); }
private LanguageExpression ConvertString(StringSyntax syntax) { if (!syntax.IsInterpolated()) { // no need to build a format string return(new JTokenExpression(syntax.GetLiteralValue()));; } var formatArgs = new LanguageExpression[syntax.Expressions.Length + 1]; var formatString = StringFormatConverter.BuildFormatString(syntax); formatArgs[0] = new JTokenExpression(formatString); for (var i = 0; i < syntax.Expressions.Length; i++) { formatArgs[i + 1] = ConvertExpression(syntax.Expressions[i]); } return(new FunctionExpression("format", formatArgs, Array.Empty <LanguageExpression>())); }
/// <summary> /// If the expression is a single string JTokenExpression and serialization as string is enabled, it performs the serialization. /// </summary> /// <param name="buffer">The buffer</param> /// <param name="expression">The expression</param> /// <param name="settings">The settings</param> /// <returns>True if expression has been written out to the buffer or false otherwise.</returns> private static bool TryWriteSingleStringToBuffer(StringBuilder buffer, LanguageExpression expression, ExpressionSerializerSettings settings) { if (settings.SingleStringHandling == ExpressionSerializerSingleStringHandling.SerializeAsString && expression is JTokenExpression valueExpression) { var value = ExpressionSerializer.GetValueFromValueExpression(valueExpression: valueExpression); if (value.Type == JTokenType.String) { var valueStr = value.ToString(); // Note(majastrz): Add escape bracket if needed. if (valueStr.Length > 0 && valueStr[0] == '[') { buffer.Append(value: '['); } buffer.Append(value: valueStr); return(true); } } // Note(majastrz): Returning false REQUIRES that buffer not be modified in any way. return(false); }
private FunctionExpression ConvertObject(ObjectSyntax syntax) { // need keys and values in one array of parameters var parameters = new LanguageExpression[syntax.Properties.Count() * 2]; int index = 0; foreach (var propertySyntax in syntax.Properties) { parameters[index] = propertySyntax.Key switch { IdentifierSyntax identifier => new JTokenExpression(identifier.IdentifierName), StringSyntax @string => ConvertString(@string), _ => throw new NotImplementedException($"Encountered an unexpected type '{propertySyntax.Key.GetType().Name}' when generating object's property name.") }; index++; parameters[index] = ConvertExpression(propertySyntax.Value); index++; } // we are using the createObject() function as a proxy for an object literal return(GetCreateObjectExpression(parameters)); }
private LanguageExpression InlineVariables(LanguageExpression original) { var inlined = InlineVariablesRecursive(original); return(ExpressionHelpers.FlattenStringOperations(inlined)); }
public ExpressionConverter AppendReplacement(LocalVariableSymbol symbol, LanguageExpression replacement) =>
public static LanguageExpression GenerateScopedResourceId(LanguageExpression scope, string fullyQualifiedType, IEnumerable <LanguageExpression> nameSegments) => CreateFunction( "extensionResourceId", new [] { scope, new JTokenExpression(fullyQualifiedType), }.Concat(nameSegments));
private LanguageExpression ConvertBinary(BinaryOperationSyntax syntax) { LanguageExpression operand1 = ConvertExpression(syntax.LeftExpression); LanguageExpression operand2 = ConvertExpression(syntax.RightExpression); switch (syntax.Operator) { case BinaryOperator.LogicalOr: return(CreateFunction("or", operand1, operand2)); case BinaryOperator.LogicalAnd: return(CreateFunction("and", operand1, operand2)); case BinaryOperator.Equals: return(CreateFunction("equals", operand1, operand2)); case BinaryOperator.NotEquals: return(CreateFunction("not", CreateFunction("equals", operand1, operand2))); case BinaryOperator.EqualsInsensitive: return(CreateFunction("equals", CreateFunction("toLower", operand1), CreateFunction("toLower", operand2))); case BinaryOperator.NotEqualsInsensitive: return(CreateFunction("not", CreateFunction("equals", CreateFunction("toLower", operand1), CreateFunction("toLower", operand2)))); case BinaryOperator.LessThan: return(CreateFunction("less", operand1, operand2)); case BinaryOperator.LessThanOrEqual: return(CreateFunction("lessOrEquals", operand1, operand2)); case BinaryOperator.GreaterThan: return(CreateFunction("greater", operand1, operand2)); case BinaryOperator.GreaterThanOrEqual: return(CreateFunction("greaterOrEquals", operand1, operand2)); case BinaryOperator.Add: return(CreateFunction("add", operand1, operand2)); case BinaryOperator.Subtract: return(CreateFunction("sub", operand1, operand2)); case BinaryOperator.Multiply: return(CreateFunction("mul", operand1, operand2)); case BinaryOperator.Divide: return(CreateFunction("div", operand1, operand2)); case BinaryOperator.Modulo: return(CreateFunction("mod", operand1, operand2)); default: throw new NotImplementedException($"Cannot emit unexpected binary operator '{syntax.Operator}'."); } }
private string GetResourceNameKey(string typeString, LanguageExpression nameExpression) { var nameString = ExpressionsEngine.SerializeExpression(nameExpression); return(EscapeIdentifier($"{typeString}_{nameString}")); }