From 24179601d206f1507baeee6e4c47ff8207fdf826 Mon Sep 17 00:00:00 2001 From: Mikolaj Mackowiak <7921224+miqm@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:54:58 +0100 Subject: [PATCH 1/5] WIP Decompile bicepparam --- ...compileForPasteBicepCommandHandlerTests.cs | 1861 +++++++++++++++++ ...ForPasteBicepParamsCommandHandlerTests.cs} | 20 +- .../BicepDecompileForPasteCommandHandler.cs | 64 +- src/vscode-bicep/src/commands/pasteAsBicep.ts | 14 +- src/vscode-bicep/src/language/protocol.ts | 1 + 5 files changed, 1925 insertions(+), 35 deletions(-) create mode 100644 src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs rename src/Bicep.LangServer.UnitTests/Handlers/{BicepDecompileForPasteCommandHandlerTests.cs => BicepDecompileForPasteBicepParamsCommandHandlerTests.cs} (98%) diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs new file mode 100644 index 00000000000..b7d30d45c4f --- /dev/null +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs @@ -0,0 +1,1861 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Bicep.Core.UnitTests; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Mock; +using Bicep.Core.UnitTests.Utils; +using Bicep.LangServer.UnitTests.Mocks; +using Bicep.LanguageServer.Handlers; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OmniSharp.Extensions.JsonRpc; +using static Bicep.LanguageServer.Handlers.BicepDecompileForPasteCommandHandler; + +namespace Bicep.LangServer.UnitTests.Handlers +{ + [TestClass] + public class BicepDecompileForPasteBicepCommandHandlerTests + { + [NotNull] + public TestContext? TestContext { get; set; } + + private BicepDecompileForPasteCommandHandler CreateHandler(LanguageServerMock server) + { + var builder = ServiceBuilder.Create(services => services + .AddSingleton(StrictMock.Of().Object) + .AddSingleton(BicepTestConstants.CreateMockTelemetryProvider().Object) + .AddSingleton(server.Mock.Object) + .AddSingleton() + ); + + return builder.Construct(); + } + + public enum PasteType + { + None, + FullTemplate, + SingleResource, + ResourceList, + JsonValue, + BicepValue, + } + + record Options( + string pastedJson, + PasteType? expectedPasteType = null, + PasteContext expectedPasteContext = PasteContext.None, + string? expectedBicep = null, + bool ignoreGeneratedBicep = false, + string? expectedErrorMessage = null, + string? editorContentsWithCursor = null); + + private async Task TestDecompileForPaste( + string json, + PasteType? expectedPasteType = null, + string? expectedBicep = null, + string? expectedErrorMessage = null, + string? editorContentsWithCursor = null) + { + await TestDecompileForPaste(new( + json, + expectedPasteType, + PasteContext.None, + expectedBicep: expectedBicep, + ignoreGeneratedBicep: false, + expectedErrorMessage: expectedErrorMessage, + editorContentsWithCursor: editorContentsWithCursor)); + } + + private async Task TestDecompileForPaste(Options options) + { + var (editorContents, cursorOffset) = (options.editorContentsWithCursor is not null && options.editorContentsWithCursor.Contains('|')) + ? ParserHelper.GetFileWithSingleCursor(options.editorContentsWithCursor, '|') + : (string.Empty, 0); + + var editorContentsWithPastedJson = string.Concat(editorContents.AsSpan(0, cursorOffset), options.pastedJson, editorContents.AsSpan(cursorOffset)); + _ = FileHelper.SaveResultFile(TestContext, "main.bicep", editorContentsWithPastedJson); + LanguageServerMock server = new(); + var handler = CreateHandler(server); + + + var result = await handler.Handle(new(editorContentsWithPastedJson, cursorOffset, options.pastedJson.Length, options.pastedJson, queryCanPaste: false, "bicep"), CancellationToken.None); + + result.ErrorMessage.Should().Be(options.expectedErrorMessage); + + if (!options.ignoreGeneratedBicep) + { + var expectedBicep = options.expectedBicep?.Trim('\n'); + var actualBicep = result.Bicep?.Trim('\n'); + actualBicep.Should().EqualTrimmedLines(expectedBicep); + } + + result.PasteContext.Should().Be(options.expectedPasteContext switch + { + PasteContext.None => "none", + PasteContext.String => "string", + _ => throw new NotImplementedException() + }); + + result.PasteType.Should().Be(options.expectedPasteType switch + { + PasteType.None => PasteType_None, + PasteType.FullTemplate => PasteType_FullTemplate, + PasteType.SingleResource => PasteType_SingleResource, + PasteType.ResourceList => PasteType_ResourceList, + PasteType.JsonValue => PasteType_JsonValue, + PasteType.BicepValue => PasteType_BicepValue, + _ => throw new NotImplementedException(), + }); + } + + #region JSON/Bicep Constants + + private const string jsonFullTemplateMembers = @" + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""parameters"": { + ""location"": { + ""type"": ""string"", + ""defaultValue"": ""[resourceGroup().location]"" + } + }, + ""resources"": [ + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name"", + ""location"": ""[parameters('location')]"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + } + ]"; + private const string jsonFullTemplate = $@"{{ +{jsonFullTemplateMembers} +}}"; + + const string Resource1Json = @" { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""eastus"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }"; + const string Resource2Json = @" { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name2"", + ""location"": ""eastus"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }"; + + const string Resource1Bicep = @"resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } +}"; + const string Resource2Bicep = @"resource name2 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name2' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } +}"; + + #endregion + + [DataTestMethod] + [DataRow( + jsonFullTemplate, + PasteType.FullTemplate, + @" + param location string = resourceGroup().location + + resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }", + DisplayName = "Full template" + )] + [DataRow( + $@"{{ +{jsonFullTemplateMembers} + , extraProperty: ""hello"" +}}", + PasteType.FullTemplate, + @" + param location string = resourceGroup().location + + resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }", + DisplayName = "Extra property" + )] + [DataRow( + $@"{{ +{jsonFullTemplateMembers} +}} +}} // extra", + PasteType.FullTemplate, + @" + param location string = resourceGroup().location + + resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }", + DisplayName = "Extra brace at end (succeeds)" + )] + [DataRow( + $@"{{ +{jsonFullTemplateMembers} +}} +random characters", + PasteType.FullTemplate, + @" + param location string = resourceGroup().location + + resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }", + DisplayName = "Extra random characters at end" + )] + [DataRow( + $@"{{ +{{ // extra +{jsonFullTemplateMembers} +}} +random characters", + PasteType.None, + null, + DisplayName = "Extra brace at beginning (can't paste)" + )] + [DataRow( + $@" +random characters +{{ +{jsonFullTemplateMembers} +}}", + PasteType.None, + null, + DisplayName = "Extra random characters at beginning (can't paste)" + )] + [DataRow( + $@"{{ + ""$schema"": {{}}, + ""parameters"": {{ + ""location"": {{ + ""type"": ""string"", + ""defaultValue"": ""[resourceGroup().location]"" + }} + }} + }}", + PasteType.JsonValue, + // Treats it simply as a JSON object + $@"{{ + '$schema': {{}} + parameters: {{ + location: {{ + type: 'string' + defaultValue: resourceGroup().location + }} + }} + }}", + DisplayName = "Schema not a string" + )] + public async Task FullTemplate(string json, PasteType expectedPasteType, string expectedBicep, string? errorMessage = null) + { + await TestDecompileForPaste( + json: json, + expectedPasteType: expectedPasteType, + expectedBicep: expectedBicep, + errorMessage); + } + + [TestMethod] + public async Task PasteFullTemplate_ButNoSchema_ConvertsIntoPlainOldObject() + { + const string json = @" + { + ""contentVersion"": ""1.0.0.0"", + ""parameters"": { + ""location"": { + ""type"": ""string"", + ""defaultValue"": ""[resourceGroup().location]"" + } + }, + ""resources"": [ + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name"", + ""location"": ""[parameters('location')]"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + } + ] + }"; + var expectedBicep = @"{ +contentVersion: '1.0.0.0' +parameters: { +location: { +type: 'string' +defaultValue: resourceGroup().location +} +} +resources: [ +{ +type: 'Microsoft.Storage/storageAccounts' +apiVersion: '2021-02-01' +name: 'name' +location: location +kind: 'StorageV2' +sku: { +name: 'Premium_LRS' +} +} +] +}"; + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.JsonValue, + expectedErrorMessage: null, + expectedBicep: expectedBicep); + } + + [TestMethod] + public async Task FullTemplate_ButMissingParameter_ShouldGiveError() + { + const string json = @" + { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""parameters"": { + }, + ""resources"": [ + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name"", + ""location"": ""[parameters('location')]"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + } + ] + }"; + await TestDecompileForPaste( + json, + expectedPasteType: PasteType.FullTemplate, + expectedErrorMessage: "[12:60]: Unable to find parameter location", + expectedBicep: null); + } + + [DataTestMethod] + [DataRow( + @" + { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""metadata"": { + ""_generator"": { + ""name"": ""bicep"", + ""version"": ""0.10.61.36676"", + ""templateHash"": ""8521684133784798165"" + } + }, + ""resources"": { // whoops + { + ""type"": ""Microsoft.Compute/virtualMachines/providers/configurationProfileAssignments"", + ""apiVersion"": ""2022-05-04"", + ""name"": ""vmName/Microsoft.Automanage/default"", + ""properties"": { + ""configurationProfile"": ""/providers/Microsoft.Automanage/bestPractices/AzureBestPracticesDevTest"" + } + } + ]", + PasteType.None, + null, + null, + DisplayName = "{ instead of [" + )] + [DataRow( + @"{ + ""type"": ""Microsoft.Compute/virtualMachines/providers/configurationProfileAssignments"", + ""apiVersion"": ""2022-12-12"", + ""name"": ""name"", + ""properties"": { + ""configurationProfile"": ""[bad-expression]"" + } + }", + PasteType.SingleResource, + null, + "[6:46]: The language expression 'bad-expression' is not valid: the string character 'x' at position '5' is not expected.", + DisplayName = "Bad expression" + )] + public async Task Errors(string json, PasteType pasteType, string? expectedBicep, string? expectedErrorMessage) + { + await TestDecompileForPaste(json, pasteType, expectedBicep, expectedErrorMessage); + } + + [TestMethod] + public async Task JustString_WithNoQuotes_CantConvert() + { + var json = @"just a string"; + await TestDecompileForPaste( + json: json, + PasteType.None, + expectedErrorMessage: null, + expectedBicep: null); + } + + [TestMethod] + public async Task NonResourceObject_WrongPropertyType_Object_PastesAsSimpleObject() + { + var json = @$" + {Resource1Json.Replace("\"2021-02-01\"", "{}")} + "; + await TestDecompileForPaste( + json: json, + PasteType.JsonValue, + expectedErrorMessage: null, + expectedBicep: @" + { + type: 'Microsoft.Storage/storageAccounts' + apiVersion: {} + name: 'name1' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }"); + } + + [TestMethod] + public async Task NonResourceObject_WrongPropertyType_Number_PastesAsSimpleObject() + { + var json = @$" + {Resource1Json.Replace("\"2021-02-01\"", "1234")} + "; + await TestDecompileForPaste( + json: json, + PasteType.JsonValue, + expectedErrorMessage: null, + expectedBicep: @" + { + type: 'Microsoft.Storage/storageAccounts' + apiVersion: 1234 + name: 'name1' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }"); + } + + [TestMethod] + public async Task MissingParametersAndVars() + { + var json = @" + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name"", + ""location"": ""[parameters('location')]"", + ""kind"": ""[variables('storageKind')]"", + ""sku"": { + ""name"": ""[variables('sku')]"" + } + } + "; + var expected = @"resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: storageKind + sku: { + name: sku + } +}"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedBicep: expected); + } + + [TestMethod] + public async Task MissingParametersAndVars_Conflict() + { + var json = """ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-02-01", + "name": "name", + "location": "[concat(parameters('location'), variables('location'), parameters('location_var'), variables('location_var'), parameters('location_param'), variables('location_param'))]", + "kind": "[variables('location')]", + "sku": { + "name": "Premium_LRS" + } + } + """; + + await TestDecompileForPaste( + json: json, + expectedErrorMessage: null, + expectedPasteType: PasteType.SingleResource, + expectedBicep: """ + resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: concat(location, location_var, location_var, location_var_var, location_param, location_param_var) + kind: location_var + sku: { + name: 'Premium_LRS' + } + } + """); + } + + [TestMethod] + public async Task SingleResourceObject_ShouldSucceed() + { + var json = @" + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name"", + ""location"": ""eastus"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }"; + var expected = @" + resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }"; + + await TestDecompileForPaste( + json: json, + PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: expected); + } + + [TestMethod] + public async Task MultipleResourceObjects_ShouldSucceed() + { + var json = @" + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""eastus"", // comment and blank line + + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }, + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name2"", + ""location"": ""eastus"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }, + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name3"", + ""location"": ""eastus"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }"; + var expected = @" + resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + } + + resource name2 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name2' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + } + + resource name3 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name3' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }"; + + await TestDecompileForPaste( + json: json, + PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: expected); + } + + [TestMethod] + public async Task MultipleResourceObjects_SkipTrivia_ShouldSucceed() + { + var json = $@" + + // This is a comment + // So is this + /* And this + also */ + + {Resource1Json} + // This is a comment + // So is this + /* And this + also */ +, + // This is a comment + // So is this + /* And this + also */ + + {Resource2Json} // This is a comment + // So is this + /* And this + also */"; + ; + var expected = $@" + {Resource1Bicep} + + {Resource2Bicep}"; + + await TestDecompileForPaste( + json: json, + PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: expected); + } + + [TestMethod] + public async Task MultipleResourceObjects_NoComma_ShouldSucceed() + { + var json = @" + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""eastus"", // comment and blank line + + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + } + { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name2"", + ""location"": ""eastus"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }"; + var expected = @" + resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + } + + resource name2 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name2' + location: 'eastus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }"; + + await TestDecompileForPaste( + json: json, + PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: expected); + } + + [TestMethod] + public async Task Modules() + { + const string json = @" + { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""resources"": [ + { + ""name"": ""nestedDeploymentInner"", + ""type"": ""Microsoft.Resources/deployments"", + ""apiVersion"": ""2021-04-01"", + ""properties"": { + ""expressionEvaluationOptions"": { + ""scope"": ""inner"" + }, + ""mode"": ""Incremental"", + ""parameters"": {}, + ""template"": { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""parameters"": {}, + ""variables"": {}, + ""resources"": [ + { + ""name"": ""storageaccount1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-04-01"", + ""tags"": { + ""displayName"": ""storageaccount1"" + }, + ""location"": ""[resourceGroup().location]"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"", + ""tier"": ""Premium"" + } + } + ], + ""outputs"": {} + } + } + }, + { + ""name"": ""nestedDeploymentOuter"", + ""type"": ""Microsoft.Resources/deployments"", + ""apiVersion"": ""2021-04-01"", + ""properties"": { + ""mode"": ""Incremental"", + ""template"": { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""variables"": {}, + ""resources"": [ + { + ""name"": ""storageaccount2"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-04-01"", + ""tags"": { + ""displayName"": ""storageaccount2"" + }, + ""location"": ""[resourceGroup().location]"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"", + ""tier"": ""Premium"" + } + } + ], + ""outputs"": {} + } + } + }, + { + ""name"": ""storageaccount"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-04-01"", + ""tags"": { + ""displayName"": ""storageaccount"" + }, + ""location"": ""[resourceGroup().location]"", + ""kind"": ""StorageV2"", + ""sku"": { + ""name"": ""Premium_LRS"", + ""tier"": ""Premium"" + } + }, + { + ""name"": ""nestedDeploymentInner2"", + ""type"": ""Microsoft.Resources/deployments"", + ""apiVersion"": ""2021-04-01"", + ""properties"": { + ""expressionEvaluationOptions"": { + ""scope"": ""inner"" + }, + ""mode"": ""Incremental"", + ""parameters"": {}, + ""template"": { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""parameters"": {}, + ""variables"": {}, + ""resources"": [], + ""outputs"": {} + } + } + } + ] + }"; + + var expected = @" + module nestedDeploymentInner './nested_nestedDeploymentInner.bicep' = { + name: 'nestedDeploymentInner' + params: {} + } + + module nestedDeploymentOuter './nested_nestedDeploymentOuter.bicep' = { + name: 'nestedDeploymentOuter' + params: {} + } + + resource storageaccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { + name: 'storageaccount' + tags: { + displayName: 'storageaccount' + } + location: resourceGroup().location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + tier: 'Premium' + } + } + + module nestedDeploymentInner2 './nested_nestedDeploymentInner2.bicep' = { + name: 'nestedDeploymentInner2' + params: {} + }"; + + await TestDecompileForPaste( + json: json, + PasteType.FullTemplate, + expectedErrorMessage: null, + expectedBicep: expected); + } + + [TestMethod] + public async Task SingleResource_WithNestedInnerScopedTemplateWithMissingParam_ShouldSucceed() + { + const string json = @" + { + ""name"": ""nestedDeploymentInner"", + ""type"": ""Microsoft.Resources/deployments"", + ""apiVersion"": ""2021-04-01"", + ""properties"": { + ""mode"": ""Incremental"", + ""expressionEvaluationOptions"": { + ""scope"": ""inner"" + }, + ""template"": { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""resources"": [ + { + ""name"": ""storageaccount1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-04-01"", + // Refers to a local parameter in the module, which is missing. + // This should cause error during conversion (because the nested template should be valid), + // although a missing parameter at the top level would not (because the top level is + // not expected to be complete). + ""location"": ""[parameters('location')]"" + } + ] + } + } + }"; + + await TestDecompileForPaste( + json: json, + PasteType.SingleResource, + expectedErrorMessage: "[18:48]: Unable to find parameter location", + expectedBicep: null); + } + + [TestMethod] + public async Task SingleResource_WithNestedOuterScopedTemplate_WithMissingParam_ShouldBePastable_ButShouldGiveError() + { + const string json = @" + { + ""name"": ""nestedDeploymentInner"", + ""type"": ""Microsoft.Resources/deployments"", + ""apiVersion"": ""2021-04-01"", + ""properties"": { + ""mode"": ""Incremental"", + ""template"": { + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": ""1.0.0.0"", + ""resources"": [ + { + ""name"": ""storageaccount1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-04-01"", + // Refers to a local parameter in the module, which is missing. + // This should cause error during conversion (because the nested template should be valid), + // although a missing parameter at the top level would not (because the top level is + // not expected to be complete). + ""location"": ""[parameters('location')]"" + } + ] + } + } + }"; + + await TestDecompileForPaste( + json: json, + PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + module nestedDeploymentInner './nested_nestedDeploymentInner.bicep' = { + name: 'nestedDeploymentInner' + params: { + location: location + } + }"); + } + + [TestMethod] + public async Task MultipleResourceObjects_ExtraBraceAfterwards_ShouldSucceed() + { + var json = @$" + {Resource1Json} + {Resource2Json} + }}}} // extra"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: @$" + {Resource1Bicep} + + {Resource2Bicep}"); + } + + [TestMethod] + public async Task MultipleResourceObjects_ExtraOpenBraceAfterwards_ShouldSucceed() + { + var json = @$" + {Resource1Json} + {Resource2Json} + {{ // extra"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: @$" + {Resource1Bicep} + + {Resource2Bicep}"); + } + + [TestMethod] + public async Task MultipleResourceObjects_ExtraEmptyObjectAfterwards_ShouldSucceed() + { + var json = @$" + {Resource1Json} + {Resource2Json} + {{}} // extra"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: @$" + {Resource1Bicep} + + {Resource2Bicep}"); + } + + [TestMethod] + public async Task MultipleResourceObjects_NameConflict_ShouldAllowPaste_ButGiveError() + { + var json = @$" + {Resource1Json} + {Resource1Json} + {Resource1Json}"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: "[21:1]: Unable to pick unique name for resource Microsoft.Storage/storageAccounts name1", + expectedBicep: null); + } + + [TestMethod] + public async Task MultipleResourceObjects_RandomCharactersAfterwards_ShouldSucceed_AndIgnoreRemaining() + { + var json = @$" + {Resource1Json} + {Resource2Json} + something else {{ // extra"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: @$" + {Resource1Bicep} + + {Resource2Bicep}" + ); + } + + [TestMethod] + public async Task MultipleResourceObjects_NonResourceInMiddle_ShouldSucceed_AndIgnoreNonResources() + { + var json = @$" + {Resource1Json} + {{ + ""notAResource"": ""honest"" + }} + {Resource2Json} +"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: $@" + {Resource1Bicep} + + {Resource2Bicep}" + ); + } + + [TestMethod] + public async Task MultipleResourceObjects_ExtraCommaAtEnd_ShouldSucceed() + { + var json = @$" + {Resource1Json} + {Resource2Json} + ,,, // extra"; + var expected = @$" + {Resource1Bicep} + + {Resource2Bicep}"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.ResourceList, + expectedErrorMessage: null, + expectedBicep: expected); + } + + [TestMethod] + public async Task MissingVariable_UsedMultipleTimes_ShouldSucceed() + { + var json = @" { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""[variables('v1')]"", + ""kind"": ""[variables('v1')]"", + ""sku"": { + ""name"": ""[variables('v1')]"" + } + }"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: v1 + kind: v1 + sku: { + name: v1 + } + }"); + } + + [TestMethod] + public async Task MissingVariable_UsedMultipleTimes_CasedDifferently_ShouldSucceed() + { + var json = @" { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""[variables('v1')]"", + ""kind"": ""[variables('v1')]"", + ""sku"": { + ""name"": ""[variables('V1')]"" + } + }"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: v1 + kind: v1 + sku: { + name: v1 + } + }"); + } + + [TestMethod] + public async Task MissingParameter_UsedMultipleTimes_ShouldSucceed() + { + var json = @"{ + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""[parameters('p1')]"", + ""kind"": ""[parameters('p1')]"", + ""sku"": { + ""name"": ""[parameters('p1')]"" + } + }"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: p1 + kind: p1 + sku: { + name: p1 + } + }"); + } + + [TestMethod] + public async Task MissingParameter_UsedMultipleTimes_CasedDifferently_ShouldSucceed() + { + var json = @" { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""name1"", + ""location"": ""[parameters('p1')]"", + ""kind"": ""[parameters('p1')]"", + ""sku"": { + ""name"": ""[parameters('P1')]"" + } + }"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name1' + location: p1 + kind: p1 + sku: { + name: p1 + } + }"); + } + + [TestMethod] + public async Task MissingParameterVariable_CollidesWithResourceName_ShouldSucceed() + { + var json = @" { + ""type"": ""Microsoft.Storage/storageAccounts"", + ""apiVersion"": ""2021-02-01"", + ""name"": ""v1"", + ""location"": ""[variables('v1')]"", + ""kind"": ""[parameters('v1')]"", + ""sku"": { + ""name"": ""Premium_LRS"" + } + }"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + resource v1 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'v1' + location: v1_var + kind: v1_param + sku: { + name: 'Premium_LRS' + } + }"); + } + + [TestMethod] + public async Task MultilineStrings_ShouldSucceed() + { + var json = @"{ + ""type"": ""Microsoft.Compute/virtualMachines"", + ""apiVersion"": ""2018-10-01"", + ""name"": ""[variables('vmName')]"", // to customize name, change it in variables + ""location"": ""[ + parameters('location') + ]"", +}"; + + await TestDecompileForPaste( + json: json, + expectedPasteType: PasteType.SingleResource, + expectedErrorMessage: null, + expectedBicep: @" + resource vm 'Microsoft.Compute/virtualMachines@2018-10-01' = { + name: vmName + location: location + } +"); + } + + [DataTestMethod] + [DataRow( + @"""just a string with double quotes""", + @"'just a string with double quotes'", + DisplayName = "String with double quotes" + )] + [DataRow( + @"{""hello"": ""there""}", + @"{ + hello: 'there' + }", + DisplayName = "simple object" + )] + [DataRow( + @"{""hello there"": ""again""}", + @"{ + 'hello there': 'again' + }", + DisplayName = "object with properties needing quotes" + )] + [DataRow( + @"""[resourceGroup().location]""", + @"resourceGroup().location", + DisplayName = "String with ARM expression" + )] + [DataRow( + @"[""[resourceGroup().location]""]", + """ + [ + resourceGroup().location + ] + """, + DisplayName = "Array with string expression" + )] + [DataRow( + @"""[concat(variables('leftBracket'), 'dbo', variables('rightBracket'), '.', variables('leftBracket'), 'table', variables('rightBracket')) ]""", + @"'${leftBracket}dbo${rightBracket}.${leftBracket}table${rightBracket}'", + DisplayName = "concat changes to interpolated string" + )] + [DataRow( + @"""[concat('Correctly escaped single quotes ''here'' and ''''here'''' ', variables('and'), ' ''wherever''')]""", + @"'Correctly escaped single quotes \'here\' and \'\'here\'\' ${and} \'wherever\''", + DisplayName = "Correctly escaped single quotes" + )] + [DataRow( + @"""[concat('string', ' ', 'string')]""", + @"'string string'", + DisplayName = "Concat is simplified" + )] + [DataRow( + @"""[concat('''Something in single quotes - '' ', 'and something not ', variables('v1'))]""", + @"'\'Something in single quotes - \' and something not ${v1}'", + DisplayName = "Escaped and unescaped single quotes in string" + )] + [DataRow( + @"""[[this will be in brackets, not an expression - variables('blobName') should not be converted to Bicep, but single quotes should be escaped]""", + @"'[this will be in brackets, not an expression - variables(\'blobName\') should not be converted to Bicep, but single quotes should be escaped]'", + DisplayName = "string starting with [[ is not an expression, [[ should get converted to single [" + )] + [DataRow( + @"""[json(concat('{\""storageAccountType\"": \""Premium_LRS\""}'))]""", + @"json('{""storageAccountType"": ""Premium_LRS""}')", + DisplayName = "double quotes inside strings inside object" + )] + [DataRow( + @"""[concat(variables('blobName'),parameters('blobName'))]""", + "concat(blobName, blobName_param)", + DisplayName = "param and variable with same name" + )] + [DataRow( + @"""Double quotes \""here\""""", + @"'Double quotes ""here""'", + DisplayName = "Double quotes in string" + )] + [DataRow( + @"'Double quotes \""here\""'", + @"'Double quotes ""here""'", + DisplayName = "Double quotes in single-quote string" + )] + [DataRow( + @"""['Double quotes \""here\""']""", + @"'Double quotes ""here""'", + DisplayName = "Double quotes in string inside string expression" + )] + [DataRow( + @""" [A string that has whitespace before the bracket is not an expression]""", + @"' [A string that has whitespace before the bracket is not an expression]'", + DisplayName = "Whitespace before the bracket" + )] + [DataRow( + @"""[A string that has whitespace after the bracket is not an expression] """, + @"'[A string that has whitespace after the bracket is not an expression] '", + DisplayName = "Whitespace after the bracket" + )] + [DataRow( + @"[ + 1, 2, + 3 +]", + """ + [ + 1 + 2 + 3 + ] + """, + DisplayName = "Multiline array" + )] + [DataRow( + " \t \"abc\" \t ", + "'abc'", + DisplayName = "Whitespace before/after")] + [DataRow( + " /*comment*/\n // another comment\r\n /*hi*/\"abc\"// there\n/*and another*/ // but wait, there's more!\n// end", + "'abc'", + DisplayName = "Comments before/after")] + [DataRow( + "\t\"abc\"\t", + "'abc'", + DisplayName = "Tabs before/after")] + [DataRow( + "\"2012-03-21T05:40Z\"", + "'2012-03-21T05:40Z'", + DisplayName = "datetime string")] + public async Task JsonValue_Valid_ShouldSucceed(string json, string expectedBicep) + { + await TestDecompileForPaste( + json: json, + expectedBicep is null ? PasteType.None : PasteType.JsonValue, + expectedErrorMessage: null, + expectedBicep: expectedBicep); + } + + [DataRow( + @"{ + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: 'subnetRef' + } + privateIPAllocationMethod: 'Dynamic' + publicIpAddress: { + id: resourceId('Microsoft.Network/publicIPAddresses', 'publicIPAddressName') + } + } + } + ] + networkSecurityGroup: { + id: resourceId('Microsoft.Network/networkSecurityGroups', 'networkSecurityGroupName') + } + }", + DisplayName = "Bicep object" + )] + [DataRow( + @"3/14/2001", + DisplayName = "Date" + )] + [DataRow( + @"""kubernetesVersion"": ""1.15.7""", + DisplayName = "\"property\": \"value\"" + )] + [DataRow( + @"// hello there", + DisplayName = "single-line comment" + )] + [DataRow( + @"/* hello + there */", + DisplayName = "multi-line comment" + )] + [DataRow( + @"resourceGroup().location", + DisplayName = "Invalid JSON expression" + )] + [DataRow( + @"[resourceGroup().location]", + DisplayName = "Invalid JSON expression inside array" + )] + [DataRow( + @"[concat('Unescaped single quotes 'here' ')]", + DisplayName = "Invalid unescaped single quotes" + )] + [DataRow( + @"", + DisplayName = "Empty")] + [DataRow( + " \t ", + DisplayName = "just whitespace")] + [DataRow( + @" /* hello there! */ // more comment", + DisplayName = "just a comment")] + [DataRow( + "2012-03-21T05:40Z", + DisplayName = "datetime")] + public async Task JsonValue_Invalid_CantConvert(string json) + { + await TestDecompileForPaste( + json: json, + PasteType.None, + expectedErrorMessage: null, + expectedBicep: null); + } + + [DataTestMethod] + [DataRow( + @"{ abc: 1, def: 'def' }", // this is not technically valid JSON but the Newtonsoft parser accepts it anyway and it is already valid Bicep + PasteType.BicepValue, // Valid json and valid Bicep expression + @"{ + abc: 1 + def: 'def' + }")] + [DataRow( + @"{ abc: 1, /*hi*/ def: 'def' }", // this is not technically valid JSON but the Newtonsoft parser accepts it anyway and it is already valid Bicep + PasteType.BicepValue, // Valid json and valid Bicep expression + @"{ + abc: 1 + def: 'def' + }")] + [DataRow( + "[1]", + PasteType.BicepValue, // Valid json and valid Bicep expression + @"[ + 1 + ]")] + [DataRow( + "[1, 1]", + PasteType.BicepValue, // Valid json and valid Bicep expression + """ + [ + 1 + 1 + ] + """)] + [DataRow( + "[ /* */ ]", + PasteType.BicepValue, // Valid json and valid Bicep expression + "[]")] + [DataRow( + @"[ +/* */ ]", + PasteType.BicepValue, // Valid json and valid Bicep expression + "[]")] + [DataRow( + @"[ + 1]", + PasteType.BicepValue, // Valid json and valid Bicep expression + """ + [ + 1 + ] + """)] + [DataRow( + "null", + PasteType.BicepValue, // Valid json and valid Bicep expression + "null", + DisplayName = "null")] + [DataRow( + @"'just a string with single quotes'", + PasteType.BicepValue, // Valid json and valid Bicep expression + @"'just a string with single quotes'", + DisplayName = "String with single quotes" + )] + [DataRow( + @"// comment that shouldn't get removed because code is already valid Bicep + + /* another comment + + */ + '123' // yet another comment + + /* and another + */ + + ", + PasteType.BicepValue, // Valid json and valid Bicep expression (will get pasted as original for copy/paste, as '123' for "paste as Bicep" command) + "'123'", + DisplayName = "Regress #10940 Paste removes comments when copying/pasting Bicep" + )] + [DataRow( + @"param p1 string + param p2 string", + PasteType.None, // Valid Bicep, but not as a single expression + null, + DisplayName = "multiple valid Bicep statements - shouldn't be changed" + )] + [DataRow( + @"param p1 string // comment 1 + // comment 2 + param p2 string /* comment 3 */", + PasteType.None, // Valid Bicep, but not as a single expression + null, + DisplayName = "multiple valid Bicep statements with comments - shouldn't be changed" + )] + [DataRow( + @"[ + 1 + 2 + ]", + PasteType.None, // Valid Bicep but not valid JSON, therefore not converted + null, + DisplayName = "multi-line valid Bicep expression - shouldn't be changed" + )] + [DataRow( + @"[ + 1 // comment 1 + // comment 2 + 2 /* comment 3 + 3 + /* + 4 + ]", + PasteType.None, // Valid Bicep but not valid JSON, therefore not converted + null, + DisplayName = "multi-line valid Bicep expression with comments - shouldn't be changed" + )] + public async Task IsAlreadyLegalBicep(string pasted, PasteType expectedPasteType, string expectedBicep) + { + await TestDecompileForPaste( + pasted, + expectedPasteType, + expectedBicep, + expectedErrorMessage: null); + } + + [TestMethod] + public async Task Template_JsonConvertsToEmptyBicep() + { + await TestDecompileForPaste( + @"{ + ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", + ""contentVersion"": """", + ""apiProfile"": """", + ""parameters"": { }, + ""variables"": { }, + ""functions"": [ ], + ""resources"": [ ], + ""outputs"": { } +}", + PasteType.None, + expectedErrorMessage: null, + expectedBicep: null); + } + + [DataTestMethod] + [DataRow( + @"|@description('bicep string') + param s string", + PasteContext.None, + DisplayName = "simple: cursor at start of bicep file" + )] + [DataRow( + @"@description('bicep string') + param s string|", + PasteContext.None, + DisplayName = "simple: cursor at end of bicep file" + )] + [DataRow( + @"var a = |'bicep string'", + PasteContext.None, + DisplayName = "variable value: right before string's beginning single quote" + )] + [DataRow( + @"var a = '|bicep string'", + PasteContext.String, + DisplayName = "variable value: right after string's beginning single quote" + )] + [DataRow( + @"var a = 'bicep |string'", + PasteContext.String, + DisplayName = "variable value: in middle" + )] + [DataRow( + @"var a = 'bicep string|'", + PasteContext.String, + DisplayName = "variable value: right before string's ending single quote" + )] + [DataRow( + @"var a = 'bicep string'|", + PasteContext.None, + DisplayName = "variable value: right after string's ending single quote" + )] + [DataRow( + @"var a = 1 // 'not a| string'", + PasteContext.None, + DisplayName = "comments: string inside a comment" + )] + [DataRow( + @"var a = /* 'not a| string' */ 123", + PasteContext.None, + DisplayName = "comments: string inside a /**/ comment" + )] + [DataRow( + @"var a = /* + 'not a| string' */ 123 + ", + PasteContext.None, + DisplayName = "comments: string inside a multiline comment" + )] + // @description does not use a StringSyntax, we have to look for string tokens... + [DataRow( + @"@description(|'bicep string') + param s string", + PasteContext.None, + DisplayName = "@description: before beginning quote" + )] + [DataRow( + @"@description('|bicep string') + param s string", + PasteContext.String, + DisplayName = "@description: after beginning quote" + )] + [DataRow( + @"@description('bicep string|') + param s string", + PasteContext.String, + DisplayName = "@description: before end quote" + )] + [DataRow( + @"@description('bicep string'|) + param s string", + PasteContext.None, + DisplayName = "@description: after end quote" + )] + [DataRow( + @"output s string = '|'", + PasteContext.String, + DisplayName = "output s = '|'" + )] + [DataRow( + @"output s string = 'Here\|'s to you!'", + PasteContext.String, + DisplayName = "escapes: inside escaped single quotes in string" + )] + [DataRow( + @"output s string = 'This is |${aValue} interpolated value'", + PasteContext.String, + DisplayName = "interpolation: right before $" + )] + [DataRow( + @"output s string = 'This is $|{aValue} interpolated value'", + PasteContext.String, + DisplayName = "interpolation: right before value" + )] + [DataRow( + @"output s string = 'This is ${|aValue} interpolated'", + PasteContext.None, + DisplayName = "interpolation: just inside of expression" + )] + [DataRow( + @"output s string = 'This is ${aValue|} interpolated'", + PasteContext.None, + DisplayName = "interpolation: right before ending }" + )] + [DataRow( + @"output s string ='This is ${aValue}| interpolated'", + PasteContext.String, + DisplayName = "interpolation: right after ending }" + )] + [DataRow( + @"output s string ='This is ${concat(a, |'string', value)} interpolated'", + PasteContext.None, + DisplayName = "string inside interpolation: right before beginning quote" + )] + [DataRow( + @"output s string ='This is ${concat(a, '|string', value)} interpolated'", + PasteContext.String, + DisplayName = "string inside interpolation: inside string" + )] + [DataRow( + @"output s string ='This is ${concat(a, 'string'|, value)} interpolated'", + PasteContext.None, + DisplayName = "string inside interpolation: right after string" + )] + [DataRow( + @"output s string ='This is ${concat(a, 'before|${'a'}after', value)} interpolated'", + PasteContext.String, + DisplayName = "nested interpolation: right before $" + )] + [DataRow( + @"output s string ='This is ${concat(a, 'before${|'a'}after', value)} interpolated'", + PasteContext.None, + DisplayName = "nested interpolation: right before string inside interpolation" + )] + [DataRow( + "output s string ='This is ${concat(a, 'before${'|a'}after', value)} interpolated'", + PasteContext.String, + DisplayName = "nested interpolation: right inside string inside interpolation" + )] + [DataRow( + @"output s string ='This is ${concat(a, 'before${'a'|}after', value)} interpolated'", + PasteContext.None, + DisplayName = "nested interpolation: right after string inside interpolation, still inside string hole" + )] + [DataRow( + @"var s = |'''hello ${not a hole} + there '''", + PasteContext.None, + DisplayName = "multi-line string: just before starting quotes" + )] + [DataRow( + @"var s = '''|hello ${not a hole} + there '''", + PasteContext.String, + DisplayName = "multi-line string: just inside string" + )] + [DataRow( + @"var s = '''hello ${not a |hole} + there '''", + PasteContext.String, + DisplayName = "multi-line string: not a hole" + )] + [DataRow( + @"var s = '''hello ${not a hole} + there |'''", + PasteContext.String, + DisplayName = "multi-line string: just before ending quotes" + )] + [DataRow( + @"var s = '''hello ${not a hole} + there '''|", + PasteContext.None, + DisplayName = "multi-line string: just outside ending quotes" + )] + [DataRow( + @"resource stg '|Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: 'location' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + }", + PasteContext.String, + DisplayName = "resources: inside resource type" + )] + [DataRow( + @"resource stg 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: 'location' + kind: '|StorageV2' + sku: { + name: 'Premium_LRS' + } + }", + PasteContext.String, + DisplayName = "resources: inside resource property value" + )] + [DataRow( + @"resource loadBalancerPublicIPAddress 'Microsoft.Network/publicIPAddresses@2020-11-01' = { + name: 'loadBalancerName' + location: '|location' + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'static' + } + }", + PasteContext.String, + DisplayName = "resources: inside resource property value" + )] + public async Task DontPasteIntoStrings(string editorContentsWithCursor, PasteContext expectedPasteContext) + { + await TestDecompileForPaste(new( + "\"json string\"", + expectedPasteContext == PasteContext.String ? PasteType.None : PasteType.JsonValue, + expectedPasteContext, + ignoreGeneratedBicep: true, + expectedErrorMessage: null, + editorContentsWithCursor: editorContentsWithCursor + )); + + await TestDecompileForPaste(new( + @"{ + ""type"": ""Microsoft.Resources/resourceGroups"", + ""apiVersion"": ""2022-09-01"", + ""name"": ""rg"", + ""location"": ""[parameters('location')]"" + }", + expectedPasteContext == PasteContext.String ? PasteType.None : PasteType.SingleResource, + expectedPasteContext, + ignoreGeneratedBicep: true, + expectedErrorMessage: null, + editorContentsWithCursor: editorContentsWithCursor + )); + } + } +} + diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs similarity index 98% rename from src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs rename to src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs index 60415f7f227..1a4c18e82e0 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs @@ -17,7 +17,7 @@ namespace Bicep.LangServer.UnitTests.Handlers { [TestClass] - public class BicepDecompileForPasteCommandHandlerTests + public class BicepDecompileForPasteBicepParamsCommandHandlerTests { [NotNull] public TestContext? TestContext { get; set; } @@ -33,7 +33,7 @@ private BicepDecompileForPasteCommandHandler CreateHandler(LanguageServerMock se return builder.Construct(); } - + public enum PasteType { None, @@ -82,14 +82,14 @@ private async Task TestDecompileForPaste(Options options) var handler = CreateHandler(server); - var result = await handler.Handle(new BicepDecompileForPasteCommandParams(editorContentsWithPastedJson, cursorOffset, options.pastedJson.Length, options.pastedJson, queryCanPaste: false), CancellationToken.None); + var result = await handler.Handle(new(editorContentsWithPastedJson, cursorOffset, options.pastedJson.Length, options.pastedJson, queryCanPaste: false, "bicep-params"), CancellationToken.None); result.ErrorMessage.Should().Be(options.expectedErrorMessage); if (!options.ignoreGeneratedBicep) { var expectedBicep = options.expectedBicep?.Trim('\n'); - string? actualBicep = result.Bicep?.Trim('\n'); + var actualBicep = result.Bicep?.Trim('\n'); actualBicep.Should().EqualTrimmedLines(expectedBicep); } @@ -102,12 +102,12 @@ private async Task TestDecompileForPaste(Options options) result.PasteType.Should().Be(options.expectedPasteType switch { - PasteType.None => BicepDecompileForPasteCommandHandler.PasteType_None, - PasteType.FullTemplate => BicepDecompileForPasteCommandHandler.PasteType_FullTemplate, - PasteType.SingleResource => BicepDecompileForPasteCommandHandler.PasteType_SingleResource, - PasteType.ResourceList => BicepDecompileForPasteCommandHandler.PasteType_ResourceList, - PasteType.JsonValue => BicepDecompileForPasteCommandHandler.PasteType_JsonValue, - PasteType.BicepValue => BicepDecompileForPasteCommandHandler.PasteType_BicepValue, + PasteType.None => PasteType_None, + PasteType.FullTemplate => PasteType_FullTemplate, + PasteType.SingleResource => PasteType_SingleResource, + PasteType.ResourceList => PasteType_ResourceList, + PasteType.JsonValue => PasteType_JsonValue, + PasteType.BicepValue => PasteType_BicepValue, _ => throw new NotImplementedException(), }); } diff --git a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs index 699b930b7b6..ca55f1e78db 100644 --- a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs @@ -27,7 +27,8 @@ public record BicepDecompileForPasteCommandParams( int rangeOffset, int rangeLength, string jsonContent, - bool queryCanPaste // True if client is testing clipboard text for menu enabling only, false if the user actually requested a paste + bool queryCanPaste, // True if client is testing clipboard text for menu enabling only, false if the user actually requested a paste, + string languageId ); public record BicepDecompileForPasteCommandResult @@ -65,6 +66,22 @@ public enum PasteContext String, // Pasting inside of a string } + public enum LanguageId + { + Bicep, + BicepParams, + } + + private static LanguageId GetLanguageId(string languageId) + { + return languageId switch + { + "bicep" => LanguageId.Bicep, + "bicep-params" => LanguageId.BicepParams, + _ => throw new ArgumentException($"Unexpected languageId value {languageId}"), + }; + } + private record ResultAndTelemetry(BicepDecompileForPasteCommandResult Result, BicepTelemetryEvent? SuccessTelemetry); public BicepDecompileForPasteCommandHandler( @@ -88,7 +105,8 @@ public override Task Handle(BicepDecompileF parameters.rangeOffset, parameters.rangeLength, parameters.jsonContent, - parameters.queryCanPaste); + parameters.queryCanPaste, + GetLanguageId(parameters.languageId)); return (result, successTelemetry); })); } @@ -150,10 +168,10 @@ private static void Log(StringBuilder output, string message) Trace.TraceInformation(message); } - private async Task TryDecompileForPaste(string bicepContents, int rangeOffset, int rangeLength, string json, bool queryCanPaste) + private async Task TryDecompileForPaste(string bicepContents, int rangeOffset, int rangeLength, string json, bool queryCanPaste, LanguageId languageId) { StringBuilder output = new(); - string decompileId = Guid.NewGuid().ToString(); + var decompileId = Guid.NewGuid().ToString(); var pasteContext = GetPasteContext(bicepContents, rangeOffset, rangeLength); if (pasteContext == PasteContext.String) @@ -166,26 +184,32 @@ private async Task TryDecompileForPaste(string bicepContents GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType: null, bicep: null)); } - if (!string.IsNullOrWhiteSpace(json)) + if (string.IsNullOrWhiteSpace(json)) { - var (pasteType, constructedJsonTemplate) = TryConstructFullJsonTemplate(json); - if (pasteType is null) + return new ResultAndTelemetry( + new BicepDecompileForPasteCommandResult( + decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteType: null, ErrorMessage: null, + Bicep: null, Disclaimer: null), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType: null, bicep: null)); + } + + var (pasteType, constructedJsonTemplate) = languageId == LanguageId.Bicep ? TryConstructFullJsonTemplate(json) : (null, null); + if (pasteType is null) + { + // It's not a template or resource. Try treating it as a JSON value. + var resultAndTelemetry = TryConvertFromJsonValue(output, json, decompileId, pasteContext, queryCanPaste); + if (resultAndTelemetry is not null) { - // It's not a template or resource. Try treating it as a JSON value. - var resultAndTelemetry = TryConvertFromJsonValue(output, json, decompileId, pasteContext, queryCanPaste); - if (resultAndTelemetry is not null) - { - return resultAndTelemetry; - } + return resultAndTelemetry; } - else + } + else + { + // It's a full or partial template and we have converted it into a full template to parse + var result = await TryConvertFromConstructedTemplate(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); + if (result is not null) { - // It's a full or partial template and we have converted it into a full template to parse - var result = await TryConvertFromConstructedTemplate(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); - if (result is not null) - { - return result; - } + return result; } } diff --git a/src/vscode-bicep/src/commands/pasteAsBicep.ts b/src/vscode-bicep/src/commands/pasteAsBicep.ts index 114568c0ecb..ef78cc47ea5 100644 --- a/src/vscode-bicep/src/commands/pasteAsBicep.ts +++ b/src/vscode-bicep/src/commands/pasteAsBicep.ts @@ -18,7 +18,7 @@ import { } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { BicepDecompileForPasteCommandParams, BicepDecompileForPasteCommandResult } from "../language"; -import { bicepConfigurationKeys, bicepLanguageId } from "../language/constants"; +import { bicepConfigurationKeys, bicepLanguageId, bicepParamLanguageId } from "../language/constants"; import { getBicepConfiguration } from "../language/getBicepConfiguration"; import { areEqualIgnoringWhitespace } from "../utils/areEqualIgnoringWhitespace"; import { Disposable } from "../utils/disposable"; @@ -50,13 +50,13 @@ export class PasteAsBicepCommand implements Command { let finalPastedBicep: string | undefined; try { - documentUri = await findOrCreateActiveBicepFile(context, documentUri, "Choose which Bicep file to paste into"); + documentUri = await findOrCreateActiveBicepFile(context, documentUri, "Choose which Bicep file to paste into", true); const document = await workspace.openTextDocument(documentUri); const editor = await window.showTextDocument(document); - if (editor?.document.languageId !== bicepLanguageId) { - throw new Error("Cannot paste as Bicep: Editor is not editing a Bicep document."); + if (editor?.document.languageId !== bicepLanguageId && editor?.document.languageId !== bicepParamLanguageId) { + throw new Error("Cannot paste as Bicep: Editor is not editing a Bicep or BicepParam document."); } clipboardText = await env.clipboard.readText(); @@ -81,6 +81,7 @@ export class PasteAsBicepCommand implements Command { rangeEnd - rangeStart, clipboardText, false /* queryCanPaste */, + editor.document.languageId ); context.telemetry.properties.pasteType = result.pasteType; @@ -130,6 +131,7 @@ export class PasteAsBicepCommand implements Command { rangeLength: number, jsonContent: string, queryCanPaste: boolean, + languageId: string, ): Promise { return await withProgressAfterDelay( { @@ -144,6 +146,7 @@ export class PasteAsBicepCommand implements Command { rangeLength, jsonContent, queryCanPaste, + languageId }; const decompileResult: BicepDecompileForPasteCommandResult = await this.client.sendRequest( "workspace/executeCommand", @@ -183,7 +186,7 @@ export class PasteAsBicepCommand implements Command { e.reason !== TextDocumentChangeReason.Redo && e.reason !== TextDocumentChangeReason.Undo && e.document === editor?.document && - e.document.languageId === bicepLanguageId && + (e.document.languageId === bicepLanguageId || e.document.languageId === bicepParamLanguageId ) && e.contentChanges.length === 1 ) { const contentChange = e.contentChanges[0]; @@ -230,6 +233,7 @@ export class PasteAsBicepCommand implements Command { formattedPastedText.length, clipboardText, true, // queryCanPaste + e.document.languageId ); if (!canPasteResult.pasteType) { // Nothing we know how to convert, or pasting is not allowed in this context diff --git a/src/vscode-bicep/src/language/protocol.ts b/src/vscode-bicep/src/language/protocol.ts index 23d0c6de4f6..c07fa415113 100644 --- a/src/vscode-bicep/src/language/protocol.ts +++ b/src/vscode-bicep/src/language/protocol.ts @@ -250,6 +250,7 @@ export interface BicepDecompileForPasteCommandParams { rangeLength: number; jsonContent: string; queryCanPaste: boolean; + languageId: string; } export interface BicepDecompileForPasteCommandResult { From 12a56fdfde22e70f1cdf8b91f938a6ae85384378 Mon Sep 17 00:00:00 2001 From: Mikolaj Mackowiak <7921224+miqm@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:41:56 +0100 Subject: [PATCH 2/5] Decompiling json params in paste --- src/Bicep.Core/LanguageConstants.cs | 1 + ...compileForPasteBicepCommandHandlerTests.cs | 12 +- ...eForPasteBicepParamsCommandHandlerTests.cs | 12 +- .../BicepDecompileForPasteCommandHandler.cs | 329 ++++++++++++------ src/vscode-bicep/src/language/protocol.ts | 2 +- 5 files changed, 236 insertions(+), 120 deletions(-) diff --git a/src/Bicep.Core/LanguageConstants.cs b/src/Bicep.Core/LanguageConstants.cs index 2e23d8dbe63..aac4d0a50b8 100644 --- a/src/Bicep.Core/LanguageConstants.cs +++ b/src/Bicep.Core/LanguageConstants.cs @@ -99,6 +99,7 @@ public static class LanguageConstants public const string DisableNextLineDiagnosticsKeyword = "disable-next-line"; public static readonly Regex ArmTemplateSchemaRegex = new(@"https?:\/\/schema\.management\.azure\.com\/schemas\/([^""\/]+\/[a-zA-Z]*[dD]eploymentTemplate\.json)#?"); + public static readonly Regex ArmParametersSchemaRegex = new(@"https?:\/\/schema\.management\.azure\.com\/schemas\/([^""\/]+\/[dD]eploymentParameters\.json)#?"); public static readonly ImmutableSortedSet DeclarationKeywords = ImmutableSortedSet.Create( StringComparer.Ordinal, diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs index b7d30d45c4f..659c9ab2af3 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs @@ -102,12 +102,12 @@ private async Task TestDecompileForPaste(Options options) result.PasteType.Should().Be(options.expectedPasteType switch { - PasteType.None => PasteType_None, - PasteType.FullTemplate => PasteType_FullTemplate, - PasteType.SingleResource => PasteType_SingleResource, - PasteType.ResourceList => PasteType_ResourceList, - PasteType.JsonValue => PasteType_JsonValue, - PasteType.BicepValue => PasteType_BicepValue, + PasteType.None => "none", + PasteType.FullTemplate => "fullTemplate", + PasteType.SingleResource => "singleResource", + PasteType.ResourceList => "resourceList", + PasteType.JsonValue => "jsonValue", + PasteType.BicepValue => "bicepValue", _ => throw new NotImplementedException(), }); } diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs index 1a4c18e82e0..b6e7a87da67 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs @@ -102,12 +102,12 @@ private async Task TestDecompileForPaste(Options options) result.PasteType.Should().Be(options.expectedPasteType switch { - PasteType.None => PasteType_None, - PasteType.FullTemplate => PasteType_FullTemplate, - PasteType.SingleResource => PasteType_SingleResource, - PasteType.ResourceList => PasteType_ResourceList, - PasteType.JsonValue => PasteType_JsonValue, - PasteType.BicepValue => PasteType_BicepValue, + PasteType.None => "none", + PasteType.FullTemplate => "fullTemplate", + PasteType.SingleResource => "singleResource", + PasteType.ResourceList => "resourceList", + PasteType.JsonValue => "jsonValue", + PasteType.BicepValue => "bicepValue", _ => throw new NotImplementedException(), }); } diff --git a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs index ca55f1e78db..bf305cfe19a 100644 --- a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs @@ -42,6 +42,17 @@ public record BicepDecompileForPasteCommandResult string? Disclaimer ); + public enum PasteType + { + None, + FullTemplate, // Full template + SingleResource, // Single resource + ResourceList,// List of multiple resources + JsonValue, // Single JSON value (number, object, array etc) + BicepValue, // JSON value that is also valid Bicep (e.g. "[1, {}]") + FullParams + } + /// /// Handles a request from the client to analyze/decompile a JSON fragment for possible conversion into Bicep (for pasting into a Bicep file) /// @@ -52,13 +63,7 @@ public class BicepDecompileForPasteCommandHandler : ExecuteTypedResponseCommandH private static readonly Uri JsonDummyUri = new("file:///from-clipboard.json", UriKind.Absolute); private static readonly Uri BicepDummyUri = PathHelper.ChangeToBicepExtension(JsonDummyUri); - - public const string? PasteType_None = null; - public const string PasteType_FullTemplate = "fullTemplate"; // Full template - public const string PasteType_SingleResource = "resource"; // Single resource - public const string PasteType_ResourceList = "resourceList"; // List of multiple resources - public const string PasteType_JsonValue = "jsonValue"; // Single JSON value (number, object, array etc) - public const string PasteType_BicepValue = "bicepValue"; // JSON value that is also valid Bicep (e.g. "[1, {}]") + private static readonly Uri BicepParamsDummyUri = PathHelper.ChangeToBicepparamExtension(JsonDummyUri); public enum PasteContext { @@ -66,6 +71,7 @@ public enum PasteContext String, // Pasting inside of a string } + public enum LanguageId { Bicep, @@ -82,6 +88,7 @@ private static LanguageId GetLanguageId(string languageId) }; } + private record ResultAndTelemetry(BicepDecompileForPasteCommandResult Result, BicepTelemetryEvent? SuccessTelemetry); public BicepDecompileForPasteCommandHandler( @@ -92,7 +99,7 @@ BicepCompiler bicepCompiler ) : base(LangServerConstants.DecompileForPasteCommand, serializer) { - this.telemetryHelper = new TelemetryAndErrorHandlingHelper(server.Window, telemetryProvider); + this.telemetryHelper = new(server.Window, telemetryProvider); this.bicepCompiler = bicepCompiler; } @@ -177,51 +184,70 @@ private async Task TryDecompileForPaste(string bicepContents if (pasteContext == PasteContext.String) { // Don't convert to Bicep if inside a string - return new ResultAndTelemetry( - new BicepDecompileForPasteCommandResult( - decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteType: null, ErrorMessage: null, + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteType: null, ErrorMessage: null, Bicep: null, Disclaimer: null), GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType: null, bicep: null)); } if (string.IsNullOrWhiteSpace(json)) { - return new ResultAndTelemetry( - new BicepDecompileForPasteCommandResult( - decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteType: null, ErrorMessage: null, + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteType: null, ErrorMessage: null, Bicep: null, Disclaimer: null), GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType: null, bicep: null)); } - var (pasteType, constructedJsonTemplate) = languageId == LanguageId.Bicep ? TryConstructFullJsonTemplate(json) : (null, null); - if (pasteType is null) + var (pasteType, constructedJsonTemplate) = languageId switch + { + LanguageId.Bicep => TryConstructFullJsonTemplate(json), + LanguageId.BicepParams => TryConstructFullJsonParams(json), + _ => (PasteType.None, null), + }; + switch (pasteType) { - // It's not a template or resource. Try treating it as a JSON value. - var resultAndTelemetry = TryConvertFromJsonValue(output, json, decompileId, pasteContext, queryCanPaste); - if (resultAndTelemetry is not null) + case PasteType.None: { - return resultAndTelemetry; + // It's not a template or resource. Try treating it as a JSON value. + var resultAndTelemetry = TryConvertFromJsonValue(output, json, decompileId, pasteContext, queryCanPaste); + if (resultAndTelemetry is not null) + { + return resultAndTelemetry; + } + + break; } - } - else - { - // It's a full or partial template and we have converted it into a full template to parse - var result = await TryConvertFromConstructedTemplate(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); - if (result is not null) + case PasteType.FullParams: { - return result; + // It's a full parameters file + var result = TryConvertFromConstructedParameters(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); + if (result is not null) + { + return result; + } + + break; + } + default: + { + // It's a full or partial template and we have converted it into a full template to parse + var result = await TryConvertFromConstructedTemplate(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); + if (result is not null) + { + return result; + } + + break; } } // It's not anything we know how to convert to Bicep - return new ResultAndTelemetry( - new BicepDecompileForPasteCommandResult( + return new( + new( decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteType: null, ErrorMessage: null, Bicep: null, Disclaimer: null), GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType: null, bicep: null)); } - private async Task TryConvertFromConstructedTemplate(StringBuilder output, string json, string decompileId, PasteContext pasteContext, string pasteType, bool queryCanPaste, string? constructedJsonTemplate) + private async Task TryConvertFromConstructedTemplate(StringBuilder output, string json, string decompileId, PasteContext pasteContext, PasteType pasteType, bool queryCanPaste, string? constructedJsonTemplate) { ImmutableDictionary filesToSave; try @@ -242,9 +268,8 @@ private async Task TryDecompileForPaste(string bicepContents var message = ex.Message; Log(output, $"Decompilation failed: {message}"); - return new ResultAndTelemetry( - new BicepDecompileForPasteCommandResult(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType, message, Bicep: null, Disclaimer: null), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType, bicep: null)); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), message, Bicep: null, Disclaimer: null), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicep: null)); } // Get Bicep output from the main file (all others are currently ignored) @@ -260,18 +285,58 @@ private async Task TryDecompileForPaste(string bicepContents // Show disclaimer and return result Log(output, DisclaimerMessage); - return new ResultAndTelemetry( - new BicepDecompileForPasteCommandResult(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType, null, bicepOutput, DisclaimerMessage), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType, bicepOutput)); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), null, bicepOutput, DisclaimerMessage), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicepOutput)); + } + + private ResultAndTelemetry? TryConvertFromConstructedParameters(StringBuilder output, string json, string decompileId, PasteContext pasteContext, PasteType pasteType, bool queryCanPaste, string? constructedJsonTemplate) + { + ImmutableDictionary filesToSave; + try + { + // Decompile the full template + Debug.Assert(constructedJsonTemplate is not null); + Log(output, string.Format(LangServerResources.Decompile_DecompilationStartMsg, "clipboard text")); + + var decompiler = new BicepDecompiler(this.bicepCompiler); + (_, filesToSave) = decompiler.DecompileParameters(constructedJsonTemplate, BicepParamsDummyUri, null); + } + catch (Exception ex) + { + // We don't ever throw. If we reached here, the pasted text was in a format we think we can handle but there was some + // sort of error. Tell the client it can be pasted and let the client show the end user the error if they do. + // deal with any bicep errors found. + var message = ex.Message; + Log(output, $"Decompilation failed: {message}"); + + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), message, Bicep: null, Disclaimer: null), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicep: null)); + } + + // Get Bicep output from the main file (all others are currently ignored) + var bicepOutput = filesToSave.Single(kvp => BicepParamsDummyUri.Equals(kvp.Key)).Value; + + if (string.IsNullOrWhiteSpace(bicepOutput)) + { + return null; + } + + // Ensure ends with newline + bicepOutput = bicepOutput.TrimEnd() + "\n"; + + // Show disclaimer and return result + Log(output, DisclaimerMessage); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), null, bicepOutput, DisclaimerMessage), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicepOutput)); } - private string? PasteContextAsString(PasteContext pasteContext) + private static string PasteContextAsString(PasteContext pasteContext) { return pasteContext switch { PasteContext.None => "none", PasteContext.String => "string", - _ => throw new Exception($"Unexpected pasteContext value {pasteContext}"), + _ => throw new($"Unexpected pasteContext value {pasteContext}"), }; } @@ -286,111 +351,109 @@ private async Task TryDecompileForPaste(string bicepContents BicepTelemetryEvent.DecompileForPaste(decompileId, PasteContextAsString(pasteContext), pasteType, json.Length, bicep?.Length); } - private static DecompileOptions GetDecompileOptions(string pasteType) + private static DecompileOptions GetDecompileOptions(PasteType pasteType) { - return new DecompileOptions() + return new() { // For partial template pastes, we don't error out on missing parameters and variables because they won't // ever have definitions in the pasted portion - AllowMissingParamsAndVars = pasteType != PasteType_FullTemplate, + AllowMissingParamsAndVars = pasteType != PasteType.FullTemplate, // ... but don't allow them in nested templates, which should be fully complete and valid AllowMissingParamsAndVarsInNestedTemplates = false, - IgnoreTrailingInput = pasteType != PasteType_JsonValue, + IgnoreTrailingInput = pasteType != PasteType.JsonValue, }; } private ResultAndTelemetry? TryConvertFromJsonValue(StringBuilder output, string json, string decompileId, PasteContext pasteContext, bool queryCanPaste) { // Is it valid JSON that we can convert into Bicep? - var pasteType = PasteType_JsonValue; + var pasteType = PasteType.JsonValue; var options = GetDecompileOptions(pasteType); var bicep = BicepDecompiler.DecompileJsonValue(json, options); - if (bicep is not null) + if (bicep is null) { - // Technically we've already converted, but we only want to show this message if we think the pasted text is convertible - Log(output, String.Format(LangServerResources.Decompile_DecompilationStartMsg, "clipboard text")); + return null; + } + + // Technically we've already converted, but we only want to show this message if we think the pasted text is convertible + Log(output, string.Format(LangServerResources.Decompile_DecompilationStartMsg, "clipboard text")); - // Even though it's valid JSON, it might also be valid Bicep, in which case we want to leave it alone if we're - // doing an automatic copy/paste conversion. + // Even though it's valid JSON, it might also be valid Bicep, in which case we want to leave it alone if we're + // doing an automatic copy/paste conversion. - // Is the input already a valid Bicep expression with comments removed? - var parser = new Parser("var v = " + json); + // Is the input already a valid Bicep expression with comments removed? + var parser = new Parser("var v = " + json); + _ = parser.Program(); + if (!parser.LexingErrorLookup.Any() && !parser.ParsingErrorLookup.Any()) + { + // We still want to have the converted bicep available (via the "bicep" output) in the case + // that the user is explicitly doing a Paste as Bicep command, so allow "bicep" to keep its value. + pasteType = PasteType.BicepValue; + } + else + { + // An edge case - it could be a valid Bicep expression with comments and newlines. This would be + // valid if pasting inside a multi-line array. We don't want to convert it in this case because the + // comments would be removed by the decompiler. + parser = new("var v = [\n" + json + "\n]"); _ = parser.Program(); + if (!parser.LexingErrorLookup.Any() && !parser.ParsingErrorLookup.Any()) { // We still want to have the converted bicep available (via the "bicep" output) in the case // that the user is explicitly doing a Paste as Bicep command, so allow "bicep" to keep its value. - pasteType = PasteType_BicepValue; - } - else - { - // An edge case - it could be a valid Bicep expression with comments and newlines. This would be - // valid if pasting inside a multi-line array. We don't want to convert it in this case because the - // comments would be removed by the decompiler. - parser = new Parser("var v = [\n" + json + "\n]"); - _ = parser.Program(); - - if (!parser.LexingErrorLookup.Any() && !parser.ParsingErrorLookup.Any()) - { - // We still want to have the converted bicep available (via the "bicep" output) in the case - // that the user is explicitly doing a Paste as Bicep command, so allow "bicep" to keep its value. - pasteType = PasteType_BicepValue; - } + pasteType = PasteType.BicepValue; } - - return new ResultAndTelemetry( - new BicepDecompileForPasteCommandResult( - decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType, - ErrorMessage: null, bicep, Disclaimer: null), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType, bicep)); } - return null; + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), + ErrorMessage: null, bicep, Disclaimer: null), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicep)); + } /// /// If the given JSON matches a pattern that we know how to paste as Bicep, convert it into a full template to be decompiled /// - private (string? pasteType, string? fullJsonTemplate) TryConstructFullJsonTemplate(string json) + private (PasteType pasteType, string? fullJsonTemplate) TryConstructFullJsonTemplate(string json) { using var streamReader = new StringReader(json); using var reader = new JsonTextReader(streamReader); reader.SupportMultipleContent = true; // Allows for handling of lists of resources separated by commas - if (LoadValue(reader, readFirst: true) is JToken value) + if (LoadValue(reader, readFirst: true) is not { } value) { - if (value.Type == JTokenType.Object) - { - var obj = (JObject)value; - if (TryGetStringProperty(obj, "$schema") is string schema) - { - // Template converter will do a more thorough check, we just want to know if it *looks* like a template - var looksLikeArmSchema = LanguageConstants.ArmTemplateSchemaRegex.IsMatch(schema); - if (looksLikeArmSchema) - { - return this.ConstructFullTemplateFromTemplateObject(json); - } - else - { - return (PasteType_None, null); - } - } + return (PasteType.None, null); + } - // If it's a resource object or a list of resource objects, accept it - if (IsResourceObject(obj)) - { - return ConstructFullTemplateFromSequenceOfResources(obj, reader); - } + if (value.Type != JTokenType.Object) + { + return (PasteType.None, null); + } + + var obj = (JObject)value; + if (TryGetStringProperty(obj, "$schema") is { } schema) + { + // Template converter will do a more thorough check, we just want to know if it *looks* like a template + var looksLikeArmSchema = LanguageConstants.ArmTemplateSchemaRegex.IsMatch(schema); + if (looksLikeArmSchema) + { + // Json is already a full template + return (PasteType.FullTemplate, json); + } + else + { + return (PasteType.None, null); } } - return (PasteType_None, null); - } + // If it's a resource object or a list of resource objects, accept it + if (IsResourceObject(obj)) + { + return ConstructFullTemplateFromSequenceOfResources(obj, reader); + } - private (string pasteType, string constructedJsonTemplate) ConstructFullTemplateFromTemplateObject(string json) - { - // Json is already a full template - return (PasteType_FullTemplate, json); + return (PasteType.None, null); } /// @@ -407,14 +470,14 @@ private static DecompileOptions GetDecompileOptions(string pasteType) /// /// Note that this is not a valid JSON construct by itself, unless it's just a single resource /// - private static (string pasteType, string constructedJsonTemplate) ConstructFullTemplateFromSequenceOfResources(JObject firstResourceObject, JsonTextReader reader) + private static (PasteType pasteType, string constructedJsonTemplate) ConstructFullTemplateFromSequenceOfResources(JObject firstResourceObject, JsonTextReader reader) { Debug.Assert(IsResourceObject(firstResourceObject)); Debug.Assert(reader.TokenType == JsonToken.EndObject, "Reader should be on end squiggly of first resource object"); var resourceObjects = new List(); - JObject? obj = firstResourceObject; + var obj = firstResourceObject; while (obj is not null) { if (IsResourceObject(obj)) @@ -446,12 +509,12 @@ private static (string pasteType, string constructedJsonTemplate) ConstructFullT } var resourcesAsJson = string.Join(",\n", resourceObjects.Select(ro => ro.ToString())); - var templateJson = @"{ ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", ""contentVersion"": ""1.0.0.0"", ""resources"": [" + + var templateJson = """{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "resources": [""" + resourcesAsJson + "]}"; return ( - resourceObjects.Count == 1 ? PasteType_SingleResource : PasteType_ResourceList, + resourceObjects.Count == 1 ? PasteType.SingleResource : PasteType.ResourceList, templateJson ); } @@ -485,7 +548,7 @@ private static bool IsResourceObject(JObject? obj) return null; } - return JToken.Load(reader, new JsonLoadSettings + return JToken.Load(reader, new() { CommentHandling = CommentHandling.Ignore, }); @@ -501,5 +564,57 @@ private static bool IsResourceObject(JObject? obj) private static string? TryGetStringProperty(JObject obj, string name) => (TryGetProperty(obj, name)?.Value as JValue)?.Value as string; + + /// + /// If the given JSON matches a pattern that we know how to paste as Bicep, convert it into a full json params to be decompiled + /// + private (PasteType pasteType, string? fullJsonTemplate) TryConstructFullJsonParams(string json) + { + using var streamReader = new StringReader(json); + using var reader = new JsonTextReader(streamReader); + reader.SupportMultipleContent = true; // Allows for handling of lists of resources separated by commas + + if (LoadValue(reader, readFirst: true) is not { } value) + { + return (PasteType.None, null); + } + + if (value.Type != JTokenType.Object) + { + return (PasteType.None, null); + } + + var obj = (JObject)value; + if (TryGetStringProperty(obj, "$schema") is not { } schema) + { + return (PasteType.None, null); + } + + // Template converter will do a more thorough check, we just want to know if it *looks* like a template + var looksLikeArmSchema = LanguageConstants.ArmParametersSchemaRegex.IsMatch(schema); + if (looksLikeArmSchema) + { + // Json is already a full json params + return (PasteType.FullParams, json); + } + + return (PasteType.None, null); + + } + } + + public static class BicepDecompileForPasteCommandHandlerExtensions + { + public static string? ConvertToString(this PasteType pasteType) => pasteType switch + { + PasteType.None => null, + PasteType.FullTemplate => "fullTemplate", + PasteType.SingleResource => "resource", + PasteType.ResourceList => "resourceList", + PasteType.JsonValue => "jsonValue", + PasteType.BicepValue => "bicepValue", + PasteType.FullParams => "fullParams", + _ => throw new($"Unexpected pasteType value {pasteType}"), + }; } } diff --git a/src/vscode-bicep/src/language/protocol.ts b/src/vscode-bicep/src/language/protocol.ts index c07fa415113..b6676b79ab1 100644 --- a/src/vscode-bicep/src/language/protocol.ts +++ b/src/vscode-bicep/src/language/protocol.ts @@ -259,7 +259,7 @@ export interface BicepDecompileForPasteCommandResult { errorMessage?: string; pasteContext?: "none" | "string"; // undefined if can't be pasted - pasteType: undefined | "fullTemplate" | "resource" | "resourceList" | "jsonValue" | "bicepValue"; + pasteType: undefined | "fullTemplate" | "resource" | "resourceList" | "jsonValue" | "bicepValue" | "fullParams"; bicep?: string; disclaimer?: string; } From 0ae7f0f7fbf4ea104f5f69242707fccfd7a35835 Mon Sep 17 00:00:00 2001 From: Mikolaj Mackowiak <7921224+miqm@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:14:53 +0100 Subject: [PATCH 3/5] Tests of pasting json parameters --- src/Bicep.Decompiler/BicepDecompiler.cs | 33 +- src/Bicep.Decompiler/DecompileOptions.cs | 5 + ...eForPasteBicepParamsCommandHandlerTests.cs | 1829 ++++------------- ...epDecompileForPasteCommandHandlerTests.cs} | 103 +- .../BicepDecompileForPasteCommandHandler.cs | 218 +- .../src/commands/decompileParams.ts | 4 +- 6 files changed, 633 insertions(+), 1559 deletions(-) rename src/Bicep.LangServer.UnitTests/Handlers/{BicepDecompileForPasteBicepCommandHandlerTests.cs => BicepDecompileForPasteCommandHandlerTests.cs} (96%) diff --git a/src/Bicep.Decompiler/BicepDecompiler.cs b/src/Bicep.Decompiler/BicepDecompiler.cs index 854c5c0f20a..80bc1e3f92b 100644 --- a/src/Bicep.Decompiler/BicepDecompiler.cs +++ b/src/Bicep.Decompiler/BicepDecompiler.cs @@ -57,41 +57,46 @@ public async Task Decompile(Uri bicepUri, string jsonContent, D bicepUri, this.PrintFiles(workspace)); } - public DecompileResult DecompileParameters(string contents, Uri entryBicepparamUri, Uri? bicepFileUri) + public DecompileResult DecompileParameters(string contents, Uri entryBicepparamUri, Uri? bicepFileUri, DecompileParamOptions? options = null) { + options ??= new(); + var workspace = new Workspace(); - var program = DecompileParametersFile(contents, entryBicepparamUri, bicepFileUri); + var program = DecompileParametersFile(contents, entryBicepparamUri, bicepFileUri, options); var bicepparamFile = SourceFileFactory.CreateBicepParamFile(entryBicepparamUri, program.ToString()); workspace.UpsertSourceFile(bicepparamFile); - return new DecompileResult(entryBicepparamUri, this.PrintFiles(workspace)); + return new(entryBicepparamUri, this.PrintFiles(workspace)); } - private ProgramSyntax DecompileParametersFile(string jsonInput, Uri entryBicepparamUri, Uri? bicepFileUri) + private ProgramSyntax DecompileParametersFile(string jsonInput, Uri entryBicepparamUri, Uri? bicepFileUri, DecompileParamOptions options) { var statements = new List(); - var jsonObject = JTokenHelpers.LoadJson(jsonInput, JObject.Load, ignoreTrailingContent: false); - var bicepPath = bicepFileUri is { } ? PathHelper.GetRelativePath(entryBicepparamUri, bicepFileUri) : null; + var jsonObject = JTokenHelpers.LoadJson(jsonInput, JObject.Load, ignoreTrailingContent: options.IgnoreTrailingInput); - statements.Add(new UsingDeclarationSyntax( - SyntaxFactory.UsingKeywordToken, - bicepPath is { } ? - SyntaxFactory.CreateStringLiteral(bicepPath) : - SyntaxFactory.CreateStringLiteralWithComment("", "TODO: Provide a path to a bicep template"))); + if (options.IncludeUsingDeclaration) + { + var bicepPath = bicepFileUri is not null ? PathHelper.GetRelativePath(entryBicepparamUri, bicepFileUri) : null; + statements.Add(new UsingDeclarationSyntax( + SyntaxFactory.UsingKeywordToken, + bicepPath is not null + ? SyntaxFactory.CreateStringLiteral(bicepPath) + : SyntaxFactory.CreateStringLiteralWithComment("", "TODO: Provide a path to a bicep template"))); - statements.Add(SyntaxFactory.DoubleNewlineToken); + statements.Add(SyntaxFactory.DoubleNewlineToken); + } var parameters = (TemplateHelpers.GetProperty(jsonObject, "parameters")?.Value as JObject ?? new JObject()).Properties(); foreach (var parameter in parameters) { - var metadata = parameter.Value?["metadata"]; + var metadata = parameter.Value["metadata"]; - if (metadata is { }) + if (metadata is not null) { statements.Add(ParseParameterWithComment(metadata)); statements.Add(SyntaxFactory.NewlineToken); diff --git a/src/Bicep.Decompiler/DecompileOptions.cs b/src/Bicep.Decompiler/DecompileOptions.cs index 488bce4d074..6e5f0d1c6c9 100644 --- a/src/Bicep.Decompiler/DecompileOptions.cs +++ b/src/Bicep.Decompiler/DecompileOptions.cs @@ -8,3 +8,8 @@ public record DecompileOptions( bool AllowMissingParamsAndVarsInNestedTemplates = false, bool IgnoreTrailingInput = true ); + +public record DecompileParamOptions( + bool IgnoreTrailingInput = true, + bool IncludeUsingDeclaration = true +); diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs index b6e7a87da67..4693f0b37b7 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs @@ -17,7 +17,7 @@ namespace Bicep.LangServer.UnitTests.Handlers { [TestClass] - public class BicepDecompileForPasteBicepParamsCommandHandlerTests + public class BicepDecompileForPasteParamsCommandHandlerTests { [NotNull] public TestContext? TestContext { get; set; } @@ -33,18 +33,21 @@ private BicepDecompileForPasteCommandHandler CreateHandler(LanguageServerMock se return builder.Construct(); } - + public enum PasteType { None, - FullTemplate, - SingleResource, - ResourceList, JsonValue, BicepValue, + FullParams + } + public enum PasteContext + { + None, + String } - record Options( + private record Options( string pastedJson, PasteType? expectedPasteType = null, PasteContext expectedPasteContext = PasteContext.None, @@ -60,7 +63,7 @@ private async Task TestDecompileForPaste( string? expectedErrorMessage = null, string? editorContentsWithCursor = null) { - await TestDecompileForPaste(new Options( + await TestDecompileForPaste(new( json, expectedPasteType, PasteContext.None, @@ -102,1199 +105,267 @@ private async Task TestDecompileForPaste(Options options) result.PasteType.Should().Be(options.expectedPasteType switch { - PasteType.None => "none", - PasteType.FullTemplate => "fullTemplate", - PasteType.SingleResource => "singleResource", - PasteType.ResourceList => "resourceList", + PasteType.None => null, PasteType.JsonValue => "jsonValue", PasteType.BicepValue => "bicepValue", - _ => throw new NotImplementedException(), - }); - } - - #region JSON/Bicep Constants - - private const string jsonFullTemplateMembers = @" - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""parameters"": { - ""location"": { - ""type"": ""string"", - ""defaultValue"": ""[resourceGroup().location]"" - } - }, - ""resources"": [ - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name"", - ""location"": ""[parameters('location')]"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - } - ]"; - private const string jsonFullTemplate = $@"{{ -{jsonFullTemplateMembers} -}}"; - - const string Resource1Json = @" { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""eastus"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }"; - const string Resource2Json = @" { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name2"", - ""location"": ""eastus"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }"; - - const string Resource1Bicep = @"resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } -}"; - const string Resource2Bicep = @"resource name2 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name2' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } -}"; - - #endregion - - [DataTestMethod] - [DataRow( - jsonFullTemplate, - PasteType.FullTemplate, - @" - param location string = resourceGroup().location - - resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: location - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }", - DisplayName = "Full template" - )] - [DataRow( - $@"{{ -{jsonFullTemplateMembers} - , extraProperty: ""hello"" -}}", - PasteType.FullTemplate, - @" - param location string = resourceGroup().location - - resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: location - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }", - DisplayName = "Extra property" - )] - [DataRow( - $@"{{ -{jsonFullTemplateMembers} -}} -}} // extra", - PasteType.FullTemplate, - @" - param location string = resourceGroup().location - - resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: location - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }", - DisplayName = "Extra brace at end (succeeds)" - )] - [DataRow( - $@"{{ -{jsonFullTemplateMembers} -}} -random characters", - PasteType.FullTemplate, - @" - param location string = resourceGroup().location - - resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: location - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }", - DisplayName = "Extra random characters at end" - )] - [DataRow( - $@"{{ -{{ // extra -{jsonFullTemplateMembers} -}} -random characters", - PasteType.None, - null, - DisplayName = "Extra brace at beginning (can't paste)" - )] - [DataRow( - $@" -random characters -{{ -{jsonFullTemplateMembers} -}}", - PasteType.None, - null, - DisplayName = "Extra random characters at beginning (can't paste)" - )] - [DataRow( - $@"{{ - ""$schema"": {{}}, - ""parameters"": {{ - ""location"": {{ - ""type"": ""string"", - ""defaultValue"": ""[resourceGroup().location]"" - }} - }} - }}", - PasteType.JsonValue, - // Treats it simply as a JSON object - $@"{{ - '$schema': {{}} - parameters: {{ - location: {{ - type: 'string' - defaultValue: resourceGroup().location - }} - }} - }}", - DisplayName = "Schema not a string" - )] - public async Task FullTemplate(string json, PasteType expectedPasteType, string expectedBicep, string? errorMessage = null) - { - await TestDecompileForPaste( - json: json, - expectedPasteType: expectedPasteType, - expectedBicep: expectedBicep, - errorMessage); - } - - [TestMethod] - public async Task PasteFullTemplate_ButNoSchema_ConvertsIntoPlainOldObject() - { - const string json = @" - { - ""contentVersion"": ""1.0.0.0"", - ""parameters"": { - ""location"": { - ""type"": ""string"", - ""defaultValue"": ""[resourceGroup().location]"" - } - }, - ""resources"": [ - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name"", - ""location"": ""[parameters('location')]"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - } - ] - }"; - var expectedBicep = @"{ -contentVersion: '1.0.0.0' -parameters: { -location: { -type: 'string' -defaultValue: resourceGroup().location -} -} -resources: [ -{ -type: 'Microsoft.Storage/storageAccounts' -apiVersion: '2021-02-01' -name: 'name' -location: location -kind: 'StorageV2' -sku: { -name: 'Premium_LRS' -} -} -] -}"; - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.JsonValue, - expectedErrorMessage: null, - expectedBicep: expectedBicep); - } - - [TestMethod] - public async Task FullTemplate_ButMissingParameter_ShouldGiveError() - { - const string json = @" - { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""parameters"": { - }, - ""resources"": [ - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name"", - ""location"": ""[parameters('location')]"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - } - ] - }"; - await TestDecompileForPaste( - json, - expectedPasteType: PasteType.FullTemplate, - expectedErrorMessage: "[12:60]: Unable to find parameter location", - expectedBicep: null); - } - - [DataTestMethod] - [DataRow( - @" - { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""metadata"": { - ""_generator"": { - ""name"": ""bicep"", - ""version"": ""0.10.61.36676"", - ""templateHash"": ""8521684133784798165"" - } - }, - ""resources"": { // whoops - { - ""type"": ""Microsoft.Compute/virtualMachines/providers/configurationProfileAssignments"", - ""apiVersion"": ""2022-05-04"", - ""name"": ""vmName/Microsoft.Automanage/default"", - ""properties"": { - ""configurationProfile"": ""/providers/Microsoft.Automanage/bestPractices/AzureBestPracticesDevTest"" - } - } - ]", - PasteType.None, - null, - null, - DisplayName = "{ instead of [" - )] - [DataRow( - @"{ - ""type"": ""Microsoft.Compute/virtualMachines/providers/configurationProfileAssignments"", - ""apiVersion"": ""2022-12-12"", - ""name"": ""name"", - ""properties"": { - ""configurationProfile"": ""[bad-expression]"" - } - }", - PasteType.SingleResource, - null, - "[6:46]: The language expression 'bad-expression' is not valid: the string character 'x' at position '5' is not expected.", - DisplayName = "Bad expression" - )] - public async Task Errors(string json, PasteType pasteType, string? expectedBicep, string? expectedErrorMessage) - { - await TestDecompileForPaste(json, pasteType, expectedBicep, expectedErrorMessage); - } - - [TestMethod] - public async Task JustString_WithNoQuotes_CantConvert() - { - string json = @"just a string"; - await TestDecompileForPaste( - json: json, - PasteType.None, - expectedErrorMessage: null, - expectedBicep: null); - } - - [TestMethod] - public async Task NonResourceObject_WrongPropertyType_Object_PastesAsSimpleObject() - { - string json = @$" - {Resource1Json.Replace("\"2021-02-01\"", "{}")} - "; - await TestDecompileForPaste( - json: json, - PasteType.JsonValue, - expectedErrorMessage: null, - expectedBicep: @" - { - type: 'Microsoft.Storage/storageAccounts' - apiVersion: {} - name: 'name1' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }"); - } - - [TestMethod] - public async Task NonResourceObject_WrongPropertyType_Number_PastesAsSimpleObject() - { - string json = @$" - {Resource1Json.Replace("\"2021-02-01\"", "1234")} - "; - await TestDecompileForPaste( - json: json, - PasteType.JsonValue, - expectedErrorMessage: null, - expectedBicep: @" - { - type: 'Microsoft.Storage/storageAccounts' - apiVersion: 1234 - name: 'name1' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }"); - } - - [TestMethod] - public async Task MissingParametersAndVars() - { - string json = @" - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name"", - ""location"": ""[parameters('location')]"", - ""kind"": ""[variables('storageKind')]"", - ""sku"": { - ""name"": ""[variables('sku')]"" - } - } - "; - string expected = @"resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: location - kind: storageKind - sku: { - name: sku - } -}"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.SingleResource, - expectedBicep: expected); - } - - [TestMethod] - public async Task MissingParametersAndVars_Conflict() - { - string json = """ - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2021-02-01", - "name": "name", - "location": "[concat(parameters('location'), variables('location'), parameters('location_var'), variables('location_var'), parameters('location_param'), variables('location_param'))]", - "kind": "[variables('location')]", - "sku": { - "name": "Premium_LRS" - } - } - """; - - await TestDecompileForPaste( - json: json, - expectedErrorMessage: null, - expectedPasteType: PasteType.SingleResource, - expectedBicep: """ - resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: concat(location, location_var, location_var, location_var_var, location_param, location_param_var) - kind: location_var - sku: { - name: 'Premium_LRS' - } - } - """); - } - - [TestMethod] - public async Task SingleResourceObject_ShouldSucceed() - { - string json = @" - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name"", - ""location"": ""eastus"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }"; - string expected = @" - resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }"; - - await TestDecompileForPaste( - json: json, - PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: expected); - } - - [TestMethod] - public async Task MultipleResourceObjects_ShouldSucceed() - { - string json = @" - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""eastus"", // comment and blank line - - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }, - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name2"", - ""location"": ""eastus"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }, - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name3"", - ""location"": ""eastus"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }"; - string expected = @" - resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - } - - resource name2 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name2' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - } - - resource name3 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name3' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }"; + PasteType.FullParams => "fullParams", - await TestDecompileForPaste( - json: json, - PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: expected); - } - - [TestMethod] - public async Task MultipleResourceObjects_SkipTrivia_ShouldSucceed() - { - string json = $@" - - // This is a comment - // So is this - /* And this - also */ - - {Resource1Json} - // This is a comment - // So is this - /* And this - also */ -, - // This is a comment - // So is this - /* And this - also */ - - {Resource2Json} // This is a comment - // So is this - /* And this - also */"; - ; - string expected = $@" - {Resource1Bicep} - - {Resource2Bicep}"; - - await TestDecompileForPaste( - json: json, - PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: expected); - } - - [TestMethod] - public async Task MultipleResourceObjects_NoComma_ShouldSucceed() - { - string json = @" - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""eastus"", // comment and blank line - - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - } - { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name2"", - ""location"": ""eastus"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }"; - string expected = @" - resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - } - - resource name2 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name2' - location: 'eastus' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }"; - - await TestDecompileForPaste( - json: json, - PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: expected); - } - - [TestMethod] - public async Task Modules() - { - const string json = @" - { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""resources"": [ - { - ""name"": ""nestedDeploymentInner"", - ""type"": ""Microsoft.Resources/deployments"", - ""apiVersion"": ""2021-04-01"", - ""properties"": { - ""expressionEvaluationOptions"": { - ""scope"": ""inner"" - }, - ""mode"": ""Incremental"", - ""parameters"": {}, - ""template"": { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""parameters"": {}, - ""variables"": {}, - ""resources"": [ - { - ""name"": ""storageaccount1"", - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-04-01"", - ""tags"": { - ""displayName"": ""storageaccount1"" - }, - ""location"": ""[resourceGroup().location]"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"", - ""tier"": ""Premium"" - } - } - ], - ""outputs"": {} - } - } - }, - { - ""name"": ""nestedDeploymentOuter"", - ""type"": ""Microsoft.Resources/deployments"", - ""apiVersion"": ""2021-04-01"", - ""properties"": { - ""mode"": ""Incremental"", - ""template"": { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""variables"": {}, - ""resources"": [ - { - ""name"": ""storageaccount2"", - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-04-01"", - ""tags"": { - ""displayName"": ""storageaccount2"" - }, - ""location"": ""[resourceGroup().location]"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"", - ""tier"": ""Premium"" - } - } - ], - ""outputs"": {} - } - } - }, - { - ""name"": ""storageaccount"", - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-04-01"", - ""tags"": { - ""displayName"": ""storageaccount"" - }, - ""location"": ""[resourceGroup().location]"", - ""kind"": ""StorageV2"", - ""sku"": { - ""name"": ""Premium_LRS"", - ""tier"": ""Premium"" - } - }, - { - ""name"": ""nestedDeploymentInner2"", - ""type"": ""Microsoft.Resources/deployments"", - ""apiVersion"": ""2021-04-01"", - ""properties"": { - ""expressionEvaluationOptions"": { - ""scope"": ""inner"" - }, - ""mode"": ""Incremental"", - ""parameters"": {}, - ""template"": { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""parameters"": {}, - ""variables"": {}, - ""resources"": [], - ""outputs"": {} - } - } - } - ] - }"; - - string expected = @" - module nestedDeploymentInner './nested_nestedDeploymentInner.bicep' = { - name: 'nestedDeploymentInner' - params: {} - } - - module nestedDeploymentOuter './nested_nestedDeploymentOuter.bicep' = { - name: 'nestedDeploymentOuter' - params: {} - } - - resource storageaccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { - name: 'storageaccount' - tags: { - displayName: 'storageaccount' - } - location: resourceGroup().location - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - tier: 'Premium' - } - } - - module nestedDeploymentInner2 './nested_nestedDeploymentInner2.bicep' = { - name: 'nestedDeploymentInner2' - params: {} - }"; - - await TestDecompileForPaste( - json: json, - PasteType.FullTemplate, - expectedErrorMessage: null, - expectedBicep: expected); - } - - [TestMethod] - public async Task SingleResource_WithNestedInnerScopedTemplateWithMissingParam_ShouldSucceed() - { - const string json = @" - { - ""name"": ""nestedDeploymentInner"", - ""type"": ""Microsoft.Resources/deployments"", - ""apiVersion"": ""2021-04-01"", - ""properties"": { - ""mode"": ""Incremental"", - ""expressionEvaluationOptions"": { - ""scope"": ""inner"" - }, - ""template"": { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""resources"": [ - { - ""name"": ""storageaccount1"", - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-04-01"", - // Refers to a local parameter in the module, which is missing. - // This should cause error during conversion (because the nested template should be valid), - // although a missing parameter at the top level would not (because the top level is - // not expected to be complete). - ""location"": ""[parameters('location')]"" - } - ] - } - } - }"; - - await TestDecompileForPaste( - json: json, - PasteType.SingleResource, - expectedErrorMessage: "[18:48]: Unable to find parameter location", - expectedBicep: null); - } - - [TestMethod] - public async Task SingleResource_WithNestedOuterScopedTemplate_WithMissingParam_ShouldBePastable_ButShouldGiveError() - { - const string json = @" - { - ""name"": ""nestedDeploymentInner"", - ""type"": ""Microsoft.Resources/deployments"", - ""apiVersion"": ""2021-04-01"", - ""properties"": { - ""mode"": ""Incremental"", - ""template"": { - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": ""1.0.0.0"", - ""resources"": [ - { - ""name"": ""storageaccount1"", - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-04-01"", - // Refers to a local parameter in the module, which is missing. - // This should cause error during conversion (because the nested template should be valid), - // although a missing parameter at the top level would not (because the top level is - // not expected to be complete). - ""location"": ""[parameters('location')]"" - } - ] - } - } - }"; - - await TestDecompileForPaste( - json: json, - PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: @" - module nestedDeploymentInner './nested_nestedDeploymentInner.bicep' = { - name: 'nestedDeploymentInner' - params: { - location: location - } - }"); - } - - [TestMethod] - public async Task MultipleResourceObjects_ExtraBraceAfterwards_ShouldSucceed() - { - string json = @$" - {Resource1Json} - {Resource2Json} - }}}} // extra"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: @$" - {Resource1Bicep} - - {Resource2Bicep}"); - } - - [TestMethod] - public async Task MultipleResourceObjects_ExtraOpenBraceAfterwards_ShouldSucceed() - { - string json = @$" - {Resource1Json} - {Resource2Json} - {{ // extra"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: @$" - {Resource1Bicep} - - {Resource2Bicep}"); - } - - [TestMethod] - public async Task MultipleResourceObjects_ExtraEmptyObjectAfterwards_ShouldSucceed() - { - string json = @$" - {Resource1Json} - {Resource2Json} - {{}} // extra"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: @$" - {Resource1Bicep} - - {Resource2Bicep}"); - } - - [TestMethod] - public async Task MultipleResourceObjects_NameConflict_ShouldAllowPaste_ButGiveError() - { - string json = @$" - {Resource1Json} - {Resource1Json} - {Resource1Json}"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: "[21:1]: Unable to pick unique name for resource Microsoft.Storage/storageAccounts name1", - expectedBicep: null); - } - - [TestMethod] - public async Task MultipleResourceObjects_RandomCharactersAfterwards_ShouldSucceed_AndIgnoreRemaining() - { - string json = @$" - {Resource1Json} - {Resource2Json} - something else {{ // extra"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: @$" - {Resource1Bicep} - - {Resource2Bicep}" - ); - } - - [TestMethod] - public async Task MultipleResourceObjects_NonResourceInMiddle_ShouldSucceed_AndIgnoreNonResources() - { - string json = @$" - {Resource1Json} - {{ - ""notAResource"": ""honest"" - }} - {Resource2Json} -"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: $@" - {Resource1Bicep} - - {Resource2Bicep}" - ); - } - - [TestMethod] - public async Task MultipleResourceObjects_ExtraCommaAtEnd_ShouldSucceed() - { - string json = @$" - {Resource1Json} - {Resource2Json} - ,,, // extra"; - string expected = @$" - {Resource1Bicep} - - {Resource2Bicep}"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.ResourceList, - expectedErrorMessage: null, - expectedBicep: expected); - } - - [TestMethod] - public async Task MissingVariable_UsedMultipleTimes_ShouldSucceed() - { - string json = @" { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""[variables('v1')]"", - ""kind"": ""[variables('v1')]"", - ""sku"": { - ""name"": ""[variables('v1')]"" - } - }"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: @" - resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: v1 - kind: v1 - sku: { - name: v1 - } - }"); - } - - [TestMethod] - public async Task MissingVariable_UsedMultipleTimes_CasedDifferently_ShouldSucceed() - { - string json = @" { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""[variables('v1')]"", - ""kind"": ""[variables('v1')]"", - ""sku"": { - ""name"": ""[variables('V1')]"" - } - }"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: @" - resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: v1 - kind: v1 - sku: { - name: v1 - } - }"); - } - - [TestMethod] - public async Task MissingParameter_UsedMultipleTimes_ShouldSucceed() - { - string json = @"{ - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""[parameters('p1')]"", - ""kind"": ""[parameters('p1')]"", - ""sku"": { - ""name"": ""[parameters('p1')]"" - } - }"; - - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: @" - resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: p1 - kind: p1 - sku: { - name: p1 - } - }"); + _ => throw new NotImplementedException(), + }); } - [TestMethod] - public async Task MissingParameter_UsedMultipleTimes_CasedDifferently_ShouldSucceed() - { - string json = @" { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""name1"", - ""location"": ""[parameters('p1')]"", - ""kind"": ""[parameters('p1')]"", - ""sku"": { - ""name"": ""[parameters('P1')]"" - } - }"; + #region JSON/Bicep Constants - await TestDecompileForPaste( - json: json, - expectedPasteType: PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: @" - resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name1' - location: p1 - kind: p1 - sku: { - name: p1 - } - }"); - } + //private const string jsonFullArmTemplateMembers = """ + // "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + // "contentVersion": "1.0.0.0", + // "parameters": { + // "location": { + // "type": "string", + // "defaultValue": "[resourceGroup().location]" + // } + // }, + // "resources": [ + // { + // "type": "Microsoft.Storage/storageAccounts", + // "apiVersion": "2021-02-01", + // "name": "name", + // "location": "[parameters('location')]", + // "kind": "StorageV2", + // "sku": { + // "name": "Premium_LRS" + // } + // } + // ] + // """; + //private const string jsonFullArmTemplate = $$""" + // { + // {{jsonFullArmTemplateMembers}} + // } + // """; + private const string jsonFullParamsTemplateMembers = """ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "pString": { + "value": "" + }, + "pInt": { + "value": 0 + }, + "pBool": { + "value": false + }, + "pObject": { + "value": {} + }, + "pArray": { + "value": [] + } + } + """; + private const string jsonFullParamsTemplate = $$""" + { + {{jsonFullParamsTemplateMembers}} + } + """; - [TestMethod] - public async Task MissingParameterVariable_CollidesWithResourceName_ShouldSucceed() - { - string json = @" { - ""type"": ""Microsoft.Storage/storageAccounts"", - ""apiVersion"": ""2021-02-01"", - ""name"": ""v1"", - ""location"": ""[variables('v1')]"", - ""kind"": ""[parameters('v1')]"", - ""sku"": { - ""name"": ""Premium_LRS"" - } - }"; + #endregion + + [DataTestMethod] + [DataRow( + jsonFullParamsTemplate, + PasteType.FullParams, + """ + using '' /*TODO: Provide a path to a bicep template*/ + + param pString = '' + + param pInt = 0 + + param pBool = false + + param pObject = {} + + param pArray = [] + """, + DisplayName = "Full Params" + )] + [DataRow( + $$""" + { + {{jsonFullParamsTemplateMembers}} + , extraProperty: "hello" + } + """, + PasteType.FullParams, + """ + using '' /*TODO: Provide a path to a bicep template*/ + + param pString = '' + + param pInt = 0 + + param pBool = false + + param pObject = {} + + param pArray = [] + """, + DisplayName = "Extra property" + )] + [DataRow( + $$""" + { + {{jsonFullParamsTemplateMembers}} + } + } // extra + """, + PasteType.FullParams, + """ + using '' /*TODO: Provide a path to a bicep template*/ + + param pString = '' + + param pInt = 0 + + param pBool = false + + param pObject = {} + + param pArray = [] + """, + DisplayName = "Extra brace at end (succeeds)" + )] + [DataRow( + $$""" + { + {{jsonFullParamsTemplateMembers}} + } + random characters + """, + PasteType.FullParams, + """ + + using '' /*TODO: Provide a path to a bicep template*/ + + param pString = '' + + param pInt = 0 + + param pBool = false + + param pObject = {} + + param pArray = [] + """, + DisplayName = "Extra random characters at end" + )] + [DataRow( + $$""" + { + { // extra + {{jsonFullParamsTemplateMembers}} + } + random characters + """, + PasteType.None, + null, + DisplayName = "Extra brace at beginning (can't paste)" + )] + [DataRow( + $$""" + random characters + { + {{jsonFullParamsTemplateMembers}} + } + """, + PasteType.None, + null, + DisplayName = "Extra random characters at beginning (can't paste)" + )] + [DataRow( + """ + { + "$schema": {}, + "parameters": { + "test": { + "value": "test" + } + } + } + """, + PasteType.JsonValue, + // Treats it simply as a JSON object + """ + { + '$schema': {} + parameters: { + test: { + value: 'test' + } + } + } + """, + DisplayName = "Schema not a string" + )] + public async Task FullJsonParams(string json, PasteType expectedPasteType, string expectedBicep, string? errorMessage = null) + { await TestDecompileForPaste( json: json, - expectedPasteType: PasteType.SingleResource, - expectedErrorMessage: null, - expectedBicep: @" - resource v1 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'v1' - location: v1_var - kind: v1_param - sku: { - name: 'Premium_LRS' - } - }"); + expectedPasteType: expectedPasteType, + expectedBicep: expectedBicep, + errorMessage); } [TestMethod] - public async Task MultilineStrings_ShouldSucceed() + public async Task JustString_WithNoQuotes_CantConvert() { - string json = @"{ - ""type"": ""Microsoft.Compute/virtualMachines"", - ""apiVersion"": ""2018-10-01"", - ""name"": ""[variables('vmName')]"", // to customize name, change it in variables - ""location"": ""[ - parameters('location') - ]"", -}"; - + string json = @"just a string"; await TestDecompileForPaste( json: json, - expectedPasteType: PasteType.SingleResource, + PasteType.None, expectedErrorMessage: null, - expectedBicep: @" - resource vm 'Microsoft.Compute/virtualMachines@2018-10-01' = { - name: vmName - location: location - } -"); + expectedBicep: null); } + [DataTestMethod] [DataRow( - @"""just a string with double quotes""", + """ + "just a string with double quotes" + """, @"'just a string with double quotes'", DisplayName = "String with double quotes" )] [DataRow( - @"{""hello"": ""there""}", - @"{ - hello: 'there' - }", + """{"hello": "there"}""", + """ + { + hello: 'there' + } + """, DisplayName = "simple object" )] [DataRow( - @"{""hello there"": ""again""}", - @"{ - 'hello there': 'again' - }", + """{"hello there": "again"}""", + """ + { + 'hello there': 'again' + } + """, DisplayName = "object with properties needing quotes" )] [DataRow( - @"""[resourceGroup().location]""", + """ + "[resourceGroup().location]" + """, @"resourceGroup().location", DisplayName = "String with ARM expression" )] [DataRow( - @"[""[resourceGroup().location]""]", + """["[resourceGroup().location]"]""", """ [ resourceGroup().location @@ -1303,70 +374,94 @@ await TestDecompileForPaste( DisplayName = "Array with string expression" )] [DataRow( - @"""[concat(variables('leftBracket'), 'dbo', variables('rightBracket'), '.', variables('leftBracket'), 'table', variables('rightBracket')) ]""", + """ + "[concat(variables('leftBracket'), 'dbo', variables('rightBracket'), '.', variables('leftBracket'), 'table', variables('rightBracket')) ]" + """, @"'${leftBracket}dbo${rightBracket}.${leftBracket}table${rightBracket}'", DisplayName = "concat changes to interpolated string" )] [DataRow( - @"""[concat('Correctly escaped single quotes ''here'' and ''''here'''' ', variables('and'), ' ''wherever''')]""", + """ + "[concat('Correctly escaped single quotes ''here'' and ''''here'''' ', variables('and'), ' ''wherever''')]" + """, @"'Correctly escaped single quotes \'here\' and \'\'here\'\' ${and} \'wherever\''", DisplayName = "Correctly escaped single quotes" )] [DataRow( - @"""[concat('string', ' ', 'string')]""", + """ + "[concat('string', ' ', 'string')]" + """, @"'string string'", DisplayName = "Concat is simplified" )] [DataRow( - @"""[concat('''Something in single quotes - '' ', 'and something not ', variables('v1'))]""", + """ + "[concat('''Something in single quotes - '' ', 'and something not ', variables('v1'))]" + """, @"'\'Something in single quotes - \' and something not ${v1}'", DisplayName = "Escaped and unescaped single quotes in string" )] [DataRow( - @"""[[this will be in brackets, not an expression - variables('blobName') should not be converted to Bicep, but single quotes should be escaped]""", + """ + "[[this will be in brackets, not an expression - variables('blobName') should not be converted to Bicep, but single quotes should be escaped]" + """, @"'[this will be in brackets, not an expression - variables(\'blobName\') should not be converted to Bicep, but single quotes should be escaped]'", DisplayName = "string starting with [[ is not an expression, [[ should get converted to single [" )] [DataRow( - @"""[json(concat('{\""storageAccountType\"": \""Premium_LRS\""}'))]""", - @"json('{""storageAccountType"": ""Premium_LRS""}')", + """ + "[json(concat('{\"storageAccountType\": \"Premium_LRS\"}'))]" + """, + """json('{"storageAccountType": "Premium_LRS"}')""", DisplayName = "double quotes inside strings inside object" )] [DataRow( - @"""[concat(variables('blobName'),parameters('blobName'))]""", + """ + "[concat(variables('blobName'),parameters('blobName'))]" + """, "concat(blobName, blobName_param)", DisplayName = "param and variable with same name" )] [DataRow( - @"""Double quotes \""here\""""", - @"'Double quotes ""here""'", + """ + "Double quotes \"here\"" + """, + """'Double quotes "here"'""", DisplayName = "Double quotes in string" )] [DataRow( - @"'Double quotes \""here\""'", - @"'Double quotes ""here""'", + """'Double quotes \"here\"'""", + """'Double quotes "here"'""", DisplayName = "Double quotes in single-quote string" )] [DataRow( - @"""['Double quotes \""here\""']""", - @"'Double quotes ""here""'", + """ + "['Double quotes \"here\"']" + """, + """'Double quotes "here"'""", DisplayName = "Double quotes in string inside string expression" )] [DataRow( - @""" [A string that has whitespace before the bracket is not an expression]""", + """ + " [A string that has whitespace before the bracket is not an expression]" + """, @"' [A string that has whitespace before the bracket is not an expression]'", DisplayName = "Whitespace before the bracket" )] [DataRow( - @"""[A string that has whitespace after the bracket is not an expression] """, + """ + "[A string that has whitespace after the bracket is not an expression] " + """, @"'[A string that has whitespace after the bracket is not an expression] '", DisplayName = "Whitespace after the bracket" )] [DataRow( - @"[ - 1, 2, - 3 -]", + """ + [ + 1, 2, + 3 + ] + """, """ [ 1 @@ -1402,7 +497,8 @@ await TestDecompileForPaste( } [DataRow( - @"{ + """ + { ipConfigurations: [ { name: 'ipconfig1' @@ -1420,7 +516,8 @@ await TestDecompileForPaste( networkSecurityGroup: { id: resourceId('Microsoft.Network/networkSecurityGroups', 'networkSecurityGroupName') } - }", + } + """, DisplayName = "Bicep object" )] [DataRow( @@ -1428,7 +525,9 @@ await TestDecompileForPaste( DisplayName = "Date" )] [DataRow( - @"""kubernetesVersion"": ""1.15.7""", + """ + "kubernetesVersion": "1.15.7" + """, DisplayName = "\"property\": \"value\"" )] [DataRow( @@ -1436,8 +535,10 @@ await TestDecompileForPaste( DisplayName = "single-line comment" )] [DataRow( - @"/* hello - there */", + """ + /* hello + there */ + """, DisplayName = "multi-line comment" )] [DataRow( @@ -1477,23 +578,29 @@ await TestDecompileForPaste( [DataRow( @"{ abc: 1, def: 'def' }", // this is not technically valid JSON but the Newtonsoft parser accepts it anyway and it is already valid Bicep PasteType.BicepValue, // Valid json and valid Bicep expression - @"{ - abc: 1 - def: 'def' - }")] + """ + { + abc: 1 + def: 'def' + } + """)] [DataRow( @"{ abc: 1, /*hi*/ def: 'def' }", // this is not technically valid JSON but the Newtonsoft parser accepts it anyway and it is already valid Bicep PasteType.BicepValue, // Valid json and valid Bicep expression - @"{ - abc: 1 - def: 'def' - }")] + """ + { + abc: 1 + def: 'def' + } + """)] [DataRow( "[1]", PasteType.BicepValue, // Valid json and valid Bicep expression - @"[ - 1 - ]")] + """ + [ + 1 + ] + """)] [DataRow( "[1, 1]", PasteType.BicepValue, // Valid json and valid Bicep expression @@ -1508,13 +615,17 @@ await TestDecompileForPaste( PasteType.BicepValue, // Valid json and valid Bicep expression "[]")] [DataRow( - @"[ -/* */ ]", + """ + [ + /* */ ] + """, PasteType.BicepValue, // Valid json and valid Bicep expression "[]")] [DataRow( - @"[ - 1]", + """ + [ + 1] + """, PasteType.BicepValue, // Valid json and valid Bicep expression """ [ @@ -1533,54 +644,64 @@ await TestDecompileForPaste( DisplayName = "String with single quotes" )] [DataRow( - @"// comment that shouldn't get removed because code is already valid Bicep - - /* another comment - - */ - '123' // yet another comment - - /* and another - */ - - ", + """ + // comment that shouldn't get removed because code is already valid Bicep + + /* another comment + + */ + '123' // yet another comment + + /* and another + */ + + + """, PasteType.BicepValue, // Valid json and valid Bicep expression (will get pasted as original for copy/paste, as '123' for "paste as Bicep" command) "'123'", DisplayName = "Regress #10940 Paste removes comments when copying/pasting Bicep" )] [DataRow( - @"param p1 string - param p2 string", + """ + param p1 string + param p2 string + """, PasteType.None, // Valid Bicep, but not as a single expression null, DisplayName = "multiple valid Bicep statements - shouldn't be changed" )] [DataRow( - @"param p1 string // comment 1 - // comment 2 - param p2 string /* comment 3 */", + """ + param p1 string // comment 1 + // comment 2 + param p2 string /* comment 3 */ + """, PasteType.None, // Valid Bicep, but not as a single expression null, DisplayName = "multiple valid Bicep statements with comments - shouldn't be changed" )] [DataRow( - @"[ - 1 - 2 - ]", + """ + [ + 1 + 2 + ] + """, PasteType.None, // Valid Bicep but not valid JSON, therefore not converted null, DisplayName = "multi-line valid Bicep expression - shouldn't be changed" )] [DataRow( - @"[ - 1 // comment 1 - // comment 2 - 2 /* comment 3 - 3 - /* - 4 - ]", + """ + [ + 1 // comment 1 + // comment 2 + 2 /* comment 3 + 3 + /* + 4 + ] + """, PasteType.None, // Valid Bicep but not valid JSON, therefore not converted null, DisplayName = "multi-line valid Bicep expression with comments - shouldn't be changed" @@ -1598,31 +719,51 @@ await TestDecompileForPaste( public async Task Template_JsonConvertsToEmptyBicep() { await TestDecompileForPaste( - @"{ - ""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"", - ""contentVersion"": """", - ""apiProfile"": """", - ""parameters"": { }, - ""variables"": { }, - ""functions"": [ ], - ""resources"": [ ], - ""outputs"": { } -}", - PasteType.None, + """ + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "parameters": { }, + } + """, + PasteType.FullParams, expectedErrorMessage: null, - expectedBicep: null); + expectedBicep: "using '' /*TODO: Provide a path to a bicep template*/"); + } + + [TestMethod] + public async Task Template_JsonConvertsToEmptyBicepIfUsingIsPresent() + { + await TestDecompileForPaste( + """ + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "parameters": { }, + } + """, + PasteType.None, + expectedErrorMessage: null, + expectedBicep: null, + editorContentsWithCursor: """ + using 'exiting.bicep' + | + """ + ); } [DataTestMethod] [DataRow( - @"|@description('bicep string') - param s string", + """ + |using '' + param s = '' + """, PasteContext.None, DisplayName = "simple: cursor at start of bicep file" )] [DataRow( - @"@description('bicep string') - param s string|", + """ + using '' + param s = ''| + """, PasteContext.None, DisplayName = "simple: cursor at end of bicep file" )] @@ -1662,178 +803,90 @@ await TestDecompileForPaste( DisplayName = "comments: string inside a /**/ comment" )] [DataRow( - @"var a = /* - 'not a| string' */ 123 - ", + """ + var a = /* + 'not a| string' */ 123 + + """, PasteContext.None, DisplayName = "comments: string inside a multiline comment" )] // @description does not use a StringSyntax, we have to look for string tokens... [DataRow( - @"@description(|'bicep string') - param s string", + """ + @description(|'bicep string') + var s = 'str' + """, PasteContext.None, DisplayName = "@description: before beginning quote" )] [DataRow( - @"@description('|bicep string') - param s string", + """ + @description('|bicep string') + var s = 'str' + """, PasteContext.String, DisplayName = "@description: after beginning quote" )] [DataRow( - @"@description('bicep string|') - param s string", + """ + @description('bicep string|') + var s = 'str' + """, PasteContext.String, DisplayName = "@description: before end quote" )] [DataRow( - @"@description('bicep string'|) - param s string", + """ + @description('bicep string'|) + var s = 'str' + """, PasteContext.None, DisplayName = "@description: after end quote" )] [DataRow( - @"output s string = '|'", - PasteContext.String, - DisplayName = "output s = '|'" - )] - [DataRow( - @"output s string = 'Here\|'s to you!'", - PasteContext.String, - DisplayName = "escapes: inside escaped single quotes in string" - )] - [DataRow( - @"output s string = 'This is |${aValue} interpolated value'", - PasteContext.String, - DisplayName = "interpolation: right before $" - )] - [DataRow( - @"output s string = 'This is $|{aValue} interpolated value'", - PasteContext.String, - DisplayName = "interpolation: right before value" - )] - [DataRow( - @"output s string = 'This is ${|aValue} interpolated'", - PasteContext.None, - DisplayName = "interpolation: just inside of expression" - )] - [DataRow( - @"output s string = 'This is ${aValue|} interpolated'", - PasteContext.None, - DisplayName = "interpolation: right before ending }" - )] - [DataRow( - @"output s string ='This is ${aValue}| interpolated'", - PasteContext.String, - DisplayName = "interpolation: right after ending }" - )] - [DataRow( - @"output s string ='This is ${concat(a, |'string', value)} interpolated'", - PasteContext.None, - DisplayName = "string inside interpolation: right before beginning quote" - )] - [DataRow( - @"output s string ='This is ${concat(a, '|string', value)} interpolated'", - PasteContext.String, - DisplayName = "string inside interpolation: inside string" - )] - [DataRow( - @"output s string ='This is ${concat(a, 'string'|, value)} interpolated'", - PasteContext.None, - DisplayName = "string inside interpolation: right after string" - )] - [DataRow( - @"output s string ='This is ${concat(a, 'before|${'a'}after', value)} interpolated'", - PasteContext.String, - DisplayName = "nested interpolation: right before $" - )] - [DataRow( - @"output s string ='This is ${concat(a, 'before${|'a'}after', value)} interpolated'", - PasteContext.None, - DisplayName = "nested interpolation: right before string inside interpolation" - )] - [DataRow( - "output s string ='This is ${concat(a, 'before${'|a'}after', value)} interpolated'", - PasteContext.String, - DisplayName = "nested interpolation: right inside string inside interpolation" - )] - [DataRow( - @"output s string ='This is ${concat(a, 'before${'a'|}after', value)} interpolated'", - PasteContext.None, - DisplayName = "nested interpolation: right after string inside interpolation, still inside string hole" - )] - [DataRow( - @"var s = |'''hello ${not a hole} - there '''", + """ + var s = |'''hello ${not a hole} + there ''' + """, PasteContext.None, DisplayName = "multi-line string: just before starting quotes" )] [DataRow( - @"var s = '''|hello ${not a hole} - there '''", + """ + var s = '''|hello ${not a hole} + there ''' + """, PasteContext.String, DisplayName = "multi-line string: just inside string" )] [DataRow( - @"var s = '''hello ${not a |hole} - there '''", + """ + var s = '''hello ${not a |hole} + there ''' + """, PasteContext.String, DisplayName = "multi-line string: not a hole" )] [DataRow( - @"var s = '''hello ${not a hole} - there |'''", + """ + var s = '''hello ${not a hole} + there |''' + """, PasteContext.String, DisplayName = "multi-line string: just before ending quotes" )] [DataRow( - @"var s = '''hello ${not a hole} - there '''|", + """ + var s = '''hello ${not a hole} + there '''| + """, PasteContext.None, DisplayName = "multi-line string: just outside ending quotes" )] - [DataRow( - @"resource stg '|Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: 'location' - kind: 'StorageV2' - sku: { - name: 'Premium_LRS' - } - }", - PasteContext.String, - DisplayName = "resources: inside resource type" - )] - [DataRow( - @"resource stg 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: 'name' - location: 'location' - kind: '|StorageV2' - sku: { - name: 'Premium_LRS' - } - }", - PasteContext.String, - DisplayName = "resources: inside resource property value" - )] - [DataRow( - @"resource loadBalancerPublicIPAddress 'Microsoft.Network/publicIPAddresses@2020-11-01' = { - name: 'loadBalancerName' - location: '|location' - sku: { - name: 'Standard' - } - properties: { - publicIPAllocationMethod: 'static' - } - }", - PasteContext.String, - DisplayName = "resources: inside resource property value" - )] public async Task DontPasteIntoStrings(string editorContentsWithCursor, PasteContext expectedPasteContext) { - await TestDecompileForPaste(new Options( + await TestDecompileForPaste(new( "\"json string\"", expectedPasteContext == PasteContext.String ? PasteType.None : PasteType.JsonValue, expectedPasteContext, @@ -1842,14 +895,16 @@ await TestDecompileForPaste(new Options( editorContentsWithCursor: editorContentsWithCursor )); - await TestDecompileForPaste(new Options( - @"{ - ""type"": ""Microsoft.Resources/resourceGroups"", - ""apiVersion"": ""2022-09-01"", - ""name"": ""rg"", - ""location"": ""[parameters('location')]"" - }", - expectedPasteContext == PasteContext.String ? PasteType.None : PasteType.SingleResource, + await TestDecompileForPaste(new( + """ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2022-09-01", + "name": "rg", + "location": "[parameters('location')]" + } + """, + expectedPasteType: expectedPasteContext == PasteContext.String ? PasteType.None : PasteType.JsonValue, expectedPasteContext, ignoreGeneratedBicep: true, expectedErrorMessage: null, diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs similarity index 96% rename from src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs rename to src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs index 659c9ab2af3..33985a61c9f 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs @@ -17,7 +17,7 @@ namespace Bicep.LangServer.UnitTests.Handlers { [TestClass] - public class BicepDecompileForPasteBicepCommandHandlerTests + public class BicepDecompileForPasteCommandHandlerTests { [NotNull] public TestContext? TestContext { get; set; } @@ -33,7 +33,7 @@ private BicepDecompileForPasteCommandHandler CreateHandler(LanguageServerMock se return builder.Construct(); } - + public enum PasteType { None, @@ -43,6 +43,11 @@ public enum PasteType JsonValue, BicepValue, } + public enum PasteContext + { + None, + String + } record Options( string pastedJson, @@ -60,7 +65,7 @@ private async Task TestDecompileForPaste( string? expectedErrorMessage = null, string? editorContentsWithCursor = null) { - await TestDecompileForPaste(new( + await TestDecompileForPaste(new Options( json, expectedPasteType, PasteContext.None, @@ -82,14 +87,14 @@ private async Task TestDecompileForPaste(Options options) var handler = CreateHandler(server); - var result = await handler.Handle(new(editorContentsWithPastedJson, cursorOffset, options.pastedJson.Length, options.pastedJson, queryCanPaste: false, "bicep"), CancellationToken.None); + var result = await handler.Handle(new BicepDecompileForPasteCommandParams(editorContentsWithPastedJson, cursorOffset, options.pastedJson.Length, options.pastedJson, queryCanPaste: false, "bicep"), CancellationToken.None); result.ErrorMessage.Should().Be(options.expectedErrorMessage); if (!options.ignoreGeneratedBicep) { var expectedBicep = options.expectedBicep?.Trim('\n'); - var actualBicep = result.Bicep?.Trim('\n'); + string? actualBicep = result.Bicep?.Trim('\n'); actualBicep.Should().EqualTrimmedLines(expectedBicep); } @@ -102,9 +107,9 @@ private async Task TestDecompileForPaste(Options options) result.PasteType.Should().Be(options.expectedPasteType switch { - PasteType.None => "none", + PasteType.None => null, PasteType.FullTemplate => "fullTemplate", - PasteType.SingleResource => "singleResource", + PasteType.SingleResource => "resource", PasteType.ResourceList => "resourceList", PasteType.JsonValue => "jsonValue", PasteType.BicepValue => "bicepValue", @@ -437,7 +442,7 @@ public async Task Errors(string json, PasteType pasteType, string? expectedBicep [TestMethod] public async Task JustString_WithNoQuotes_CantConvert() { - var json = @"just a string"; + string json = @"just a string"; await TestDecompileForPaste( json: json, PasteType.None, @@ -448,7 +453,7 @@ await TestDecompileForPaste( [TestMethod] public async Task NonResourceObject_WrongPropertyType_Object_PastesAsSimpleObject() { - var json = @$" + string json = @$" {Resource1Json.Replace("\"2021-02-01\"", "{}")} "; await TestDecompileForPaste( @@ -471,7 +476,7 @@ await TestDecompileForPaste( [TestMethod] public async Task NonResourceObject_WrongPropertyType_Number_PastesAsSimpleObject() { - var json = @$" + string json = @$" {Resource1Json.Replace("\"2021-02-01\"", "1234")} "; await TestDecompileForPaste( @@ -494,7 +499,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingParametersAndVars() { - var json = @" + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", @@ -506,7 +511,7 @@ public async Task MissingParametersAndVars() } } "; - var expected = @"resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { + string expected = @"resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { name: 'name' location: location kind: storageKind @@ -524,18 +529,18 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingParametersAndVars_Conflict() { - var json = """ - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2021-02-01", - "name": "name", - "location": "[concat(parameters('location'), variables('location'), parameters('location_var'), variables('location_var'), parameters('location_param'), variables('location_param'))]", - "kind": "[variables('location')]", - "sku": { - "name": "Premium_LRS" - } - } - """; + string json = """ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-02-01", + "name": "name", + "location": "[concat(parameters('location'), variables('location'), parameters('location_var'), variables('location_var'), parameters('location_param'), variables('location_param'))]", + "kind": "[variables('location')]", + "sku": { + "name": "Premium_LRS" + } + } + """; await TestDecompileForPaste( json: json, @@ -556,7 +561,7 @@ await TestDecompileForPaste( [TestMethod] public async Task SingleResourceObject_ShouldSucceed() { - var json = @" + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", @@ -567,7 +572,7 @@ public async Task SingleResourceObject_ShouldSucceed() ""name"": ""Premium_LRS"" } }"; - var expected = @" + string expected = @" resource name 'Microsoft.Storage/storageAccounts@2021-02-01' = { name: 'name' location: 'eastus' @@ -587,7 +592,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_ShouldSucceed() { - var json = @" + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", @@ -619,7 +624,7 @@ public async Task MultipleResourceObjects_ShouldSucceed() ""name"": ""Premium_LRS"" } }"; - var expected = @" + string expected = @" resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { name: 'name1' location: 'eastus' @@ -657,7 +662,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_SkipTrivia_ShouldSucceed() { - var json = $@" + string json = $@" // This is a comment // So is this @@ -680,7 +685,7 @@ public async Task MultipleResourceObjects_SkipTrivia_ShouldSucceed() /* And this also */"; ; - var expected = $@" + string expected = $@" {Resource1Bicep} {Resource2Bicep}"; @@ -695,7 +700,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_NoComma_ShouldSucceed() { - var json = @" + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", @@ -717,7 +722,7 @@ public async Task MultipleResourceObjects_NoComma_ShouldSucceed() ""name"": ""Premium_LRS"" } }"; - var expected = @" + string expected = @" resource name1 'Microsoft.Storage/storageAccounts@2021-02-01' = { name: 'name1' location: 'eastus' @@ -853,7 +858,7 @@ public async Task Modules() ] }"; - var expected = @" + string expected = @" module nestedDeploymentInner './nested_nestedDeploymentInner.bicep' = { name: 'nestedDeploymentInner' params: {} @@ -973,7 +978,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_ExtraBraceAfterwards_ShouldSucceed() { - var json = @$" + string json = @$" {Resource1Json} {Resource2Json} }}}} // extra"; @@ -991,7 +996,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_ExtraOpenBraceAfterwards_ShouldSucceed() { - var json = @$" + string json = @$" {Resource1Json} {Resource2Json} {{ // extra"; @@ -1009,7 +1014,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_ExtraEmptyObjectAfterwards_ShouldSucceed() { - var json = @$" + string json = @$" {Resource1Json} {Resource2Json} {{}} // extra"; @@ -1027,7 +1032,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_NameConflict_ShouldAllowPaste_ButGiveError() { - var json = @$" + string json = @$" {Resource1Json} {Resource1Json} {Resource1Json}"; @@ -1042,7 +1047,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_RandomCharactersAfterwards_ShouldSucceed_AndIgnoreRemaining() { - var json = @$" + string json = @$" {Resource1Json} {Resource2Json} something else {{ // extra"; @@ -1061,7 +1066,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_NonResourceInMiddle_ShouldSucceed_AndIgnoreNonResources() { - var json = @$" + string json = @$" {Resource1Json} {{ ""notAResource"": ""honest"" @@ -1083,11 +1088,11 @@ await TestDecompileForPaste( [TestMethod] public async Task MultipleResourceObjects_ExtraCommaAtEnd_ShouldSucceed() { - var json = @$" + string json = @$" {Resource1Json} {Resource2Json} ,,, // extra"; - var expected = @$" + string expected = @$" {Resource1Bicep} {Resource2Bicep}"; @@ -1102,7 +1107,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingVariable_UsedMultipleTimes_ShouldSucceed() { - var json = @" { + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", ""name"": ""name1"", @@ -1131,7 +1136,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingVariable_UsedMultipleTimes_CasedDifferently_ShouldSucceed() { - var json = @" { + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", ""name"": ""name1"", @@ -1160,7 +1165,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingParameter_UsedMultipleTimes_ShouldSucceed() { - var json = @"{ + string json = @"{ ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", ""name"": ""name1"", @@ -1189,7 +1194,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingParameter_UsedMultipleTimes_CasedDifferently_ShouldSucceed() { - var json = @" { + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", ""name"": ""name1"", @@ -1218,7 +1223,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MissingParameterVariable_CollidesWithResourceName_ShouldSucceed() { - var json = @" { + string json = @" { ""type"": ""Microsoft.Storage/storageAccounts"", ""apiVersion"": ""2021-02-01"", ""name"": ""v1"", @@ -1247,7 +1252,7 @@ await TestDecompileForPaste( [TestMethod] public async Task MultilineStrings_ShouldSucceed() { - var json = @"{ + string json = @"{ ""type"": ""Microsoft.Compute/virtualMachines"", ""apiVersion"": ""2018-10-01"", ""name"": ""[variables('vmName')]"", // to customize name, change it in variables @@ -1833,7 +1838,7 @@ await TestDecompileForPaste( )] public async Task DontPasteIntoStrings(string editorContentsWithCursor, PasteContext expectedPasteContext) { - await TestDecompileForPaste(new( + await TestDecompileForPaste(new Options( "\"json string\"", expectedPasteContext == PasteContext.String ? PasteType.None : PasteType.JsonValue, expectedPasteContext, @@ -1842,7 +1847,7 @@ await TestDecompileForPaste(new( editorContentsWithCursor: editorContentsWithCursor )); - await TestDecompileForPaste(new( + await TestDecompileForPaste(new Options( @"{ ""type"": ""Microsoft.Resources/resourceGroups"", ""apiVersion"": ""2022-09-01"", diff --git a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs index bf305cfe19a..27d4f1ca7c0 100644 --- a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Immutable; -using System.Data; using System.Diagnostics; using System.Text; using Bicep.Core; @@ -13,7 +12,6 @@ using Bicep.Core.Syntax; using Bicep.Decompiler; using Bicep.LanguageServer.Telemetry; -using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.JsonRpc; @@ -42,37 +40,41 @@ public record BicepDecompileForPasteCommandResult string? Disclaimer ); - public enum PasteType - { - None, - FullTemplate, // Full template - SingleResource, // Single resource - ResourceList,// List of multiple resources - JsonValue, // Single JSON value (number, object, array etc) - BicepValue, // JSON value that is also valid Bicep (e.g. "[1, {}]") - FullParams - } - /// /// Handles a request from the client to analyze/decompile a JSON fragment for possible conversion into Bicep (for pasting into a Bicep file) /// - public class BicepDecompileForPasteCommandHandler : ExecuteTypedResponseCommandHandlerBase + public class BicepDecompileForPasteCommandHandler( + ISerializer serializer, + ILanguageServerFacade server, + ITelemetryProvider telemetryProvider, + BicepCompiler bicepCompiler) + : ExecuteTypedResponseCommandHandlerBase(LangServerConstants.DecompileForPasteCommand, serializer) { - private readonly TelemetryAndErrorHandlingHelper telemetryHelper; - private readonly BicepCompiler bicepCompiler; + private readonly TelemetryAndErrorHandlingHelper telemetryHelper = new(server.Window, telemetryProvider); private static readonly Uri JsonDummyUri = new("file:///from-clipboard.json", UriKind.Absolute); private static readonly Uri BicepDummyUri = PathHelper.ChangeToBicepExtension(JsonDummyUri); private static readonly Uri BicepParamsDummyUri = PathHelper.ChangeToBicepparamExtension(JsonDummyUri); - public enum PasteContext + private enum PasteType { None, - String, // Pasting inside of a string + FullTemplate, // Full template + SingleResource, // Single resource + ResourceList,// List of multiple resources + JsonValue, // Single JSON value (number, object, array etc) + BicepValue, // JSON value that is also valid Bicep (e.g. "[1, {}]") + FullParams // Full parameters file + } + private enum PasteContext + { + None, + String, // Pasting inside a string + ParamsWithUsingDeclaration, // Pasting inside a parameters file with an existing using declaration } - public enum LanguageId + private enum LanguageId { Bicep, BicepParams, @@ -91,18 +93,6 @@ private static LanguageId GetLanguageId(string languageId) private record ResultAndTelemetry(BicepDecompileForPasteCommandResult Result, BicepTelemetryEvent? SuccessTelemetry); - public BicepDecompileForPasteCommandHandler( - ISerializer serializer, - ILanguageServerFacade server, - ITelemetryProvider telemetryProvider, - BicepCompiler bicepCompiler - ) - : base(LangServerConstants.DecompileForPasteCommand, serializer) - { - this.telemetryHelper = new(server.Window, telemetryProvider); - this.bicepCompiler = bicepCompiler; - } - public override Task Handle(BicepDecompileForPasteCommandParams parameters, CancellationToken cancellationToken) { return telemetryHelper.ExecuteWithTelemetryAndErrorHandling((Func>)(async () => @@ -118,54 +108,67 @@ public override Task Handle(BicepDecompileF })); } - private static PasteContext GetPasteContext(string bicepContents, int offset, int length) + private static PasteContext GetPasteContext(string bicepContents, int offset, int length, LanguageId languageId) { - var contents = bicepContents; - var newContents = string.Concat(contents.AsSpan()[..offset], contents.AsSpan(offset + length)); - var parser = new Parser(newContents); + var newContents = string.Concat(bicepContents.AsSpan()[..offset], bicepContents.AsSpan(offset + length)); + BaseParser parser = languageId switch + { + LanguageId.Bicep => new Parser(newContents), + LanguageId.BicepParams => new ParamsParser(newContents), + _ => throw new ArgumentException($"Unexpected languageId value {languageId}"), + }; var program = parser.Program(); // Find the innermost string that contains the given offset, and which isn't inside an interpolation hole. // Note that a hole can contain nested strings which may contain holes... var stringSyntax = (StringSyntax?)program.TryFindMostSpecificNodeInclusive(offset, syntax => { - if (syntax is StringSyntax stringSyntax) + if (syntax is not StringSyntax stringSyntax) + { + return false; + } + + // The inclusive version of this function does not quite match what we want (and exclusive misses some valid offsets)... + // + // Example: 'str' (the syntax span includes the quotes) + // Span start is on the first "'", span end (exclusive) is after the last "'" + // An insertion with the cursor on the beginning "'" will end up before the string, not inside it. + // An insertion with the cursor on the ending "'" will end up in the string + if (offset <= syntax.Span.Position || offset >= syntax.GetEndPosition()) { - // The inclusive version of this function does not quite match what we want (and exclusive misses some valid offsets)... + // Ignore this node + return false; + } + + foreach (var interpolation in stringSyntax.Expressions) + { + // Remove expression holes from consideration (if they contain strings that will be caught in the next iteration) // - // Example: 'str' (the syntax span includes the quotes) - // Span start is on the first "'", span end (exclusive) is after the last "'" - // An insertion with the cursor on the beginning "'" will end up before the string, not inside it. - // An insertion with the cursor on the ending "'" will end up in the string - if (offset <= syntax.Span.Position || offset >= syntax.GetEndPosition()) + // Example: 'str${v1}', the expression node 'v1' does *not* include the ${ and } delimiters + // Span start is on the 'v', span end (exclusive) is on the '}' + // An insertion with the cursor on the v, 1 or '{' will end up inside the expression hole + if (offset >= interpolation.Span.Position && offset <= interpolation.GetEndPosition()) { // Ignore this node return false; } + } - foreach (var interpolation in stringSyntax.Expressions) - { - // Remove expression holes from consideration (if they contain strings that will be caught in the next iteration) - // - // Example: 'str${v1}', the expression node 'v1' does *not* include the ${ and } delimiters - // Span start is on the 'v', span end (exclusive) is on the '}' - // An insertion with the cursor on the v, 1 or '{' will end up inside the expression hole - if (offset >= interpolation.Span.Position && offset <= interpolation.GetEndPosition()) - { - // Ignore this node - return false; - } - } + return true; - return true; - } - else - { - return false; - } }); - return stringSyntax is null ? PasteContext.None : PasteContext.String; + if (stringSyntax is not null) + { + return PasteContext.String; + } + + if (languageId == LanguageId.BicepParams && program.TryFindMostSpecificNodeInclusive(0, syntax => syntax is UsingDeclarationSyntax) is not null) + { + return PasteContext.ParamsWithUsingDeclaration; + } + + return PasteContext.None; } private static string DisclaimerMessage => BicepDecompiler.DecompilerDisclaimerMessage; @@ -179,7 +182,7 @@ private async Task TryDecompileForPaste(string bicepContents { StringBuilder output = new(); var decompileId = Guid.NewGuid().ToString(); - var pasteContext = GetPasteContext(bicepContents, rangeOffset, rangeLength); + var pasteContext = GetPasteContext(bicepContents, rangeOffset, rangeLength, languageId); if (pasteContext == PasteContext.String) { @@ -205,38 +208,38 @@ private async Task TryDecompileForPaste(string bicepContents switch (pasteType) { case PasteType.None: - { - // It's not a template or resource. Try treating it as a JSON value. - var resultAndTelemetry = TryConvertFromJsonValue(output, json, decompileId, pasteContext, queryCanPaste); - if (resultAndTelemetry is not null) { - return resultAndTelemetry; - } + // It's not a template or resource. Try treating it as a JSON value. + var resultAndTelemetry = TryConvertFromJsonValue(output, json, decompileId, pasteContext, queryCanPaste); + if (resultAndTelemetry is not null) + { + return resultAndTelemetry; + } - break; - } + break; + } case PasteType.FullParams: - { - // It's a full parameters file - var result = TryConvertFromConstructedParameters(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); - if (result is not null) { - return result; - } + // It's a full parameters file + var result = TryConvertFromConstructedParameters(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); + if (result is not null) + { + return result; + } - break; - } + break; + } default: - { - // It's a full or partial template and we have converted it into a full template to parse - var result = await TryConvertFromConstructedTemplate(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); - if (result is not null) { - return result; - } + // It's a full or partial template and we have converted it into a full template to parse + var result = await TryConvertFromConstructedTemplate(output, json, decompileId, pasteContext, pasteType, queryCanPaste, constructedJsonTemplate); + if (result is not null) + { + return result; + } - break; - } + break; + } } // It's not anything we know how to convert to Bicep @@ -254,9 +257,9 @@ private async Task TryDecompileForPaste(string bicepContents { // Decompile the full template Debug.Assert(constructedJsonTemplate is not null); - Log(output, String.Format(LangServerResources.Decompile_DecompilationStartMsg, "clipboard text")); + Log(output, string.Format(LangServerResources.Decompile_DecompilationStartMsg, "clipboard text")); - var decompiler = new BicepDecompiler(this.bicepCompiler); + var decompiler = new BicepDecompiler(bicepCompiler); var options = GetDecompileOptions(pasteType); (_, filesToSave) = await decompiler.Decompile(BicepDummyUri, constructedJsonTemplate, options: options); } @@ -268,12 +271,12 @@ private async Task TryDecompileForPaste(string bicepContents var message = ex.Message; Log(output, $"Decompilation failed: {message}"); - return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), message, Bicep: null, Disclaimer: null), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicep: null)); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteTypeAsString(pasteType), message, Bicep: null, Disclaimer: null), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, PasteTypeAsString(pasteType), bicep: null)); } // Get Bicep output from the main file (all others are currently ignored) - string bicepOutput = filesToSave.Single(kvp => BicepDummyUri.Equals(kvp.Key)).Value; + var bicepOutput = filesToSave.Single(kvp => BicepDummyUri.Equals(kvp.Key)).Value; if (string.IsNullOrWhiteSpace(bicepOutput)) { @@ -285,8 +288,8 @@ private async Task TryDecompileForPaste(string bicepContents // Show disclaimer and return result Log(output, DisclaimerMessage); - return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), null, bicepOutput, DisclaimerMessage), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicepOutput)); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteTypeAsString(pasteType), null, bicepOutput, DisclaimerMessage), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, PasteTypeAsString(pasteType), bicepOutput)); } private ResultAndTelemetry? TryConvertFromConstructedParameters(StringBuilder output, string json, string decompileId, PasteContext pasteContext, PasteType pasteType, bool queryCanPaste, string? constructedJsonTemplate) @@ -298,8 +301,11 @@ private async Task TryDecompileForPaste(string bicepContents Debug.Assert(constructedJsonTemplate is not null); Log(output, string.Format(LangServerResources.Decompile_DecompilationStartMsg, "clipboard text")); - var decompiler = new BicepDecompiler(this.bicepCompiler); - (_, filesToSave) = decompiler.DecompileParameters(constructedJsonTemplate, BicepParamsDummyUri, null); + var decompiler = new BicepDecompiler(bicepCompiler); + (_, filesToSave) = decompiler.DecompileParameters(constructedJsonTemplate, BicepParamsDummyUri, null, new() + { + IncludeUsingDeclaration = pasteContext != PasteContext.ParamsWithUsingDeclaration + }); } catch (Exception ex) { @@ -309,8 +315,8 @@ private async Task TryDecompileForPaste(string bicepContents var message = ex.Message; Log(output, $"Decompilation failed: {message}"); - return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), message, Bicep: null, Disclaimer: null), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicep: null)); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteTypeAsString(pasteType), message, Bicep: null, Disclaimer: null), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, PasteTypeAsString(pasteType), bicep: null)); } // Get Bicep output from the main file (all others are currently ignored) @@ -326,8 +332,8 @@ private async Task TryDecompileForPaste(string bicepContents // Show disclaimer and return result Log(output, DisclaimerMessage); - return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), null, bicepOutput, DisclaimerMessage), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicepOutput)); + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteTypeAsString(pasteType), null, bicepOutput, DisclaimerMessage), + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, PasteTypeAsString(pasteType), bicepOutput)); } private static string PasteContextAsString(PasteContext pasteContext) @@ -336,6 +342,7 @@ private static string PasteContextAsString(PasteContext pasteContext) { PasteContext.None => "none", PasteContext.String => "string", + PasteContext.ParamsWithUsingDeclaration => "none", _ => throw new($"Unexpected pasteContext value {pasteContext}"), }; } @@ -406,9 +413,9 @@ private static DecompileOptions GetDecompileOptions(PasteType pasteType) } } - return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), pasteType.ConvertToString(), + return new(new(decompileId, output.ToString(), PasteContextAsString(pasteContext), PasteTypeAsString(pasteType), ErrorMessage: null, bicep, Disclaimer: null), - GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, pasteType.ConvertToString(), bicep)); + GetSuccessTelemetry(queryCanPaste, decompileId, json, pasteContext, PasteTypeAsString(pasteType), bicep)); } @@ -543,7 +550,7 @@ private static bool IsResourceObject(JObject? obj) { return null; } - else if (reader.TokenType == JsonToken.None) + if (reader.TokenType == JsonToken.None) { return null; } @@ -601,11 +608,8 @@ private static bool IsResourceObject(JObject? obj) return (PasteType.None, null); } - } - public static class BicepDecompileForPasteCommandHandlerExtensions - { - public static string? ConvertToString(this PasteType pasteType) => pasteType switch + private static string? PasteTypeAsString(PasteType pasteType) => pasteType switch { PasteType.None => null, PasteType.FullTemplate => "fullTemplate", diff --git a/src/vscode-bicep/src/commands/decompileParams.ts b/src/vscode-bicep/src/commands/decompileParams.ts index a4bcd9aaf4d..01f5b585dec 100644 --- a/src/vscode-bicep/src/commands/decompileParams.ts +++ b/src/vscode-bicep/src/commands/decompileParams.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import assert from "assert"; -import * as path from "path"; import { IActionContext, IAzureQuickPickItem, UserCancelledError } from "@microsoft/vscode-azext-utils"; +import assert from "assert"; import * as fse from "fs-extra"; +import * as path from "path"; import vscode, { MessageItem, Uri, window } from "vscode"; import { DocumentUri, LanguageClient } from "vscode-languageclient/node"; import { OutputChannelManager } from "../utils/OutputChannelManager"; From 5805c710f38921a0dc3416376133527fab4538bc Mon Sep 17 00:00:00 2001 From: Mikolaj Mackowiak <7921224+miqm@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:32:49 +0100 Subject: [PATCH 4/5] Remove commented out code --- ...eForPasteBicepParamsCommandHandlerTests.cs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs index 4693f0b37b7..59c6b4dd438 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using OmniSharp.Extensions.JsonRpc; -using static Bicep.LanguageServer.Handlers.BicepDecompileForPasteCommandHandler; namespace Bicep.LangServer.UnitTests.Handlers { @@ -116,33 +115,6 @@ private async Task TestDecompileForPaste(Options options) #region JSON/Bicep Constants - //private const string jsonFullArmTemplateMembers = """ - // "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - // "contentVersion": "1.0.0.0", - // "parameters": { - // "location": { - // "type": "string", - // "defaultValue": "[resourceGroup().location]" - // } - // }, - // "resources": [ - // { - // "type": "Microsoft.Storage/storageAccounts", - // "apiVersion": "2021-02-01", - // "name": "name", - // "location": "[parameters('location')]", - // "kind": "StorageV2", - // "sku": { - // "name": "Premium_LRS" - // } - // } - // ] - // """; - //private const string jsonFullArmTemplate = $$""" - // { - // {{jsonFullArmTemplateMembers}} - // } - // """; private const string jsonFullParamsTemplateMembers = """ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", From f729fbaff096cd109318d8da958b804890bda7cd Mon Sep 17 00:00:00 2001 From: Mikolaj Mackowiak <7921224+miqm@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:28:43 +0100 Subject: [PATCH 5/5] CR fixes --- ...eForPasteBicepParamsCommandHandlerTests.cs | 14 +------------- ...cepDecompileForPasteCommandHandlerTests.cs | 19 ++----------------- .../BicepDecompileForPasteCommandHandler.cs | 18 +++++++++--------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs index 59c6b4dd438..bcb77304ef6 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteBicepParamsCommandHandlerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using OmniSharp.Extensions.JsonRpc; +using static Bicep.LanguageServer.Handlers.BicepDecompileForPasteCommandHandler; namespace Bicep.LangServer.UnitTests.Handlers { @@ -33,19 +34,6 @@ private BicepDecompileForPasteCommandHandler CreateHandler(LanguageServerMock se return builder.Construct(); } - public enum PasteType - { - None, - JsonValue, - BicepValue, - FullParams - } - public enum PasteContext - { - None, - String - } - private record Options( string pastedJson, PasteType? expectedPasteType = null, diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs index 33985a61c9f..6bed801faac 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepDecompileForPasteCommandHandlerTests.cs @@ -34,22 +34,7 @@ private BicepDecompileForPasteCommandHandler CreateHandler(LanguageServerMock se return builder.Construct(); } - public enum PasteType - { - None, - FullTemplate, - SingleResource, - ResourceList, - JsonValue, - BicepValue, - } - public enum PasteContext - { - None, - String - } - - record Options( + private record Options( string pastedJson, PasteType? expectedPasteType = null, PasteContext expectedPasteContext = PasteContext.None, @@ -65,7 +50,7 @@ private async Task TestDecompileForPaste( string? expectedErrorMessage = null, string? editorContentsWithCursor = null) { - await TestDecompileForPaste(new Options( + await TestDecompileForPaste(new( json, expectedPasteType, PasteContext.None, diff --git a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs index 27d4f1ca7c0..3955cf66d28 100644 --- a/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDecompileForPasteCommandHandler.cs @@ -56,17 +56,17 @@ public class BicepDecompileForPasteCommandHandler( private static readonly Uri BicepDummyUri = PathHelper.ChangeToBicepExtension(JsonDummyUri); private static readonly Uri BicepParamsDummyUri = PathHelper.ChangeToBicepparamExtension(JsonDummyUri); - private enum PasteType + public enum PasteType { - None, - FullTemplate, // Full template - SingleResource, // Single resource - ResourceList,// List of multiple resources - JsonValue, // Single JSON value (number, object, array etc) - BicepValue, // JSON value that is also valid Bicep (e.g. "[1, {}]") - FullParams // Full parameters file + None = 0, + FullTemplate = 1, // Full template + SingleResource = 2, // Single resource + ResourceList = 3,// List of multiple resources + JsonValue = 4, // Single JSON value (number, object, array etc) + BicepValue = 5, // JSON value that is also valid Bicep (e.g. "[1, {}]") + FullParams = 6 // Full parameters file } - private enum PasteContext + public enum PasteContext { None, String, // Pasting inside a string