Skip to content
2 changes: 1 addition & 1 deletion src/Bicep.Core.IntegrationTests/Scenarios/ScopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ param postgreSqlServerId string
]

resource postgreSQL 'Microsoft.DBForPostgreSQL/servers@2017-12-01' existing = {
name: last(split(postgreSqlServerId, '/'))
name: last(split(postgreSqlServerId, '/'))!
resource database 'databases' = [for (item, index) in PSQL_DATABASES: {
name: item.database.name
properties: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ resource privateDnsZoneARecord 'Microsoft.Network/privateDnsZones/A@2020-01-01'
ttl: 3600
aRecords: [
{
ipv4Address: first(first(privateEndpoint.properties.customDnsConfigs).ipAddresses)
ipv4Address: first(first(privateEndpoint.properties.customDnsConfigs)!.ipAddresses)!
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ var aadProfileConfiguration = {
tenantID: aadProfileTenantId
}

var virtualNetworkName = last(split(virtualNetworkId, '/'))
var virtualNetworkName = last(split(virtualNetworkId, '/'))!

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2020-08-01' existing = {
name: virtualNetworkName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ var linuxConfiguration = {
provisionVMAgent: true
}

var virtualNetworkName = last(split(virtualNetworkId, '/'))
var logAnalyticsWorkspaceName = last(split(logAnalyticsWorkspaceId, '/'))
var virtualNetworkName = last(split(virtualNetworkId, '/'))!
var logAnalyticsWorkspaceName = last(split(logAnalyticsWorkspaceId, '/'))!

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-10-01' existing = {
name: logAnalyticsWorkspaceName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ var virtualMachineExtensionCustomScript = {
fileUris: [
virtualMachineExtensionCustomScriptUri
]
commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File ./${last(split(virtualMachineExtensionCustomScriptUri, '/'))}'
commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File ./${last(split(virtualMachineExtensionCustomScriptUri, '/'))!}'
}

resource vmext 'Microsoft.Compute/virtualMachines/extensions@2020-06-01' = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ param aadJoin bool = false
param intune bool = false

var emptyArray = []
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@')) : domain)
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@'))! : domain)
var storageAccountType = rdshVMDiskType
var newNsgName = '${rdshPrefix}nsg-${guidValue}'
var nsgId = (createNetworkSecurityGroup ? resourceId('Microsoft.Network/networkSecurityGroups', newNsgName) : networkSecurityGroupId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ param aadJoin bool = false
param intune bool = false

var emptyArray = []
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@')) : domain)
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@'))! : domain)
var storageAccountType = rdshVMDiskType
var imageName_var = '${rdshPrefix}image'
var newNsgName = '${rdshPrefix}nsg-${guidValue}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ param aadJoin bool = false
param intune bool = false

var emptyArray = []
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@')) : domain)
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@'))! : domain)
var storageAccountType = rdshVMDiskType
var newNsgName = '${rdshPrefix}nsg-${guidValue}'
var nsgId = (createNetworkSecurityGroup ? resourceId('Microsoft.Network/networkSecurityGroups', newNsgName) : networkSecurityGroupId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ param aadJoin bool = false
param intune bool = false

var emptyArray = []
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@')) : domain)
var domain_var = ((domain == '') ? last(split(administratorAccountUsername, '@'))! : domain)
var storageAccountName = split(split(vmImageVhdUri, '/')[2], '.')[0]
var storageaccount = concat(resourceId(storageAccountResourceGroupName, 'Microsoft.Storage/storageAccounts', storageAccountName))
var newNsgName = '${rdshPrefix}nsg-${guidValue}'
Expand Down
79 changes: 77 additions & 2 deletions src/Bicep.Core.UnitTests/TypeSystem/FunctionResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ public void ShouldNotFlatten(TypeSymbol typeToFlatten, params string[] diagnosti
.Should().HaveDiagnostics(diagnosticMessages.Select(message => ("BCP309", DiagnosticLevel.Error, message)));
}

[DataTestMethod]
[DynamicData(nameof(GetFirstTestCases), DynamicDataSourceType.Method)]
public void FirstReturnsCorrectType(TypeSymbol inputArrayType, TypeSymbol expected)
{
TypeValidator.AreTypesAssignable(EvaluateFunction("first", new List<TypeSymbol> { inputArrayType }, new[] { new FunctionArgumentSyntax(TestSyntaxFactory.CreateArray(Enumerable.Empty<SyntaxBase>())) }).Type, expected).Should().BeTrue();
}

[DataTestMethod]
[DynamicData(nameof(GetLastTestCases), DynamicDataSourceType.Method)]
public void LastReturnsCorrectType(TypeSymbol inputArrayType, TypeSymbol expected)
{
TypeValidator.AreTypesAssignable(EvaluateFunction("last", new List<TypeSymbol> { inputArrayType }, new[] { new FunctionArgumentSyntax(TestSyntaxFactory.CreateArray(Enumerable.Empty<SyntaxBase>())) }).Type, expected).Should().BeTrue();
}

private FunctionResult EvaluateFunction(string functionName, IList<TypeSymbol> argumentTypes, FunctionArgumentSyntax[] arguments)
{
var matches = GetMatches(functionName, argumentTypes, out _, out _);
Expand Down Expand Up @@ -227,6 +241,64 @@ private static IEnumerable<object[]> GetFlattenNegativeTestCases() => new[]
},
};

private static IEnumerable<object[]> GetFirstTestCases() => new[]
{
// first(resourceGroup[]) -> resourceGroup
new object[] {
new TypedArrayType(LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup), default),
TypeHelper.CreateTypeUnion(LanguageConstants.Null, LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup))
},
// first(['test', 3]) -> 'test'
new object[] {
new TupleType("['test', 3]",
ImmutableArray.Create<ITypeReference>(
new StringLiteralType("test"),
new IntegerLiteralType(3)
),
default),
new StringLiteralType("test")
},
// first([resourceGroup, subscription]) => resourceGroup
new object[] {
new TupleType("[resourceGroup, subscription]",
ImmutableArray.Create<ITypeReference>(
LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup),
LanguageConstants.CreateResourceScopeReference(ResourceScope.Subscription)
),
default),
LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup)
}
};

private static IEnumerable<object[]> GetLastTestCases() => new[]
{
// last(resourceGroup[]) -> resourceGroup
new object[] {
new TypedArrayType(LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup), default),
TypeHelper.CreateTypeUnion(LanguageConstants.Null, LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup))
},
// last(['test', 3]) -> 3
new object[] {
new TupleType("['test', 3]",
ImmutableArray.Create<ITypeReference>(
new StringLiteralType("test"),
new IntegerLiteralType(3)
),
default),
new IntegerLiteralType(3)
},
// last([resourceGroup, subscription]) => subscription
new object[] {
new TupleType("[resourceGroup, subscription]",
ImmutableArray.Create<ITypeReference>(
LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup),
LanguageConstants.CreateResourceScopeReference(ResourceScope.Subscription)
),
default),
LanguageConstants.CreateResourceScopeReference(ResourceScope.Subscription)
}
};

