Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return a transformed string literal from ARM transformation functions invoked with arguments that are all string literals #6765

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
45 changes: 44 additions & 1 deletion src/Bicep.Core.UnitTests/TypeSystem/FunctionResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@ public void IncorrectArgumentTypeShouldSetArgumentCountMismatches(string display
}
}

[DataTestMethod]
[DynamicData(nameof(GetStringLiteralTransformations), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetDisplayName))]
public void StringLiteralTransformationsYieldStringLiteralReturnType(string displayName, string functionName, string[] argumentTypeLiterals, string returnTypeLiteral)
{
var arguments = argumentTypeLiterals.Select(atl => new FunctionArgumentSyntax(TestSyntaxFactory.CreateString(atl), default)).ToList();
var argumentTypes = argumentTypeLiterals.Select(atl => new StringLiteralType(atl) as TypeSymbol).ToList();

var matches = GetMatches(functionName, argumentTypes, out _, out _);
matches.Should().HaveCount(1);

var returnType = matches.Single().ReturnTypeBuilder(
Repository.Create<IBinder>().Object,
Repository.Create<IFileResolver>().Object,
Repository.Create<IDiagnosticWriter>().Object,
arguments.ToImmutableArray(),
argumentTypes.ToImmutableArray()
);
returnType.Should().BeAssignableTo<StringLiteralType>().Subject.RawStringValue.Should().Be(returnTypeLiteral);
}

private static IEnumerable<object[]> GetStringLiteralTransformations()
{
object[] CreateRow(string returnedLiteral, string functionName, params string[] argumentLiterals)
{
string displayName = $@"{functionName}({string.Join(", ", argumentLiterals.Select(l => $@"""{l}"""))}): ""{returnedLiteral}""";
return new object[] { displayName, functionName, argumentLiterals, returnedLiteral };
}

yield return CreateRow("IEZpenog", "base64", " Fizz ");
yield return CreateRow(" Fizz ", "base64ToString", "IEZpenog");
yield return CreateRow("data:text/plain;charset=utf8;base64,IEZpenog", "dataUri", " Fizz ");
yield return CreateRow(" Fizz ", "dataUriToString", "data:text/plain;charset=utf-8;base64,IEZpenog");
yield return CreateRow("F", "first", "Fizz");
yield return CreateRow("z", "last", "Fizz");
yield return CreateRow(" fizz ", "toLower", " Fizz ");
yield return CreateRow(" FIZZ ", "toUpper", " Fizz ");
yield return CreateRow("Fizz", "trim", " Fizz ");
yield return CreateRow("%20Fizz%20", "uriComponent", " Fizz ");
yield return CreateRow(" Fizz ", "uriComponentToString", "%20Fizz%20");
yield return CreateRow("byghxckddilkc", "uniqueString", "snap", "crackle", "pop");
yield return CreateRow("2ed86837-7c7c-5eaa-9864-dd077fd19b0d", "guid", "foo", "bar", "baz");
yield return CreateRow("food", "replace", "foot", "t", "d");
}

public static string GetDisplayName(MethodInfo method, object[] row)
{
row.Length.Should().BeGreaterThan(0);
Expand Down Expand Up @@ -254,4 +298,3 @@ private IEnumerable<FunctionOverload> GetMatches(
}
}
}

10 changes: 8 additions & 2 deletions src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1376,12 +1376,18 @@ public ErrorDiagnostic UnknownModuleReferenceScheme(string badScheme, ImmutableA
TextSpan,
"BCP232",
$"Unable to delete the module with reference \"{moduleRef}\" from cache.");

public ErrorDiagnostic ModuleDeleteFailedWithMessage(string moduleRef, string message) => new(
TextSpan,
"BCP233",
$"Unable to delete the module with reference \"{moduleRef}\" from cache: {message}");


public Diagnostic ArmFunctionLiteralTypeConversionFailedWithMessage(string literalValue, string armFunctionName, string message) => new(
TextSpan,
DiagnosticLevel.Warning,
"BCP234",
$"The ARM function \"{armFunctionName}\" failed when invoked on the value [{literalValue}]: {message}");

}

public static DiagnosticBuilderInternal ForPosition(TextSpan span)
Expand Down
56 changes: 42 additions & 14 deletions src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Azure.Deployments.Expression.Expressions;
using Bicep.Core.Diagnostics;
using Bicep.Core.Extensions;
using Bicep.Core.FileSystem;
using Bicep.Core.Modules;
using Bicep.Core.Parsing;
using Bicep.Core.Syntax;
using Bicep.Core.TypeSystem;
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
Expand Down Expand Up @@ -71,7 +73,7 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("base64")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("base64"), LanguageConstants.String)
.WithGenericDescription("Returns the base64 representation of the input string.")
.WithRequiredParameter("inputString", LanguageConstants.String, "The value to return as a base64 representation.")
.Build(),
Expand All @@ -85,21 +87,21 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("replace")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("replace"), LanguageConstants.String)
.WithGenericDescription("Returns a new string with all instances of one string replaced by another string.")
.WithRequiredParameter("originalString", LanguageConstants.String, "The original string.")
.WithRequiredParameter("oldString", LanguageConstants.String, "The string to be removed from the original string.")
.WithRequiredParameter("newString", LanguageConstants.String, "The string to add in place of the removed string.")
.Build(),

new FunctionOverloadBuilder("toLower")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("toLower"), LanguageConstants.String)
.WithGenericDescription("Converts the specified string to lower case.")
.WithRequiredParameter("stringToChange", LanguageConstants.String, "The value to convert to lower case.")
.Build(),

