From 6f0fb7d9d5b2a99e79a07bc158288106de67b4be Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 11 Oct 2024 15:30:03 -0700 Subject: [PATCH] Allow extracting param as resource-derived type --- .../ExpressionAndTypeExtractorTests.cs | 316 +++++++++- .../TypeStringifierTests.cs | 542 +++++++++++++++--- .../Refactor/ExpressionAndTypeExtractor.cs | 27 +- .../Refactor/TypeStringifier.cs | 60 +- .../Telemetry/BicepTelemetryEvent.cs | 1 + 5 files changed, 826 insertions(+), 120 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/ExpressionAndTypeExtractorTests.cs b/src/Bicep.LangServer.IntegrationTests/ExpressionAndTypeExtractorTests.cs index cb0c444fef7..fb4878cb795 100644 --- a/src/Bicep.LangServer.IntegrationTests/ExpressionAndTypeExtractorTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/ExpressionAndTypeExtractorTests.cs @@ -140,7 +140,7 @@ param p2 'foo' | 'bar' """)] public async Task BicepDiscussion(string fileWithSelection, string expectedLooseParamText, string expectedMediumParamText) { - await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText); + await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText, "IGNORE"); } //////////////////////////////////////////////////////////////////// @@ -159,7 +159,9 @@ public async Task BicepDiscussion(string fileWithSelection, string expectedLoose param newParameter string = 'b' var a = newParameter - """)] + """, + null, + null)] [DataRow( """ var a = 'a' @@ -177,7 +179,9 @@ public async Task BicepDiscussion(string fileWithSelection, string expectedLoose param newParameter string = 'b' var b = newParameter var c = 'c' - """)] + """, + null, + null)] [DataRow( """ var a = 1 + 2 @@ -193,6 +197,8 @@ public async Task BicepDiscussion(string fileWithSelection, string expectedLoose param newParameter string = '${a}{a}' var b = newParameter """, + null, + null, DisplayName = "Full interpolated string")] [DataRow( """ @@ -217,6 +223,8 @@ public async Task BicepDiscussion(string fileWithSelection, string expectedLoose // comment 2 param a = newParameter """, + null, + null, DisplayName = "Preceding lines")] [DataRow( """ @@ -245,6 +253,8 @@ public async Task BicepDiscussion(string fileWithSelection, string expectedLoose 'c' ] """, + null, + null, DisplayName = "Inside a data structure")] [DataRow( """ @@ -284,10 +294,12 @@ public async Task BicepDiscussion(string fileWithSelection, string expectedLoose name: 'Premium_LRS' } } - """)] - public async Task Basics(string fileWithSelection, string? expectedVarText, string? expectedLooseParamText = null, string? expectedMediumParamText = null) + """, + null, + null)] + public async Task Basics(string fileWithSelection, string? expectedVarText, string? expectedLooseParamText, string? expectedMediumParamText, string? expectedResourceDerivedParamText) { - await RunExtractToVariableAndParameterTest(fileWithSelection, expectedVarText, expectedLooseParamText, expectedMediumParamText); + await RunExtractToVariableAndParameterTest(fileWithSelection, expectedVarText, expectedLooseParamText, expectedMediumParamText, expectedResourceDerivedParamText); } //////////////////////////////////////////////////////////////////// @@ -304,11 +316,12 @@ public async Task Basics(string fileWithSelection, string? expectedVarText, stri var v = newParameter """, + null, null)] [DataTestMethod] - public async Task NullType(string fileWithSelection, string? expectedVarText, string? expectedLooseParamText = null, string? expectedMediumParamText = null) + public async Task NullType(string fileWithSelection, string? expectedVarText, string? expectedLooseParamText, string? expectedMediumParamText, string? expectedResourceDerivedParamText) { - await RunExtractToVariableAndParameterTest(fileWithSelection, expectedVarText, expectedLooseParamText, expectedMediumParamText); + await RunExtractToVariableAndParameterTest(fileWithSelection, expectedVarText, expectedLooseParamText, expectedMediumParamText, expectedResourceDerivedParamText); } //////////////////////////////////////////////////////////////////// @@ -339,9 +352,8 @@ public async Task NullType(string fileWithSelection, string? expectedVarText, st var a = newParameter """)] - public async Task ShouldOfferTwoParameterExtractions_IffTheExtractedTypesAreDifferent(string fileWithSelection, string? expectedLooseParamText, string? expectedMediumParamText) - { - await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText); + public async Task ShouldOfferTwoParameterExtractions_IffTheExtractedTypesAreDifferent(string fileWithSelection, string? expectedLooseParamText, string? expectedMediumParamText) { + await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText, "IGNORE"); } //////////////////////////////////////////////////////////////////// @@ -458,7 +470,7 @@ public async Task ShouldRenameToAvoidConflicts(string fileWithSelection, string [DataTestMethod] public async Task WeirdNames(string fileWithSelection, string expectedText) { - await RunExtractToParameterTest(fileWithSelection, expectedText, "IGNORE"); + await RunExtractToParameterTest(fileWithSelection, expectedText, "IGNORE", "IGNORE"); } //////////////////////////////////////////////////////////////////// @@ -512,7 +524,8 @@ await RunExtractToVariableAndParameterTest( } } ] - """); + """, + null); } //////////////////////////////////////////////////////////////////// @@ -638,6 +651,31 @@ param properties { } } + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + parent: vmName_resource + name: 'cse-windows' + location: location + properties: properties + } + """, + """ + param _artifactsLocation string + param _artifactsLocationSasToken string + @description('Describes the properties of a Virtual Machine Extension.') + param properties resource<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties = { + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: commandToExecute + } + } + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { parent: vmName_resource name: 'cse-windows' @@ -659,6 +697,7 @@ param properties { var i = newParameter """, null, + null, DisplayName = "Literal integer")] [DataRow( """ @@ -671,6 +710,7 @@ param properties { var j = newParameter + 1 """, null, + null, DisplayName = "int parameter reference")] [DataRow( """ @@ -683,6 +723,7 @@ param properties { var j = newParameter """, null, + null, DisplayName = "int expression with param")] [DataRow( """ @@ -695,6 +736,7 @@ param i string var j = newParameter """, null, + null, DisplayName = "strings concatenated")] [DataRow( """ @@ -709,6 +751,7 @@ param i string var j = newParameter """, null, + null, DisplayName = "strings concatenated")] [DataRow( """ @@ -724,6 +767,7 @@ param i string var p = newParameter """, + null, DisplayName = "array literal")] [DataRow( """ @@ -739,6 +783,7 @@ param i string var p = newParameter """, + null, DisplayName = "object literal with literal types")] [DataRow( """ @@ -750,6 +795,7 @@ param i string var p = { a: a, b: 'b' } """, null, + null, DisplayName = "property value from object literal")] [DataRow( """ @@ -762,6 +808,7 @@ param i string var a = o1A """, null, + null, DisplayName = "referenced property value from object literal")] [DataRow( """ @@ -778,6 +825,7 @@ param i string param newParameter 'a' | 'b' = p var v = newParameter """, + null, DisplayName = "string literal union")] [DataRow( """ @@ -800,6 +848,7 @@ param i string param newParameter { int: int } = a var b = newParameter.int """, + null, DisplayName = "object properties")] [DataRow( """ @@ -831,6 +880,7 @@ param p { param newParameter { i: int, o: { i2: int } } = p var v = newParameter.o.i2 """, + null, DisplayName = "custom object type, whole object")] [DataRow( """ @@ -862,6 +912,7 @@ param p { param pO { i2: int } = p.o var v = pO.i2 """, + null, DisplayName = "custom object type, partial")] [DataRow(""" resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { @@ -876,6 +927,7 @@ param p { } """, null, + null, DisplayName = "resource types undefined 1")] [DataRow( """ @@ -898,6 +950,7 @@ param p1 'abc'|'def' unknownProperty: unknownProperty } """, + null, DisplayName = "resource properties unknown property, follows expression's inferred type")] [DataRow(""" var foo = <<{ intVal: 2 }>> @@ -911,7 +964,8 @@ param p1 'abc'|'def' param newParameter { intVal: int } = { intVal: 2 } var foo = newParameter - """)] + """, + null)] [DataRow( """ resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { @@ -939,6 +993,7 @@ param p1 'abc'|'def' } """, null, + null, DisplayName = "resource types - string property")] [DataRow( """ @@ -977,6 +1032,7 @@ param p1 'abc'|'def' } } """, + null, DisplayName = "resource properties - string union")] [DataRow( """ @@ -989,6 +1045,7 @@ param p int? var v = newParameter """, null, + null, DisplayName = "nullable types")] [DataRow( """ @@ -1001,6 +1058,7 @@ param p int? var v = newParameter """, null, + null, DisplayName = "error types")] [DataRow( """ @@ -1016,11 +1074,12 @@ param p int? param p1 { a: { b: string } } param newParameter { a: { b: string } } = p1 var v = newParameter - """)] + """, + null)] [DataTestMethod] - public async Task Params_InferType(string fileWithSelection, string expectedMediumParameterText, string expectedStrictParameterText) + public async Task Params_InferType(string fileWithSelection, string expectedMediumParameterText, string expectedStrictParameterText, string? expectedResourceDerivedParameterText) { - await RunExtractToParameterTest(fileWithSelection, expectedMediumParameterText, expectedStrictParameterText); + await RunExtractToParameterTest(fileWithSelection, expectedMediumParameterText, expectedStrictParameterText, expectedResourceDerivedParameterText); } //////////////////////////////////////////////////////////////////// @@ -1079,6 +1138,7 @@ param _artifactsLocationSasToken string } } """, + "IGNORE", "IGNORE"); } @@ -1724,6 +1784,21 @@ public async Task IfThereIsASelection_ThenPickUpEverythingInTheSelection_AfterEx @description('A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request.') param options { autoscaleSettings: { maxThroughput: int? }?, throughput: int? } = {} + resource cassandraKeyspace 'Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2021-06-15' = { + name: 'testResource/cassandraKeyspace' + properties: { + resource: { + id: 'id' + } + options: options + } + } + """, + """ + // My comment here + @description('A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request.') + param options resource<'Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2021-06-15'>.properties.options = {} + resource cassandraKeyspace 'Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2021-06-15' = { name: 'testResource/cassandraKeyspace' properties: { @@ -1774,6 +1849,7 @@ is very long } """, null, + null, DisplayName = "Apostrophe in description")] [DataRow( """ @@ -1814,10 +1890,11 @@ is very long } """, null, + null, DisplayName = "multiline description")] - public async Task Params_ShouldPickUpDescriptions(string fileWithSelection, string expectedLooseParamText, string? expectedMediumParamText) + public async Task Params_ShouldPickUpDescriptions(string fileWithSelection, string expectedLooseParamText, string? expectedMediumParamText, string? expectedResourceDerivedParamText) { - await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText); + await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText, expectedResourceDerivedParamText); } //////////////////////////////////////////////////////////////////// @@ -2101,8 +2178,8 @@ param location string DisplayName = "get the rename position correct")] public async Task InsertAfterExistingDeclarations(string fileWithSelection, string expectedVarText, string? expectedParamText) { - await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\n"), expectedVarText, expectedParamText, "IGNORE"); - await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\r\n"), expectedVarText, expectedParamText, "IGNORE"); + await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\n"), expectedVarText, expectedParamText, "IGNORE", "IGNORE"); + await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\r\n"), expectedVarText, expectedParamText, "IGNORE", "IGNORE"); } [DataRow( @@ -2133,8 +2210,8 @@ public async Task InsertAfterExistingDeclarations(string fileWithSelection, stri [TestMethod] public async Task VarsAndParams_ShouldInsertBeforeStatementTrivia(string fileWithSelection, string expectedVarText, string? expectedParamText) { - await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\n"), expectedVarText, expectedParamText, "IGNORE"); - await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\r\n"), expectedVarText, expectedParamText, "IGNORE"); + await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\n"), expectedVarText, expectedParamText, "IGNORE", "IGNORE"); + await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\r\n"), expectedVarText, expectedParamText, "IGNORE", "IGNORE"); } // We try to imitate the behavior of the existing declarations when inserting new declarations. @@ -2647,7 +2724,9 @@ param newParameter { } resource windowsVMExtensions 'Microsoft.Compute/virtualMachines/extensions@2020-12-01' = newParameter - """); + """, + null // resource-derived types not allowed for the entire resource + ); } [TestMethod] @@ -2687,6 +2766,7 @@ await RunExtractToParameterTest( output storageEndpoint object = propertiesPrimaryEndpoints """, + null, null); } @@ -2785,6 +2865,161 @@ await RunExtractToTypeTest( null); } + [DataTestMethod] + [DataRow( + """ + resource nsg 'Microsoft.Network/networkSecurityGroups@2023-09-01' = { + name: 'nsg' + location: 'westus' + properties: |{ + securityRules: [{ + id: 'id1' + name: 'name1' + type: 'type1' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 4096 + protocol: 'Tcp' + } + }] + } + } + """, + """ + @description('Properties of the network security group.') + param properties resource<'Microsoft.Network/networkSecurityGroups@2023-09-01'>.properties = { + securityRules: [ + { + id: 'id1' + name: 'name1' + type: 'type1' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 4096 + protocol: 'Tcp' + } + } + ] + } + + resource nsg 'Microsoft.Network/networkSecurityGroups@2023-09-01' = { + name: 'nsg' + location: 'westus' + properties: properties + } + """ + )] + [DataRow( + """ + resource nsg 'Microsoft.Network/networkSecurityGroups@2023-09-01' = |{ + name: 'nsg' + location: 'westus' + properties: { + securityRules: [{ + id: 'id1' + name: 'name1' + type: 'type1' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 4096 + protocol: 'Tcp' + } + }] + } + } + """, + null)] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + properties: { + urlPathMaps: [ + { + properties: { + pathRules: [ + { + name: 'name' + properties: { + paths: [ + 'path' + ] + |backendAddressPool: { + id: 'id' + } + } + } + ] + } + } + ] + } + } + """, + """ + @description('Backend address pool resource of URL path map path rule.') + param backendAddressPool resource<'Microsoft.Network/applicationGateways@2020-11-01'>.properties.urlPathMaps[*].properties.pathRules[*].properties.backendAddressPool = { + id: 'id' + } + + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + properties: { + urlPathMaps: [ + { + properties: { + pathRules: [ + { + name: 'name' + properties: { + paths: [ + 'path' + ] + backendAddressPool: backendAddressPool + } + } + ] + } + } + ] + } + } + """ + )] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + properties: { + urlPathMaps: [ + { + properties: { + pathRules: [ + { + name: 'name' + properties: { + paths: [ + 'path' + ] + backendAddressPool: { + |id: 'id' + } + } + } + ] + } + } + ] + } + } + """, + null // Just a simple string property + )] + public async Task ResourceDerivedTypes(string fileWithSelection, string? expectedResourceDerivedText) + { + await RunExtractToVariableAndParameterTest(fileWithSelection, "IGNORE", "IGNORE", "IGNORE", expectedResourceDerivedText); + } + [TestMethod] public void TestCheckLineContent() { @@ -3059,7 +3294,8 @@ await RunExtractToParameterTest( .Replace("EXPECTEDMODIFIEDLINE", expectedModifiedLine), expectedNewParamMediumDeclaration == null ? null : expectedOutputTemplate.Replace("EXPECTEDNEWDECLARATION", expectedNewParamMediumDeclaration) - .Replace("EXPECTEDMODIFIEDLINE", expectedModifiedLine) + .Replace("EXPECTEDMODIFIEDLINE", expectedModifiedLine), + "IGNORE" ); } @@ -3089,7 +3325,7 @@ await RunExtractToParamSingleLineTest( expectedModifiedLine); } - private async Task RunExtractToVariableAndParameterTest(string fileWithSelection, string? expectedVariableText, string? expectedLooseParamText, string? expectedMediumParamText) + private async Task RunExtractToVariableAndParameterTest(string fileWithSelection, string? expectedVariableText, string? expectedLooseParamText, string? expectedMediumParamText, string? expectedResourceDerivedText) { await RunExtractToVariableTest( fileWithSelection, @@ -3097,7 +3333,8 @@ await RunExtractToVariableTest( await RunExtractToParameterTest( fileWithSelection, expectedLooseParamText, - expectedMediumParamText); + expectedMediumParamText, + expectedResourceDerivedText); } private async Task RunExtractToVariableTest(string fileWithSelection, string? expectedText) @@ -3144,7 +3381,8 @@ private async Task RunExtractToTypeTest(string fileWithSelection, string? expect } // expectedMediumParameterText can be "SAME" or "IGNORE" - private async Task RunExtractToParameterTest(string fileWithSelection, string? expectedLooseParameterText, string? expectedMediumParameterText) + // null means that there is no menu item for that option + private async Task RunExtractToParameterTest(string fileWithSelection, string? expectedLooseParameterText, string? expectedMediumParameterText, string? expectedResourceDerivedText) { if (expectedMediumParameterText == "SAME") { @@ -3153,7 +3391,7 @@ private async Task RunExtractToParameterTest(string fileWithSelection, string? e (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(fileWithSelection); var extractedParamFixes = codeActions.Where(x => x.Title.StartsWith(ExtractToParameterTitle)).ToArray(); - extractedParamFixes.Length.Should().BeLessThanOrEqualTo(2); + extractedParamFixes.Length.Should().BeLessThanOrEqualTo(3); if (expectedLooseParameterText == null) { @@ -3183,7 +3421,7 @@ private async Task RunExtractToParameterTest(string fileWithSelection, string? e { if (expectedMediumParameterText != "IGNORE") { - extractedParamFixes.Length.Should().Be(2, "expected a second option to extract to parameter"); + extractedParamFixes.Length.Should().BeGreaterThanOrEqualTo(2, "expected a second option to extract to parameter"); var mediumFix = extractedParamFixes[1]; mediumFix.Kind.Should().Be(CodeActionKind.RefactorExtract); @@ -3195,6 +3433,24 @@ private async Task RunExtractToParameterTest(string fileWithSelection, string? e updatedFileMedium.Should().HaveSourceText(expectedMediumParameterText, "extract to param with medium-strict typing should match expected outcome"); } } + + var resourceDerivedFix = extractedParamFixes.Where(fix => fix.Title.Contains("resource<")).SingleOrDefault(); + if (expectedResourceDerivedText == null) + { + resourceDerivedFix.Should().BeNull("expected no code actions to extract parameter with resource-derived type"); + } + else if (expectedResourceDerivedText != "IGNORE") + { + resourceDerivedFix.Should().NotBeNull("expected a code action to extract to parameter with resource-derived type"); + resourceDerivedFix!.Kind.Should().Be(CodeActionKind.RefactorExtract); + + resourceDerivedFix.Command.Should().NotBeNull(); + resourceDerivedFix.Command!.Name.Should().Be(PostExtractionCommandName); + + var updatedFileResourceDerived = ApplyCodeAction(bicepFile, resourceDerivedFix); + updatedFileResourceDerived.Should().HaveSourceText(expectedResourceDerivedText, "extract to param with resource-derived typing should match expected outcome"); + } + } } } diff --git a/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs b/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs index 1094dcee58e..9c6b672b61c 100644 --- a/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs @@ -16,12 +16,16 @@ using Bicep.Core.Syntax; using Bicep.Core.TypeSystem; using Bicep.Core.TypeSystem.Types; +using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Features; using Bicep.Core.UnitTests.Utils; using Bicep.LanguageServer.Refactor; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; +using static Bicep.Core.UnitTests.Utils.CompilationHelper; namespace Bicep.LangServer.IntegrationTests; @@ -53,7 +57,7 @@ public class TypeStringifierTests "type strict = 'abc'?")] public void SimpleTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -74,7 +78,7 @@ public void SimpleTypes(string typeDeclaration, string expectedLooseSyntax, stri "type strict = true")] public void LiteralTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -104,7 +108,7 @@ public void LiteralTypes(string typeDeclaration, string expectedLooseSyntax, str "type strict = 123?")] public void DontWidenLiteralTypesWithMediumWhenPartOfAUnion(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataRow( @@ -116,7 +120,7 @@ public void DontWidenLiteralTypesWithMediumWhenPartOfAUnion(string typeDeclarati [DataTestMethod] public void MixedTypeArrays(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -152,7 +156,7 @@ public void MixedTypeArrays(string typeDeclaration, string expectedLooseSyntax, "type strict = { 'true': true }")] public void ObjectTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -183,7 +187,7 @@ public void ObjectTypes(string typeDeclaration, string expectedLooseSyntax, stri "type strict = { a: 'a'?, b: ('a'|'b')?, c: ('a'|'b'|'c')? }")] public void UnionTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -250,7 +254,7 @@ public void UnionTypes(string typeDeclaration, string expectedLooseSyntax, strin "type strict = [ 'abc'|'def', 'def'|'ghi' ]")] public void TupleTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataRow( @@ -326,7 +330,7 @@ public void TupleTypes(string typeDeclaration, string expectedLooseSyntax, strin [DataTestMethod] public void OpenEnumTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -352,7 +356,7 @@ public void OpenEnumTypes(string typeDeclaration, string expectedLooseSyntax, st "type strict = ('abc' | 'def')[]")] public void TypedArrays(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -363,7 +367,7 @@ public void TypedArrays(string typeDeclaration, string expectedLooseSyntax, stri "type strict = array")] public void ArrayType(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -375,7 +379,7 @@ public void ArrayType(string typeDeclaration, string expectedLooseSyntax, string "type strict = []")] public void EmptyArray(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -386,7 +390,7 @@ public void EmptyArray(string typeDeclaration, string expectedLooseSyntax, strin "type strict = { }")] public void EmptyObject(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -397,7 +401,8 @@ public void EmptyObject(string typeDeclaration, string expectedLooseSyntax, stri "type strict = []")] public void EmptyArrays(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -423,7 +428,7 @@ public void EmptyArrays(string typeDeclaration, string expectedLooseSyntax, stri "type strict = {a: [object /* recursive */, object /* recursive */?]?, t: object /* recursive */?}")] public void RecursiveTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -511,7 +516,7 @@ public void RecursiveTypes(string typeDeclaration, string expectedLooseSyntax, s "type strict = { a: 'a'?, b: ('a' | 'b')?, c: ('a' | 'b' | 'c')? }?")] public void NullableTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, null); } [DataTestMethod] @@ -528,6 +533,7 @@ public void NullableTypes(string typeDeclaration, string expectedLooseSyntax, st "type loose = string", "type medium = string /* 'BlobStorage' | 'BlockBlobStorage' | 'FileStorage' | 'Storage' | 'StorageV2' | string */", "type strict = string /* 'BlobStorage' | 'BlockBlobStorage' | 'FileStorage' | 'Storage' | 'StorageV2' | string */", + null, DisplayName = "Storage kind property (open enum)")] [DataRow( """ @@ -554,6 +560,7 @@ public void NullableTypes(string typeDeclaration, string expectedLooseSyntax, st "type loose = array", "type medium = string[]", "type strict = ['https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/writeblob.ps1?sas=abcd']", + null, DisplayName = "virtual machine extensions fileUris property")] // // "settings" property @@ -580,40 +587,25 @@ public void NullableTypes(string typeDeclaration, string expectedLooseSyntax, st } """, "settings", - "type loose = object", - "type medium = { commandToExecute: string, fileUris: string[] }", - "type strict = { commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1', fileUris: ['https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/writeblob.ps1?sas=abcd'] }", - DisplayName = "virtual machine extensions settings property")] + "type loose = object?", + "type medium = { commandToExecute: string, fileUris: string[] }?", + "type strict = { commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1', fileUris: ['https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/writeblob.ps1?sas=abcd'] }?", + null, + DisplayName = "typed as Any (virtual machine extensions settings property)")] // // "properties" property // [DataRow( """ - var isWindowsOS = true - var provisionExtensions = true - param _artifactsLocation string - @secure() - param _artifactsLocationSasToken string - - resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = { name: 'cse-windows/extension' location: 'location' properties: { - publisher: 'Microsoft.Compute' - type: 'CyustomScriptExtension' - typeHandlerVersion: '1.8' - autoUpgradeMinorVersion: true - settings: { - fileUris: [ - uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') - ] - commandToExecute: 'commandToExecute' - } } } """, "properties", - "type loose = object", + "type loose = object?", """ type medium = { autoUpgradeMinorVersion: bool? @@ -642,7 +634,7 @@ param _artifactsLocationSasToken string settings: object? /* any */ type: string? typeHandlerVersion: string? - } + }? """, """ type strict = { @@ -672,9 +664,117 @@ param _artifactsLocationSasToken string settings: object? /* any */ type: string? typeHandlerVersion: string? + }? + """, + "type resourceDerived = resource<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties", + DisplayName = "virtual machine extensions properties property")] + [DataRow( + """ + resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'cse-windows/extension' + location: 'location' + properties: { + } + } + """, + "properties", + "type loose = object?", + """ + type medium = { + autoUpgradeMinorVersion: bool? + forceUpdateTag: string? + instanceView: { + name: string? + statuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + substatuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + type: string? + typeHandlerVersion: string? + }? + protectedSettings: object? /* any */ + publisher: string? + settings: object? /* any */ + type: string? + typeHandlerVersion: string? + }? + """, + "IGNORE", + "type resourceDerived = resource<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties", + DisplayName = "if")] + [DataRow( + """ + resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = [for (item, index) in mylist: { + name: 'cse-windows/extension' + location: 'location' + properties: { + } + } + ] + """, + "properties", + "type loose = object?", + """ + type medium = { + autoUpgradeMinorVersion: bool? + forceUpdateTag: string? + instanceView: { + name: string? + statuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + substatuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + type: string? + typeHandlerVersion: string? + }? + protectedSettings: object? /* any */ + publisher: string? + settings: object? /* any */ + type: string? + typeHandlerVersion: string? + }? + """, + "IGNORE", + "type resourceDerived = resource<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties", + DisplayName = "for")] + [DataRow( + """ + resource virtualMachine 'Microsoft.Compute/virtualMachines@2020-12-01' = { + name: 'name' + location: location } + + resource testResource 'Microsoft.Compute/virtualMachines/extensions@2020-12-01' = { + parent: virtualMachine + name: 'name' + } """, - DisplayName = "virtual machine extensions properties")] + "", + "type loose = object? /* Microsoft.Compute/virtualMachines/extensions@2020-12-01 */", + "type medium = object? /* Microsoft.Compute/virtualMachines/extensions@2020-12-01 */", + "type strict = object? /* Microsoft.Compute/virtualMachines/extensions@2020-12-01 */", + null, // Resource-derived type for a full resource is not allowed in Bicep + DisplayName = "virtual machine extension full resource type")] [DataRow( """ resource virtualMachine 'Microsoft.Compute/virtualMachines@2020-12-01' = { @@ -688,13 +788,208 @@ param _artifactsLocationSasToken string } """, "parent", - "type loose = object? /* Microsoft.Compute/virtualMachines */", - "type medium = object? /* Microsoft.Compute/virtualMachines */", - "type strict = object? /* Microsoft.Compute/virtualMachines */", - DisplayName = "virtual machine entire object (via 'parent')")] - public void ResourcePropertyTypes(string resourceDeclaration, string resourcePropertyName, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + "type loose = object", + "type medium = { asserts: object, dependsOn: (object /* module[] | (resource | module) | resource[] */)[], eTag: string, extendedLocation: { name: string?, type: (string /* 'EdgeZone' | string */)? }?, identity: { type: ('None' | 'SystemAssigned' | 'SystemAssigned, UserAssigned' | 'UserAssigned')?, userAssignedIdentities: object? }?, kind: string, location: string, managedBy: string, managedByExtended: string[], name: string, plan: { name: string?, product: string?, promotionCode: string?, publisher: string? }?, properties: { additionalCapabilities: { ultraSSDEnabled: bool? }?, availabilitySet: { id: string? }?, billingProfile: { maxPrice: int? }?, diagnosticsProfile: { bootDiagnostics: { enabled: bool?, storageUri: string? }? }?, evictionPolicy: (string /* 'Deallocate' | 'Delete' | string */)?, extensionsTimeBudget: string?, hardwareProfile: { vmSize: (string /* 'Basic_A0' | 'Basic_A1' | 'Basic_A2' | 'Basic_A3' | 'Basic_A4' | 'Standard_A0' | 'Standard_A1' | 'Standard_A10' | 'Standard_A11' | 'Standard_A1_v2' | 'Standard_A2' | 'Standard_A2_v2' | 'Standard_A2m_v2' | 'Standard_A3' | 'Standard_A4' | 'Standard_A4_v2' | 'Standard_A4m_v2' | 'Standard_A5' | 'Standard_A6' | 'Standard_A7' | 'Standard_A8' | 'Standard_A8_v2' | 'Standard_A8m_v2' | 'Standard_A9' | 'Standard_B1ms' | 'Standard_B1s' | 'Standard_B2ms' | 'Standard_B2s' | 'Standard_B4ms' | 'Standard_B8ms' | 'Standard_D1' | 'Standard_D11' | 'Standard_D11_v2' | 'Standard_D12' | 'Standard_D12_v2' | 'Standard_D13' | 'Standard_D13_v2' | 'Standard_D14' | 'Standard_D14_v2' | 'Standard_D15_v2' | 'Standard_D16_v3' | 'Standard_D16s_v3' | 'Standard_D1_v2' | 'Standard_D2' | 'Standard_D2_v2' | 'Standard_D2_v3' | 'Standard_D2s_v3' | 'Standard_D3' | 'Standard_D32_v3' | 'Standard_D32s_v3' | 'Standard_D3_v2' | 'Standard_D4' | 'Standard_D4_v2' | 'Standard_D4_v3' | 'Standard_D4s_v3' | 'Standard_D5_v2' | 'Standard_D64_v3' | 'Standard_D64s_v3' | 'Standard_D8_v3' | 'Standard_D8s_v3' | 'Standard_DS1' | 'Standard_DS11' | 'Standard_DS11_v2' | 'Standard_DS12' | 'Standard_DS12_v2' | 'Standard_DS13' | 'Standard_DS13-2_v2' | 'Standard_DS13-4_v2' | 'Standard_DS13_v2' | 'Standard_DS14' | 'Standard_DS14-4_v2' | 'Standard_DS14-8_v2' | 'Standard_DS14_v2' | 'Standard_DS15_v2' | 'Standard_DS1_v2' | 'Standard_DS2' | 'Standard_DS2_v2' | 'Standard_DS3' | 'Standard_DS3_v2' | 'Standard_DS4' | 'Standard_DS4_v2' | 'Standard_DS5_v2' | 'Standard_E16_v3' | 'Standard_E16s_v3' | 'Standard_E2_v3' | 'Standard_E2s_v3' | 'Standard_E32-16_v3' | 'Standard_E32-8s_v3' | 'Standard_E32_v3' | 'Standard_E32s_v3' | 'Standard_E4_v3' | 'Standard_E4s_v3' | 'Standard_E64-16s_v3' | 'Standard_E64-32s_v3' | 'Standard_E64_v3' | 'Standard_E64s_v3' | 'Standard_E8_v3' | 'Standard_E8s_v3' | 'Standard_F1' | 'Standard_F16' | 'Standard_F16s' | 'Standard_F16s_v2' | 'Standard_F1s' | 'Standard_F2' | 'Standard_F2s' | 'Standard_F2s_v2' | 'Standard_F32s_v2' | 'Standard_F4' | 'Standard_F4s' | 'Standard_F4s_v2' | 'Standard_F64s_v2' | 'Standard_F72s_v2' | 'Standard_F8' | 'Standard_F8s' | 'Standard_F8s_v2' | 'Standard_G1' | 'Standard_G2' | 'Standard_G3' | 'Standard_G4' | 'Standard_G5' | 'Standard_GS1' | 'Standard_GS2' | 'Standard_GS3' | 'Standard_GS4' | 'Standard_GS4-4' | 'Standard_GS4-8' | 'Standard_GS5' | 'Standard_GS5-16' | 'Standard_GS5-8' | 'Standard_H16' | 'Standard_H16m' | 'Standard_H16mr' | 'Standard_H16r' | 'Standard_H8' | 'Standard_H8m' | 'Standard_L16s' | 'Standard_L32s' | 'Standard_L4s' | 'Standard_L8s' | 'Standard_M128-32ms' | 'Standard_M128-64ms' | 'Standard_M128ms' | 'Standard_M128s' | 'Standard_M64-16ms' | 'Standard_M64-32ms' | 'Standard_M64ms' | 'Standard_M64s' | 'Standard_NC12' | 'Standard_NC12s_v2' | 'Standard_NC12s_v3' | 'Standard_NC24' | 'Standard_NC24r' | 'Standard_NC24rs_v2' | 'Standard_NC24rs_v3' | 'Standard_NC24s_v2' | 'Standard_NC24s_v3' | 'Standard_NC6' | 'Standard_NC6s_v2' | 'Standard_NC6s_v3' | 'Standard_ND12s' | 'Standard_ND24rs' | 'Standard_ND24s' | 'Standard_ND6s' | 'Standard_NV12' | 'Standard_NV24' | 'Standard_NV6' | string */)? }?, host: { id: string? }?, hostGroup: { id: string? }?, licenseType: string?, networkProfile: { networkInterfaces: { id: string?, properties: { primary: bool? }? }[]? }?, osProfile: { adminPassword: string?, adminUsername: string?, allowExtensionOperations: bool?, computerName: string?, customData: string?, linuxConfiguration: { disablePasswordAuthentication: bool?, patchSettings: { patchMode: (string /* 'AutomaticByPlatform' | 'ImageDefault' | string */)? }?, provisionVMAgent: bool?, ssh: { publicKeys: { keyData: string?, path: string? }[]? }? }?, requireGuestProvisionSignal: bool?, secrets: { sourceVault: { id: string? }?, vaultCertificates: { certificateStore: string?, certificateUrl: string? }[]? }[]?, windowsConfiguration: { additionalUnattendContent: { componentName: string?, content: string?, passName: string?, settingName: ('AutoLogon' | 'FirstLogonCommands')? }[]?, enableAutomaticUpdates: bool?, patchSettings: { enableHotpatching: bool?, patchMode: (string /* 'AutomaticByOS' | 'AutomaticByPlatform' | 'Manual' | string */)? }?, provisionVMAgent: bool?, timeZone: string?, winRM: { listeners: { certificateUrl: string?, protocol: ('Http' | 'Https')? }[]? }? }? }?, platformFaultDomain: int?, priority: (string /* 'Low' | 'Regular' | 'Spot' | string */)?, proximityPlacementGroup: { id: string? }?, securityProfile: { encryptionAtHost: bool?, securityType: (string /* 'TrustedLaunch' | string */)?, uefiSettings: { secureBootEnabled: bool?, vTpmEnabled: bool? }? }?, storageProfile: { dataDisks: { caching: ('None' | 'ReadOnly' | 'ReadWrite')?, createOption: string /* 'Attach' | 'Empty' | 'FromImage' | string */, detachOption: (string /* 'ForceDetach' | string */)?, diskSizeGB: int?, image: { uri: string? }?, lun: int, managedDisk: { diskEncryptionSet: { id: string? }?, id: string?, storageAccountType: (string /* 'Premium_LRS' | 'Premium_ZRS' | 'StandardSSD_LRS' | 'StandardSSD_ZRS' | 'Standard_LRS' | 'UltraSSD_LRS' | string */)? }?, name: string?, toBeDetached: bool?, vhd: { uri: string? }?, writeAcceleratorEnabled: bool? }[]?, imageReference: { id: string?, offer: string?, publisher: string?, sku: string?, version: string? }?, osDisk: { caching: ('None' | 'ReadOnly' | 'ReadWrite')?, createOption: string /* 'Attach' | 'Empty' | 'FromImage' | string */, diffDiskSettings: { option: (string /* 'Local' | string */)?, placement: (string /* 'CacheDisk' | 'ResourceDisk' | string */)? }?, diskSizeGB: int?, encryptionSettings: { diskEncryptionKey: { secretUrl: string, sourceVault: { id: string? } }?, enabled: bool?, keyEncryptionKey: { keyUrl: string, sourceVault: { id: string? } }? }?, image: { uri: string? }?, managedDisk: { diskEncryptionSet: { id: string? }?, id: string?, storageAccountType: (string /* 'Premium_LRS' | 'Premium_ZRS' | 'StandardSSD_LRS' | 'StandardSSD_ZRS' | 'Standard_LRS' | 'UltraSSD_LRS' | string */)? }?, name: string?, osType: ('Linux' | 'Windows')?, vhd: { uri: string? }?, writeAcceleratorEnabled: bool? }? }?, virtualMachineScaleSet: { id: string? }? }?, scale: { capacity: int, maximum: int, minimum: int }, sku: { capacity: int, family: string, model: string, name: string, size: string, tier: string }, tags: object?, zones: string[]? }", + "type strict = { asserts: object, dependsOn: (object /* module[] | (resource | module) | resource[] */)[], eTag: string, extendedLocation: { name: string?, type: (string /* 'EdgeZone' | string */)? }?, identity: { type: ('None' | 'SystemAssigned' | 'SystemAssigned, UserAssigned' | 'UserAssigned')?, userAssignedIdentities: object? }?, kind: string, location: string, managedBy: string, managedByExtended: string[], name: string, plan: { name: string?, product: string?, promotionCode: string?, publisher: string? }?, properties: { additionalCapabilities: { ultraSSDEnabled: bool? }?, availabilitySet: { id: string? }?, billingProfile: { maxPrice: int? }?, diagnosticsProfile: { bootDiagnostics: { enabled: bool?, storageUri: string? }? }?, evictionPolicy: (string /* 'Deallocate' | 'Delete' | string */)?, extensionsTimeBudget: string?, hardwareProfile: { vmSize: (string /* 'Basic_A0' | 'Basic_A1' | 'Basic_A2' | 'Basic_A3' | 'Basic_A4' | 'Standard_A0' | 'Standard_A1' | 'Standard_A10' | 'Standard_A11' | 'Standard_A1_v2' | 'Standard_A2' | 'Standard_A2_v2' | 'Standard_A2m_v2' | 'Standard_A3' | 'Standard_A4' | 'Standard_A4_v2' | 'Standard_A4m_v2' | 'Standard_A5' | 'Standard_A6' | 'Standard_A7' | 'Standard_A8' | 'Standard_A8_v2' | 'Standard_A8m_v2' | 'Standard_A9' | 'Standard_B1ms' | 'Standard_B1s' | 'Standard_B2ms' | 'Standard_B2s' | 'Standard_B4ms' | 'Standard_B8ms' | 'Standard_D1' | 'Standard_D11' | 'Standard_D11_v2' | 'Standard_D12' | 'Standard_D12_v2' | 'Standard_D13' | 'Standard_D13_v2' | 'Standard_D14' | 'Standard_D14_v2' | 'Standard_D15_v2' | 'Standard_D16_v3' | 'Standard_D16s_v3' | 'Standard_D1_v2' | 'Standard_D2' | 'Standard_D2_v2' | 'Standard_D2_v3' | 'Standard_D2s_v3' | 'Standard_D3' | 'Standard_D32_v3' | 'Standard_D32s_v3' | 'Standard_D3_v2' | 'Standard_D4' | 'Standard_D4_v2' | 'Standard_D4_v3' | 'Standard_D4s_v3' | 'Standard_D5_v2' | 'Standard_D64_v3' | 'Standard_D64s_v3' | 'Standard_D8_v3' | 'Standard_D8s_v3' | 'Standard_DS1' | 'Standard_DS11' | 'Standard_DS11_v2' | 'Standard_DS12' | 'Standard_DS12_v2' | 'Standard_DS13' | 'Standard_DS13-2_v2' | 'Standard_DS13-4_v2' | 'Standard_DS13_v2' | 'Standard_DS14' | 'Standard_DS14-4_v2' | 'Standard_DS14-8_v2' | 'Standard_DS14_v2' | 'Standard_DS15_v2' | 'Standard_DS1_v2' | 'Standard_DS2' | 'Standard_DS2_v2' | 'Standard_DS3' | 'Standard_DS3_v2' | 'Standard_DS4' | 'Standard_DS4_v2' | 'Standard_DS5_v2' | 'Standard_E16_v3' | 'Standard_E16s_v3' | 'Standard_E2_v3' | 'Standard_E2s_v3' | 'Standard_E32-16_v3' | 'Standard_E32-8s_v3' | 'Standard_E32_v3' | 'Standard_E32s_v3' | 'Standard_E4_v3' | 'Standard_E4s_v3' | 'Standard_E64-16s_v3' | 'Standard_E64-32s_v3' | 'Standard_E64_v3' | 'Standard_E64s_v3' | 'Standard_E8_v3' | 'Standard_E8s_v3' | 'Standard_F1' | 'Standard_F16' | 'Standard_F16s' | 'Standard_F16s_v2' | 'Standard_F1s' | 'Standard_F2' | 'Standard_F2s' | 'Standard_F2s_v2' | 'Standard_F32s_v2' | 'Standard_F4' | 'Standard_F4s' | 'Standard_F4s_v2' | 'Standard_F64s_v2' | 'Standard_F72s_v2' | 'Standard_F8' | 'Standard_F8s' | 'Standard_F8s_v2' | 'Standard_G1' | 'Standard_G2' | 'Standard_G3' | 'Standard_G4' | 'Standard_G5' | 'Standard_GS1' | 'Standard_GS2' | 'Standard_GS3' | 'Standard_GS4' | 'Standard_GS4-4' | 'Standard_GS4-8' | 'Standard_GS5' | 'Standard_GS5-16' | 'Standard_GS5-8' | 'Standard_H16' | 'Standard_H16m' | 'Standard_H16mr' | 'Standard_H16r' | 'Standard_H8' | 'Standard_H8m' | 'Standard_L16s' | 'Standard_L32s' | 'Standard_L4s' | 'Standard_L8s' | 'Standard_M128-32ms' | 'Standard_M128-64ms' | 'Standard_M128ms' | 'Standard_M128s' | 'Standard_M64-16ms' | 'Standard_M64-32ms' | 'Standard_M64ms' | 'Standard_M64s' | 'Standard_NC12' | 'Standard_NC12s_v2' | 'Standard_NC12s_v3' | 'Standard_NC24' | 'Standard_NC24r' | 'Standard_NC24rs_v2' | 'Standard_NC24rs_v3' | 'Standard_NC24s_v2' | 'Standard_NC24s_v3' | 'Standard_NC6' | 'Standard_NC6s_v2' | 'Standard_NC6s_v3' | 'Standard_ND12s' | 'Standard_ND24rs' | 'Standard_ND24s' | 'Standard_ND6s' | 'Standard_NV12' | 'Standard_NV24' | 'Standard_NV6' | string */)? }?, host: { id: string? }?, hostGroup: { id: string? }?, licenseType: string?, networkProfile: { networkInterfaces: { id: string?, properties: { primary: bool? }? }[]? }?, osProfile: { adminPassword: string?, adminUsername: string?, allowExtensionOperations: bool?, computerName: string?, customData: string?, linuxConfiguration: { disablePasswordAuthentication: bool?, patchSettings: { patchMode: (string /* 'AutomaticByPlatform' | 'ImageDefault' | string */)? }?, provisionVMAgent: bool?, ssh: { publicKeys: { keyData: string?, path: string? }[]? }? }?, requireGuestProvisionSignal: bool?, secrets: { sourceVault: { id: string? }?, vaultCertificates: { certificateStore: string?, certificateUrl: string? }[]? }[]?, windowsConfiguration: { additionalUnattendContent: { componentName: 'Microsoft-Windows-Shell-Setup'?, content: string?, passName: 'OobeSystem'?, settingName: ('AutoLogon' | 'FirstLogonCommands')? }[]?, enableAutomaticUpdates: bool?, patchSettings: { enableHotpatching: bool?, patchMode: (string /* 'AutomaticByOS' | 'AutomaticByPlatform' | 'Manual' | string */)? }?, provisionVMAgent: bool?, timeZone: string?, winRM: { listeners: { certificateUrl: string?, protocol: ('Http' | 'Https')? }[]? }? }? }?, platformFaultDomain: int?, priority: (string /* 'Low' | 'Regular' | 'Spot' | string */)?, proximityPlacementGroup: { id: string? }?, securityProfile: { encryptionAtHost: bool?, securityType: (string /* 'TrustedLaunch' | string */)?, uefiSettings: { secureBootEnabled: bool?, vTpmEnabled: bool? }? }?, storageProfile: { dataDisks: { caching: ('None' | 'ReadOnly' | 'ReadWrite')?, createOption: string /* 'Attach' | 'Empty' | 'FromImage' | string */, detachOption: (string /* 'ForceDetach' | string */)?, diskSizeGB: int?, image: { uri: string? }?, lun: int, managedDisk: { diskEncryptionSet: { id: string? }?, id: string?, storageAccountType: (string /* 'Premium_LRS' | 'Premium_ZRS' | 'StandardSSD_LRS' | 'StandardSSD_ZRS' | 'Standard_LRS' | 'UltraSSD_LRS' | string */)? }?, name: string?, toBeDetached: bool?, vhd: { uri: string? }?, writeAcceleratorEnabled: bool? }[]?, imageReference: { id: string?, offer: string?, publisher: string?, sku: string?, version: string? }?, osDisk: { caching: ('None' | 'ReadOnly' | 'ReadWrite')?, createOption: string /* 'Attach' | 'Empty' | 'FromImage' | string */, diffDiskSettings: { option: (string /* 'Local' | string */)?, placement: (string /* 'CacheDisk' | 'ResourceDisk' | string */)? }?, diskSizeGB: int?, encryptionSettings: { diskEncryptionKey: { secretUrl: string, sourceVault: { id: string? } }?, enabled: bool?, keyEncryptionKey: { keyUrl: string, sourceVault: { id: string? } }? }?, image: { uri: string? }?, managedDisk: { diskEncryptionSet: { id: string? }?, id: string?, storageAccountType: (string /* 'Premium_LRS' | 'Premium_ZRS' | 'StandardSSD_LRS' | 'StandardSSD_ZRS' | 'Standard_LRS' | 'UltraSSD_LRS' | string */)? }?, name: string?, osType: ('Linux' | 'Windows')?, vhd: { uri: string? }?, writeAcceleratorEnabled: bool? }? }?, virtualMachineScaleSet: { id: string? }? }?, scale: { capacity: int, maximum: int, minimum: int }, sku: { capacity: int, family: string, model: string, name: string, size: string, tier: string }, tags: object?, zones: string[]? }", + null, + DisplayName = "virtual machine extension's 'parent' property")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + name: 'name' + location: location + properties: { + sku: { + name: 'Standard_Small' + tier: 'Standard' + capacity: 1 + } + gatewayIPConfigurations: [ + { + name: 'name' + properties: { + subnet: { + id: 'id' + } + } + } + ] + } + } + """, + "[skip]properties", + "IGNORE", + "IGNORE", + "IGNORE", + "type resourceDerived = resource<'Microsoft.Network/applicationGateways@2020-11-01'>.properties.gatewayIPConfigurations[*].properties", + DisplayName = "Array 1")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + name: 'n' + properties:{ + gatewayIPConfigurations: [ + { + id: 'configId' + properties: { + subnet: { + id: 'subnetId' + } + } + } + ] + } + } + """, + "[skip]id", + "IGNORE", + "IGNORE", + "IGNORE", + null, // subnet.id is a simple string + DisplayName = "string inside an array")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + name: 'n' + properties:{ + gatewayIPConfigurations: [ + { + id: 'configId' + properties: { + subnet: { + id: 'subnetId' + } + } + } + ] + } + } + """, + "subnet", + "IGNORE", + "IGNORE", + "IGNORE", + "type resourceDerived = resource<'Microsoft.Network/applicationGateways@2020-11-01'>.properties.gatewayIPConfigurations[*].properties.subnet", + DisplayName = "custom object inside an array")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + name: 'n' + dependsOn: [] + } + """, + "dependsOn", + "IGNORE", + "IGNORE", + "IGNORE", + "type resourceDerived = resource<'Microsoft.Network/applicationGateways@2020-11-01'>.dependsOn", + DisplayName = "dependsOn")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + name: 'n' + extendedLocation: { + type: 'ArcZone' + name: 'name' + } + } + """, + "type", + "type loose = string", + "type medium = string /* 'ArcZone' | 'CustomLocation' | 'EdgeZone' | 'NotSpecified' | string */", + "type strict = string /* 'ArcZone' | 'CustomLocation' | 'EdgeZone' | 'NotSpecified' | string */", + null, + DisplayName = "string enum")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + name: 'n' + identity: { + type: 'None' + userAssignedIdentities: {} + } + } + """, + "userAssignedIdentities", + "type loose = object?", + "type medium = object?", + "type strict = object?", + null, + DisplayName = "custom object type with zero writable properties (resource<'Microsoft.Network/applicationGateways@2020-11-01'>.identity.userAssignedIdentities)")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + properties: { + sslPolicy: { + cipherSuites: [ + // hi there + 'TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA' // hello + ] + } + } + } + """, + "cipherSuites", + "type loose = array?", + "type medium = (string /* 'TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA' | 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA' | 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA256' | 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA' | 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA256' | 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA' | 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256' | 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA' | 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384' | 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA' | 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256' | 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256' | 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA' | 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384' | 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384' | 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA' | 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256' | 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' | 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA' | 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384' | 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' | 'TLS_RSA_WITH_3DES_EDE_CBC_SHA' | 'TLS_RSA_WITH_AES_128_CBC_SHA' | 'TLS_RSA_WITH_AES_128_CBC_SHA256' | 'TLS_RSA_WITH_AES_128_GCM_SHA256' | 'TLS_RSA_WITH_AES_256_CBC_SHA' | 'TLS_RSA_WITH_AES_256_CBC_SHA256' | 'TLS_RSA_WITH_AES_256_GCM_SHA384' | string */)[]?", + "type strict = (string /* 'TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA' | 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA' | 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA256' | 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA' | 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA256' | 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA' | 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256' | 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA' | 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384' | 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA' | 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256' | 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256' | 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA' | 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384' | 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384' | 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA' | 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256' | 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' | 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA' | 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384' | 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' | 'TLS_RSA_WITH_3DES_EDE_CBC_SHA' | 'TLS_RSA_WITH_AES_128_CBC_SHA' | 'TLS_RSA_WITH_AES_128_CBC_SHA256' | 'TLS_RSA_WITH_AES_128_GCM_SHA256' | 'TLS_RSA_WITH_AES_256_CBC_SHA' | 'TLS_RSA_WITH_AES_256_CBC_SHA256' | 'TLS_RSA_WITH_AES_256_GCM_SHA384' | string */)[]?", + "type resourceDerived = resource<'Microsoft.Network/applicationGateways@2020-11-01'>.properties.sslPolicy.cipherSuites", + DisplayName = "typed array")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + properties: { + urlPathMaps: [ + { + properties: { + pathRules: [ + { + name: 'name' + properties: { + paths: [ + 'path' + ] + backendAddressPool: { + id: 'id' + } + } + } + ] + } + } + ] + } + } + """, + "backendAddressPool", + "type loose = object?", + "type medium = { id: string? }?", + "type strict = { id: string? }?", + "type resourceDerived = resource<'Microsoft.Network/applicationGateways@2020-11-01'>.properties.urlPathMaps[*].properties.pathRules[*].properties.backendAddressPool", + DisplayName = "nested typed arrays")] + [DataRow( + """ + resource testResource 'Microsoft.Network/applicationGateways@2020-11-01' = { + properties: { + [ + '*': { + cipherSuites: [ + a: 'b' + ] + } + ] + } + } + """, + "cipherSuites", + "type loose = object? /* error */", + "type medium = object? /* error */", + "type strict = object? /* error */", + null, + DisplayName = "Error")] + public void ResourcePropertyTypesAndResourceDerivedTypes(string resourceDeclaration, string resourcePropertyName, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax, string expectedResourceDerivedSyntax) { - RunTestFromResourceProperty(resourceDeclaration, resourcePropertyName, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromResourceProperty(resourceDeclaration, resourcePropertyName, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, expectedResourceDerivedSyntax); } [DataTestMethod] @@ -705,7 +1000,8 @@ public void ResourcePropertyTypes(string resourceDeclaration, string resourcePro """, "type loose = object", "type medium = { abc: int }", // TODO: better would be "type medium = t1" but Bicep type system doesn't currently support it - "type strict = { abc: int }" // TODO: better would be "type strict = t1" but Bicep type system doesn't currently support it + "type strict = { abc: int }", // TODO: better would be "type strict = t1" but Bicep type system doesn't currently support it + null )] [DataRow( """ @@ -722,8 +1018,8 @@ public void ResourcePropertyTypes(string resourceDeclaration, string resourcePro """, "type loose = array", "type medium = { t1Property: { a: string, b: string }, t2Property: { a: string, b: string }[] }[]", // TODO: better would be "type medium = t3[]" but Bicep type system doesn't currently support it - "type strict = [ { t1Property: { a: string, b: string }, t2Property: { a: string, b: string }[] } ]" // TODO: better would be "type strict = [ t3 ]" but Bicep type system doesn't currently support it - )] + "type strict = [ { t1Property: { a: string, b: string }, t2Property: { a: string, b: string }[] } ]", // TODO: better would be "type strict = [ t3 ]" but Bicep type system doesn't currently support it + null)] [DataRow( """ type t1 = { a: 'abc', b: 123 } @@ -731,10 +1027,11 @@ public void ResourcePropertyTypes(string resourceDeclaration, string resourcePro """, "type loose = object", // TODO: better: "{ a: t1, b: [t1, t1] }" "type medium = { a: { a: string, b: int }, b: { a: string, b: int }[] }", // TODO: better: "{ a: t1, b: [t1, t1] }" - "type strict = { a: { a: 'abc', b: 123 }, b: [{ a: 'abc', b: 123 }, { a: 'abc', b: 123 }] }")] - public void NamedTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + "type strict = { a: { a: 'abc', b: 123 }, b: [{ a: 'abc', b: 123 }, { a: 'abc', b: 123 }] }", + null)] + public void NamedTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax, string? expectedResourceDerivedSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, expectedResourceDerivedSyntax); } [DataTestMethod] @@ -753,77 +1050,156 @@ public void NamedTypes(string typeDeclaration, string expectedLooseSyntax, strin """, "type loose = object", "type medium = { a: int, b: int, c: bool, d: bool }", - "type strict = { a: -10, b: 10, c: false, d: true }")] - public void NegatedTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + "type strict = { a: -10, b: 10, c: false, d: true }", + null)] + public void NegatedTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax, string? expectedResourceDerivedSyntax) { - RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, expectedResourceDerivedSyntax); } #region Support + private static CompilationResult Compile(string source) + { + var services = new ServiceBuilder().WithFeatureOverrides(new FeatureProviderOverrides(ResourceDerivedTypesEnabled: true, ResourceTypedParamsAndOutputsEnabled: true)); + return CompilationHelper.Compile(services, source); + } + // input is a type declaration statement for type "testType", e.g. "type testType = int" - private static void RunTestFromTypeDeclaration(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + private static void RunTestFromTypeDeclaration(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax, string? expectedResourceDerivedSyntax) { - var compilationResult = CompilationHelper.Compile(typeDeclaration); + var compilationResult = Compile(typeDeclaration); var semanticModel = compilationResult.Compilation.GetEntrypointSemanticModel(); var declarationSyntax = semanticModel.Root.TypeDeclarations[0].DeclaringSyntax; var declaredType = semanticModel.GetDeclaredType(semanticModel.Root.TypeDeclarations.Single(t => t.Name == "testType").Value); declaredType.Should().NotBeNull(); - RunTestHelper(null, declaredType!, semanticModel, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestHelper(null, null, declaredType!, semanticModel, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, expectedResourceDerivedSyntax); } // input is a resource declaration for resource "testResource" and a property name such as "properties" that is exposed anywhere on the resource - private static void RunTestFromResourceProperty(string resourceDeclaration, string resourcePropertyName, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + private static void RunTestFromResourceProperty(string resourceDeclaration, string resourcePropertyName, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax, string expectedResourceDerivedSyntax) { - var compilationResult = CompilationHelper.Compile(resourceDeclaration); + var useEntireResourceType = resourcePropertyName == ""; + + var compilationResult = Compile(resourceDeclaration); var semanticModel = compilationResult.Compilation.GetEntrypointSemanticModel(); - var resourceSyntax = semanticModel.Root.ResourceDeclarations.Single(r => r.Name == "testResource").DeclaringResource; + var resourceSyntax = semanticModel.Root.ResourceDeclarations.SingleOrDefault(r => r.Name == "testResource")?.DeclaringResource!; + resourceSyntax.Should().NotBeNull("the resource in the test must be named 'testResource'"); var properties = GetAllSyntaxOfType(resourceSyntax); - var matchingProperty = properties.Single(p => p.Key is IdentifierSyntax id && id.NameEquals(resourcePropertyName)); + ObjectPropertySyntax? matchingPropertySyntax; + if (useEntireResourceType) + { + // Use the entire resource + matchingPropertySyntax = null; + } + else + { + int skip = 0; + while (resourcePropertyName.StartsWith("[skip]")) + { + skip++; + resourcePropertyName = resourcePropertyName.Substring("[skip]".Length); + } + matchingPropertySyntax = properties.Skip(skip).Where(p => p.Key is IdentifierSyntax id && id.NameEquals(resourcePropertyName)) + .Skip(skip) + .FirstOrDefault(); + matchingPropertySyntax.Should().NotBeNull($"can't find property {resourcePropertyName} in the resource's syntax"); + } - var inferredType = semanticModel.GetTypeInfo(matchingProperty.Value); - var declaredType = semanticModel.GetDeclaredType(matchingProperty); - var matchingPropertyType = declaredType is AnyType || declaredType == null ? inferredType : declaredType; - matchingPropertyType.Should().NotBeNull(); + var inferredType = semanticModel.GetTypeInfo(useEntireResourceType ? resourceSyntax : matchingPropertySyntax!.Value); + var declaredType = semanticModel.GetDeclaredType(useEntireResourceType ? resourceSyntax : matchingPropertySyntax!.Value); + var matchingPropertyTypeSymbol = declaredType is AnyType || declaredType == null ? inferredType : declaredType; + matchingPropertyTypeSymbol.Should().NotBeNull(); + var matchingPropertyTypeProperty = matchingPropertySyntax?.TryGetTypeProperty(semanticModel); - RunTestHelper(null, matchingPropertyType!, semanticModel, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + RunTestHelper(matchingPropertySyntax, matchingPropertyTypeProperty, matchingPropertyTypeSymbol!, semanticModel, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax, expectedResourceDerivedSyntax); } - private static void RunTestHelper(TypeProperty? typeProperty, TypeSymbol typeSymbol, SemanticModel semanticModel, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + private static void RunTestHelper(ObjectPropertySyntax? propertySyntax, TypeProperty? typeProperty, TypeSymbol typeSymbol, SemanticModel semanticModel, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax, string? expectedResourceDerivedSyntax /* null means no resource-derived type can be generated */) { + var ignoreLoose = expectedLooseSyntax == "IGNORE"; + var ignoreMediumStrict = expectedMediumStrictSyntax == "IGNORE"; + var ignoreStrict = expectedStrictSyntax == "IGNORE"; + var ignoreResourceDerived = expectedResourceDerivedSyntax == "IGNORE"; + if (debugPrintAllSyntaxNodeTypes) { DebugPrintAllSyntaxNodeTypes(semanticModel); } - var looseSyntax = TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Loose); - var mediumStrictSyntax = TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Medium); - var strictSyntax = TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Strict); + var actualLooseTypeName = ignoreLoose ? null : TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Loose); + var actualMediumTypeName = ignoreMediumStrict ? null : TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Medium); + var actualStrictTypeName = ignoreStrict ? null : TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Strict); + var actualResourceDerivedTypeName = propertySyntax is null ? null : TypeStringifier.TryGetResourceDerivedTypeName(semanticModel, propertySyntax); using (new AssertionScope()) { - CompilationHelper.Compile(expectedLooseSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected loose syntax should be error-free"); - CompilationHelper.Compile(expectedMediumStrictSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected medium strictness syntax should be error-free"); - CompilationHelper.Compile(expectedStrictSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected strict syntax should be error-free"); + if (!ignoreLoose) + { + Compile(expectedLooseSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected loose syntax should be error-free"); + } + if (!ignoreMediumStrict) + { + Compile(expectedMediumStrictSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected medium strictness syntax should be error-free"); + } + if (!ignoreStrict) + { + Compile(expectedStrictSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected strict syntax should be error-free"); + } + if (expectedResourceDerivedSyntax is { } && !ignoreResourceDerived) + { + /* TODO: Blocked by https://github.com/Azure/bicep/issues/15277 + Compile(expectedResourceDerivedSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected resource-derived syntax should be error-free"); + */ + } } using (new AssertionScope()) { - var actualLooseSyntaxType = $"type loose = {looseSyntax}"; - actualLooseSyntaxType.Should().EqualIgnoringBicepFormatting(expectedLooseSyntax); + if (!ignoreLoose) + { + var actualLooseSyntaxTypeStmt = $"type loose = {actualLooseTypeName}"; + actualLooseSyntaxTypeStmt.Should().EqualIgnoringBicepFormatting(expectedLooseSyntax); + Compile(actualLooseSyntaxTypeStmt).Diagnostics.Should().NotHaveAnyDiagnostics("the generated loose type string should compile successfully"); + } - string actualMediumLooseSyntaxType = $"type medium = {mediumStrictSyntax}"; - actualMediumLooseSyntaxType.Should().EqualIgnoringBicepFormatting(expectedMediumStrictSyntax); + if (!ignoreMediumStrict) + { + var actualMediumStrictSyntaxTypeStmt = $"type medium = {actualMediumTypeName}"; + actualMediumStrictSyntaxTypeStmt.Should().EqualIgnoringBicepFormatting(expectedMediumStrictSyntax); + Compile(actualMediumStrictSyntaxTypeStmt).Diagnostics.Should().NotHaveAnyDiagnostics("the generated medium strictness type string should compile successfully"); + } - string actualStrictSyntaxType = $"type strict = {strictSyntax}"; - actualStrictSyntaxType.Should().EqualIgnoringBicepFormatting(expectedStrictSyntax); + if (!ignoreStrict) + { + var actualStrictTypeStmt = $"type strict = {actualStrictTypeName}"; + actualStrictTypeStmt.Should().EqualIgnoringBicepFormatting(expectedStrictSyntax); + Compile(actualStrictTypeStmt).Diagnostics.Should().NotHaveAnyDiagnostics("the generated strict type string should compile successfully"); + } - CompilationHelper.Compile(actualLooseSyntaxType).Diagnostics.Should().NotHaveAnyDiagnostics("the generated loose type string should compile successfully"); - CompilationHelper.Compile(actualMediumLooseSyntaxType).Diagnostics.Should().NotHaveAnyDiagnostics("the generated medium strictness type string should compile successfully"); - CompilationHelper.Compile(actualStrictSyntaxType).Diagnostics.Should().NotHaveAnyDiagnostics("the generated loose strict string should compile successfully"); + string? actualResourceDerivedSyntaxTypeStmt = null; + if (!ignoreResourceDerived) + { + if (expectedResourceDerivedSyntax is { }) + { + actualResourceDerivedTypeName.Should().NotBeNull($"expected {expectedResourceDerivedSyntax.Replace("type resourceDerived = ", "")}"); + if (actualResourceDerivedTypeName is { }) + { + actualResourceDerivedSyntaxTypeStmt = $"type resourceDerived = {actualResourceDerivedTypeName}"; + actualResourceDerivedSyntaxTypeStmt.Should().EqualIgnoringBicepFormatting(expectedResourceDerivedSyntax); + /* TODO: Blocked by https://github.com/Azure/bicep/issues/15277 + Compile(actualResourceDerivedSyntaxTypeStmt!).Diagnostics.Should().NotHaveAnyDiagnostics("the generated resource-derived type string should compile successfully"); + */ + } + } + else + { + actualResourceDerivedTypeName.Should().BeNull("expected no resource-derived type to be found"); + } + } } } @@ -874,7 +1250,7 @@ public static ImmutableArray Build(SyntaxBase syntax) var visitor = new GetAllSyntaxNodesVisitor(); visitor.Visit(syntax); - return [..visitor.syntaxList]; + return [.. visitor.syntaxList]; } protected override void VisitInternal(SyntaxBase syntax) diff --git a/src/Bicep.LangServer/Refactor/ExpressionAndTypeExtractor.cs b/src/Bicep.LangServer/Refactor/ExpressionAndTypeExtractor.cs index c0d42864c6b..0741b7c227e 100644 --- a/src/Bicep.LangServer/Refactor/ExpressionAndTypeExtractor.cs +++ b/src/Bicep.LangServer/Refactor/ExpressionAndTypeExtractor.cs @@ -45,7 +45,8 @@ public class ExpressionAndTypeExtractor private record ExtractionContext( StatementSyntax ParentStatement, // The statement containing the extraction context ExpressionSyntax ExpressionSyntax, // The expression to be extracted - TypeProperty? TypeProperty, // The property inside a parent object's type whose value is being extracted, if any + ObjectPropertySyntax? PropertySyntax, // The property syntax containing the expression, if any + TypeProperty? TypeProperty, // The type property for PropertySyntax string? ContextDerivedName // Suggested name based on context ); @@ -93,7 +94,8 @@ public IEnumerable GetExtractionCodeFixes(List n return null; } - TypeProperty? parentTypeProperty = null; + ObjectPropertySyntax? containingProperty = null; + TypeProperty? containingTypeProperty = null; string? contextDerivedNewName = null; // Pick a semi-intelligent default name for the new param and variable. @@ -105,7 +107,8 @@ public IEnumerable GetExtractionCodeFixes(List n // `{ objectPropertyName: <> }` // entire property value expression selected // -> default to the name "objectPropertyName" contextDerivedNewName = propertyName; - parentTypeProperty = propertySyntax.TryGetTypeProperty(semanticModel); + containingProperty = propertySyntax; + containingTypeProperty = containingProperty.TryGetTypeProperty(semanticModel); } else if (expressionSyntax is ObjectPropertySyntax propertySyntax2 && propertySyntax2.TryGetKeyText() is string propertyName2) @@ -119,7 +122,8 @@ public IEnumerable GetExtractionCodeFixes(List n if (propertyValueSyntax != null) { expressionSyntax = propertyValueSyntax; - parentTypeProperty = propertySyntax2.TryGetTypeProperty(semanticModel); + containingProperty = propertySyntax2; + containingTypeProperty = containingProperty.TryGetTypeProperty(semanticModel); } else { @@ -154,7 +158,7 @@ public IEnumerable GetExtractionCodeFixes(List n return null; } - return new ExtractionContext(parentStatement, expressionSyntax, parentTypeProperty, contextDerivedNewName); + return new ExtractionContext(parentStatement, expressionSyntax, containingProperty, containingTypeProperty, contextDerivedNewName); } private IEnumerable CreateAllExtractions(ExtractionContext extractionContext) @@ -173,6 +177,7 @@ private IEnumerable CreateAllExtractions(ExtractionContext e // Strict typing for the param doesn't appear useful, providing only loose and medium at the moment var stringifiedLooseType = Stringify(newParamType, extractionContext.TypeProperty, Strictness.Loose, ignoreTopLevelNullability); var stringifiedUserDefinedType = Stringify(newParamType, extractionContext.TypeProperty, Strictness.Medium, ignoreTopLevelNullability); + var resourceDerivedType = extractionContext.PropertySyntax is null ? null : TryGetResourceDerivedTypeName(semanticModel, extractionContext.PropertySyntax); var userDefinedTypeAvailable = !string.Equals(stringifiedLooseType, stringifiedUserDefinedType, StringComparison.Ordinal); @@ -189,7 +194,19 @@ private IEnumerable CreateAllExtractions(ExtractionContext e ExtractionKind.UserParam, $"[Preview] Extract parameter of type {GetQuotedText(stringifiedUserDefinedType)}", stringifiedUserDefinedType); + } + + if (resourceDerivedType is { } && semanticModel.Features.ResourceDerivedTypesEnabled) + { + yield return CreateExtraction( + extractionContext, + ExtractionKind.ResDerivedParam, + $"[Preview] Extract parameter of type {GetQuotedText(resourceDerivedType)}", + resourceDerivedType); + } + if (userDefinedTypeAvailable) + { yield return CreateExtraction( extractionContext, ExtractionKind.Type, diff --git a/src/Bicep.LangServer/Refactor/TypeStringifier.cs b/src/Bicep.LangServer/Refactor/TypeStringifier.cs index 066b01be644..22d31513429 100644 --- a/src/Bicep.LangServer/Refactor/TypeStringifier.cs +++ b/src/Bicep.LangServer/Refactor/TypeStringifier.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Bicep.Core; using Bicep.Core.Parsing; +using Bicep.Core.Semantics; using Bicep.Core.Syntax; using Bicep.Core.TypeSystem; using Bicep.Core.TypeSystem.Types; @@ -43,6 +44,56 @@ public static string Stringify(TypeSymbol? type, TypeProperty? typeProperty, Str return StringifyCore(type, typeProperty, strictness, [], removeTopLevelNullability); } + // This works off of the syntax tree of the declared resource rather than the types due to type system limitations + public static string? TryGetResourceDerivedTypeName(SemanticModel semanticModel, ObjectPropertySyntax propertySyntax) + { + SyntaxBase? current = propertySyntax; + string propertyAccessDotNotation = ""; // Includes leading periods + + while (current is not null) + { + if (current is ResourceDeclarationSyntax resourceDeclarationSyntax) + { + // Found the resource itself + var resourceTypeName = (resourceDeclarationSyntax.Type as StringSyntax)?.TryGetLiteralValue(); + return $"resource<'{resourceTypeName}'>{propertyAccessDotNotation}"; + } + else if (current is ObjectPropertySyntax objectPropertySyntax) + { + var declaredType = semanticModel.GetDeclaredType(current); + + var isInterestingObjectType = + declaredType is ObjectType objectType + && objectType.Name != LanguageConstants.ObjectType + && GetWriteableProperties(objectType).Length > 0; + + if (isInterestingObjectType || declaredType is ArrayType) + { + var propertyName = (objectPropertySyntax.Key as IdentifierSyntax)?.IdentifierName; + if (propertyName is null) + { + return null; + } + + propertyAccessDotNotation = $".{propertyName}{propertyAccessDotNotation}"; + } + else + { + // Not an interesting resource-derived type (for example a primitive type, any, or simple object) that user can declare without needing a resource-derived type + return null; + } + } + else if (current is ArrayItemSyntax) + { + propertyAccessDotNotation = $"[*]{propertyAccessDotNotation}"; + } + + current = semanticModel.Binder.GetParent(current); + } + + return null; + } + private static string StringifyCore(TypeSymbol? type, TypeProperty? typeProperty, Strictness strictness, TypeSymbol[] visitedTypes, bool removeTopLevelNullability = false) { if (type == null) @@ -175,7 +226,7 @@ or BooleanLiteralType return LanguageConstants.Object.Name; } - var writeableProperties = objectType.Properties.Where(p => !p.Value.Flags.HasFlag(TypePropertyFlags.ReadOnly)).ToArray(); + var writeableProperties = GetWriteableProperties(objectType); // strict: {} with additional properties allowed should be "object" not "{}" // medium: Bicep infers {} with no allowable members from the literal "{}", the user more likely wants to allow members @@ -186,7 +237,7 @@ or BooleanLiteralType } return $"{{ {string.Join(", ", writeableProperties - .Select(p => GetFormattedTypeProperty(p.Value, strictness, visitedTypes)))} }}"; + .Select(p => GetFormattedTypeProperty(p, strictness, visitedTypes)))} }}"; case AnyType: return AnyTypeName; @@ -286,6 +337,11 @@ private static bool IsEmptyObjectLiteral(ObjectType objectType) return objectType.Properties.Count == 0 && !objectType.HasExplicitAdditionalPropertiesType; } + private static TypeProperty[] GetWriteableProperties(ObjectType objectType) => + objectType.Properties.Select(p => p.Value) + .Where(p => !p.Flags.HasFlag(TypePropertyFlags.ReadOnly)) + .ToArray(); + private static string GetFormattedTypeProperty(TypeProperty property, Strictness strictness, TypeSymbol[] visitedTypes) { return diff --git a/src/Bicep.LangServer/Telemetry/BicepTelemetryEvent.cs b/src/Bicep.LangServer/Telemetry/BicepTelemetryEvent.cs index 4f6175ba797..c56aae36443 100644 --- a/src/Bicep.LangServer/Telemetry/BicepTelemetryEvent.cs +++ b/src/Bicep.LangServer/Telemetry/BicepTelemetryEvent.cs @@ -337,6 +337,7 @@ public enum ExtractionKind OnlySimpleParam, // Extract parameter when only simple type is available SimpleNotUserParam, // Extract simple-typed parameter when parameter with user-defined type is also available UserParam, // Extract parameter with user-defined type (both simple and user-defined-type params are available) + ResDerivedParam, // Extract parameter with resource-derived type Type, }