private static IEnumerable<object[]> GetLiteralTransformations()
{
FunctionArgumentSyntax ToFunctionArgumentSyntax(object argument) => argument switch
Expand All @@ -240,18 +312,19 @@ private static IEnumerable<object[]> GetLiteralTransformations()
_ => throw new NotImplementedException($"Unable to transform {argument} to a literal syntax node.")
};

TypeSymbol ToTypeLiteral(object argument) => argument switch
TypeSymbol ToTypeLiteral(object? argument) => argument switch
{
string str => new StringLiteralType(str),
string[] strArray => new TupleType($"[{string.Join(", ", strArray.Select(str => $"'{str}'"))}]", strArray.Select(str => new StringLiteralType(str)).ToImmutableArray<ITypeReference>(), default),
int intVal => new IntegerLiteralType(intVal),
int[] intArray => new TupleType("", intArray.Select(@int => new IntegerLiteralType(@int)).ToImmutableArray<ITypeReference>(), default),
bool boolVal => new BooleanLiteralType(boolVal),
bool[] boolArray => new TupleType("", boolArray.Select(@bool => new BooleanLiteralType(@bool)).ToImmutableArray<ITypeReference>(), default),
null => LanguageConstants.Null,
_ => throw new NotImplementedException($"Unable to transform {argument} to a type literal.")
};

