diff --git a/src/Bicep.Core.IntegrationTests/Scenarios/ScopeTests.cs b/src/Bicep.Core.IntegrationTests/Scenarios/ScopeTests.cs index d3e73ff3dba..16d62e409f7 100644 --- a/src/Bicep.Core.IntegrationTests/Scenarios/ScopeTests.cs +++ b/src/Bicep.Core.IntegrationTests/Scenarios/ScopeTests.cs @@ -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: { diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/cloud-shell-vnet/main.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/cloud-shell-vnet/main.bicep index 85a893c1e5a..2331285f5a0 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/cloud-shell-vnet/main.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/cloud-shell-vnet/main.bicep @@ -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)! } ] } diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/aks.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/aks.bicep index d53b0ea298c..a8280a3f45c 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/aks.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/aks.bicep @@ -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 diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/jumpbox.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/jumpbox.bicep index 55f83b2437b..c1ff8410d69 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/jumpbox.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/private-aks-cluster/jumpbox.bicep @@ -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 diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/vm-windows-with-custom-script-extension/main.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/vm-windows-with-custom-script-extension/main.bicep index 30052b75ccb..dc3faad84d8 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/vm-windows-with-custom-script-extension/main.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/vm-windows-with-custom-script-extension/main.bicep @@ -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' = { diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customimagevm.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customimagevm.bicep index 315afcc9d00..355c039d706 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customimagevm.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customimagevm.bicep @@ -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) diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customvhdvm.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customvhdvm.bicep index 39fbdf05d80..e4d6d7b2b40 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customvhdvm.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-customvhdvm.bicep @@ -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}' diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-galleryvm.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-galleryvm.bicep index 4f8a8de8112..596edee4d8a 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-galleryvm.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/managedDisks-galleryvm.bicep @@ -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) diff --git a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/unmanagedDisks-customvhdvm.bicep b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/unmanagedDisks-customvhdvm.bicep index 60eebb1ce69..5918386853b 100644 --- a/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/unmanagedDisks-customvhdvm.bicep +++ b/src/Bicep.Core.Samples/Files/user_submitted/201/wvd-create-hostpool/modules/unmanagedDisks-customvhdvm.bicep @@ -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}' diff --git a/src/Bicep.Core.UnitTests/TypeSystem/FunctionResolverTests.cs b/src/Bicep.Core.UnitTests/TypeSystem/FunctionResolverTests.cs index eedfb095189..282047d898a 100644 --- a/src/Bicep.Core.UnitTests/TypeSystem/FunctionResolverTests.cs +++ b/src/Bicep.Core.UnitTests/TypeSystem/FunctionResolverTests.cs @@ -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 { inputArrayType }, new[] { new FunctionArgumentSyntax(TestSyntaxFactory.CreateArray(Enumerable.Empty())) }).Type, expected).Should().BeTrue(); + } + + [DataTestMethod] + [DynamicData(nameof(GetLastTestCases), DynamicDataSourceType.Method)] + public void LastReturnsCorrectType(TypeSymbol inputArrayType, TypeSymbol expected) + { + TypeValidator.AreTypesAssignable(EvaluateFunction("last", new List { inputArrayType }, new[] { new FunctionArgumentSyntax(TestSyntaxFactory.CreateArray(Enumerable.Empty())) }).Type, expected).Should().BeTrue(); + } + private FunctionResult EvaluateFunction(string functionName, IList argumentTypes, FunctionArgumentSyntax[] arguments) { var matches = GetMatches(functionName, argumentTypes, out _, out _); @@ -227,6 +241,64 @@ private static IEnumerable GetFlattenNegativeTestCases() => new[] }, }; + private static IEnumerable 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( + new StringLiteralType("test"), + new IntegerLiteralType(3) + ), + default), + new StringLiteralType("test") + }, + // first([resourceGroup, subscription]) => resourceGroup + new object[] { + new TupleType("[resourceGroup, subscription]", + ImmutableArray.Create( + LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup), + LanguageConstants.CreateResourceScopeReference(ResourceScope.Subscription) + ), + default), + LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup) + } + }; + + private static IEnumerable 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( + new StringLiteralType("test"), + new IntegerLiteralType(3) + ), + default), + new IntegerLiteralType(3) + }, + // last([resourceGroup, subscription]) => subscription + new object[] { + new TupleType("[resourceGroup, subscription]", + ImmutableArray.Create( + LanguageConstants.CreateResourceScopeReference(ResourceScope.ResourceGroup), + LanguageConstants.CreateResourceScopeReference(ResourceScope.Subscription) + ), + default), + LanguageConstants.CreateResourceScopeReference(ResourceScope.Subscription) + } + }; + private static IEnumerable GetLiteralTransformations() { FunctionArgumentSyntax ToFunctionArgumentSyntax(object argument) => argument switch @@ -240,7 +312,7 @@ private static IEnumerable 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(), default), @@ -248,10 +320,11 @@ private static IEnumerable GetLiteralTransformations() int[] intArray => new TupleType("", intArray.Select(@int => new IntegerLiteralType(@int)).ToImmutableArray(), default), bool boolVal => new BooleanLiteralType(boolVal), bool[] boolArray => new TupleType("", boolArray.Select(@bool => new BooleanLiteralType(@bool)).ToImmutableArray(), 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(); @@ -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() }), CreateRow("pop", "last", new[] { new[] { "fizz", "buzz", "pop" } }), + CreateRow(null, "last", new[] { Array.Empty() }), 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 }), diff --git a/src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs b/src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs index c8557cf40c7..ea90c1a9347 100644 --- a/src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs +++ b/src/Bicep.Core/Semantics/Namespaces/SystemNamespaceType.cs @@ -280,8 +280,15 @@ private static IEnumerable 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.") @@ -295,8 +302,15 @@ private static IEnumerable 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.")