new FunctionOverloadBuilder("toUpper")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("toUpper"), LanguageConstants.String)
.WithGenericDescription("Converts the specified string to upper case.")
.WithRequiredParameter("stringToChange", LanguageConstants.String, "The value to convert to upper case.")
.Build(),
Expand Down Expand Up @@ -130,19 +132,19 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("uniqueString")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("uniqueString"), LanguageConstants.String)
.WithGenericDescription("Creates a deterministic hash string based on the values provided as parameters.")
.WithVariableParameter("arg", LanguageConstants.String, minimumCount: 1, "The value used in the hash function to create a unique string.")
.Build(),

new FunctionOverloadBuilder("guid")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("guid"), LanguageConstants.String)
.WithGenericDescription("Creates a value in the format of a globally unique identifier based on the values provided as parameters.")
.WithVariableParameter("arg", LanguageConstants.String, minimumCount: 1, "The value used in the hash function to create the GUID.")
.Build(),

new FunctionOverloadBuilder("trim")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("trim"), LanguageConstants.String)
.WithGenericDescription("Removes all leading and trailing white-space characters from the specified string.")
.WithRequiredParameter("stringToTrim", LanguageConstants.String, "The value to trim.")
.Build(),
Expand Down Expand Up @@ -261,7 +263,7 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("first")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("first"), LanguageConstants.String)
.WithGenericDescription(firstDescription)
.WithDescription("Returns the first character of the string.")
.WithRequiredParameter("string", LanguageConstants.String, "The value to retrieve the first character.")
Expand All @@ -275,7 +277,7 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("last")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("last"), LanguageConstants.String)
.WithGenericDescription(lastDescription)
.WithDescription("Returns the last character of the string.")
.WithRequiredParameter("string", LanguageConstants.String, "The value to retrieve the last character.")
Expand Down Expand Up @@ -365,7 +367,7 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("base64ToString")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("base64ToString"), LanguageConstants.String)
.WithGenericDescription("Converts a base64 representation to a string.")
.WithRequiredParameter("base64Value", LanguageConstants.String, "The base64 representation to convert to a string.")
.Build(),
Expand All @@ -377,26 +379,26 @@ public static class SystemNamespaceType
.Build(),

new FunctionOverloadBuilder("uriComponentToString")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("uriComponentToString"), LanguageConstants.String)
.WithGenericDescription("Returns a string of a URI encoded value.")
.WithRequiredParameter("uriEncodedString", LanguageConstants.String, "The URI encoded value to convert to a string.")
.Build(),

new FunctionOverloadBuilder("uriComponent")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("uriComponent"), LanguageConstants.String)
.WithGenericDescription("Encodes a URI.")
.WithRequiredParameter("stringToEncode", LanguageConstants.String, "The value to encode.")
.Build(),

new FunctionOverloadBuilder("dataUriToString")
.WithGenericDescription("Converts a data URI formatted value to a string.")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("dataUriToString"), LanguageConstants.String)
.WithRequiredParameter("dataUriToConvert", LanguageConstants.String, "The data URI value to convert.")
.Build(),

// TODO: Docs have wrong param type and param name (any is actually supported)
new FunctionOverloadBuilder("dataUri")
.WithReturnType(LanguageConstants.String)
.WithDynamicReturnType(PerformArmConversionOfStringLiterals("dataUri"), LanguageConstants.String)
.WithGenericDescription("Converts a value to a data URI.")
.WithRequiredParameter("valueToConvert", LanguageConstants.Any, "The value to convert to a data URI.")
.Build(),
Expand Down Expand Up @@ -513,6 +515,32 @@ public static class SystemNamespaceType
return fileUri;
}

private static FunctionOverload.ReturnTypeBuilderDelegate PerformArmConversionOfStringLiterals(string armFunctionName) =>
(IBinder binder, IFileResolver fileResolver, IDiagnosticWriter diagnostics, ImmutableArray<FunctionArgumentSyntax> arguments, ImmutableArray<TypeSymbol> argumentTypes) =>
{
if (arguments.Length > 0 && argumentTypes.All(s => s is StringLiteralType)) {
var parameters = argumentTypes.OfType<StringLiteralType>().Select(slt => JValue.CreateString(slt.RawStringValue)).ToArray();
try {
if (ExpressionBuiltInFunctions.Functions.EvaluateFunction(armFunctionName, parameters) is JValue jValue && jValue.Value is string stringValue)
{
return new StringLiteralType(stringValue);
}
} catch (Exception e) {
// The ARM function invoked will almost certainly fail at runtime, but there's a chance a fix has been
// deployed to ARM since this version of Bicep was released. Given that context, this failure will only
// be reported as a warning, and the fallback type will be used.
diagnostics.Write(
DiagnosticBuilder.ForPosition(TextSpan.Between(arguments.First().Span, arguments.Last().Span))
.ArmFunctionLiteralTypeConversionFailedWithMessage(
string.Join(", ", parameters.Select(t => t.ToString())),
armFunctionName,
e.Message));
}
}

return LanguageConstants.String;
};

private static TypeSymbol LoadTextContentTypeBuilder(IBinder binder, IFileResolver fileResolver, IDiagnosticWriter diagnostics, ImmutableArray<FunctionArgumentSyntax> arguments, ImmutableArray<TypeSymbol> argumentTypes)
{
if (argumentTypes[0] is not StringLiteralType filePathType)
Expand Down