object[] CreateRow(object returnedLiteral, string functionName, params object[] argumentLiterals)
object[] CreateRow(object? returnedLiteral, string functionName, params object[] argumentLiterals)
{
var argumentLiteralSyntaxes = argumentLiterals.Select(ToFunctionArgumentSyntax).ToArray();
var argumentTypeLiterals = argumentLiterals.Select(ToTypeLiteral).ToList();
Expand Down Expand Up @@ -298,7 +371,9 @@ object[] CreateRow(object returnedLiteral, string functionName, params object[]
CreateRow(new[] { "pop" }, "intersection", new[] { "fizz", "buzz", "pop" }, new[] { "snap", "crackle", "pop" }),
CreateRow(new[] { "fizz", "buzz", "pop" }, "union", new[] { "fizz", "buzz" }, new[] { "pop" }),
CreateRow("fizz", "first", new[] { new[] { "fizz", "buzz", "pop" } }),
CreateRow(null, "first", new[] { Array.Empty<string>() }),
CreateRow("pop", "last", new[] { new[] { "fizz", "buzz", "pop" } }),
CreateRow(null, "last", new[] { Array.Empty<string>() }),
CreateRow(0, "indexOf", new[] { "fizz", "buzz", "pop", "fizz" }, "fizz"),
CreateRow(3, "lastIndexOf", new[] { "fizz", "buzz", "pop", "fizz" }, "fizz"),
CreateRow(1, "min", new[] { 10, 4, 1, 6 }),
Expand Down
22 changes: 18 additions & 4 deletions src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,15 @@ private static IEnumerable<FunctionOverload> GetSystemOverloads(IFeatureProvider
.Build();

yield return new FunctionOverloadBuilder("first")
// TODO even with non-literal types, some type arithmetic could be performed
.WithReturnResultBuilder(TryDeriveLiteralReturnType("first", LanguageConstants.Any), LanguageConstants.Any)
.WithReturnResultBuilder((binder, fileResolver, diagnostics, arguments, argumentTypes) =>
{
return new(argumentTypes[0] switch
{
TupleType tupleType => tupleType.Items.FirstOrDefault()?.Type ?? LanguageConstants.Null,
ArrayType arrayType => TypeHelper.CreateTypeUnion(LanguageConstants.Null, arrayType.Item.Type),
_ => LanguageConstants.Any
});
}, LanguageConstants.Any)
.WithGenericDescription(FirstDescription)
.WithDescription("Returns the first element of the array.")
.WithRequiredParameter("array", LanguageConstants.Array, "The value to retrieve the first element.")
Expand All @@ -295,8 +302,15 @@ private static IEnumerable<FunctionOverload> GetSystemOverloads(IFeatureProvider
.Build();

yield return new FunctionOverloadBuilder("last")
// TODO even with non-literal types, some type arithmetic could be performed
.WithReturnResultBuilder(TryDeriveLiteralReturnType("last", LanguageConstants.Any), LanguageConstants.Any)
.WithReturnResultBuilder((binder, fileResolver, diagnostics, arguments, argumentTypes) =>
{
return new(argumentTypes[0] switch
{
TupleType tupleType => tupleType.Items.LastOrDefault()?.Type ?? LanguageConstants.Null,
ArrayType arrayType => TypeHelper.CreateTypeUnion(LanguageConstants.Null, arrayType.Item.Type),
_ => LanguageConstants.Any
});
}, LanguageConstants.Any)
.WithGenericDescription(LastDescription)
.WithDescription("Returns the last element of the array.")
.WithRequiredParameter("array", LanguageConstants.Array, "The value to retrieve the last element.")
Expand Down