From 65effc83bf6a702035d2231d69cbeed92aab0218 Mon Sep 17 00:00:00 2001 From: Stephen Weatherford Date: Tue, 27 Aug 2024 12:04:56 -0700 Subject: [PATCH] Extract var/param refactorings --- examples.bicep | 514 ++++ .../CompileTimeImportTests.cs | 2 +- .../Emit/TemplateEmitterTests.cs | 4 +- src/Bicep.Core.IntegrationTests/LexerTests.cs | 4 +- .../ParserTests.cs | 4 +- .../PrettyPrint/PrettyPrinterV2Tests.cs | 8 +- .../Semantics/SemanticModelTests.cs | 12 +- .../Assertions/BaselineHelper.cs | 10 +- .../Assertions/BicepFileAssertions.cs | 7 +- .../Assertions/StringAssertionsExtensions.cs | 88 +- .../StringAssertionsExtensionsTests.cs | 51 + .../Baselines/BaselineFile.cs | 4 +- .../Baselines/BaselineFolder.cs | 4 +- .../Text/TextCoordinateConverterTests.cs | 25 + .../Utils/ParserHelper.cs | 76 +- .../Utils/ParserHelperTests.cs | 64 + .../Analyzers/Linter/Common/TypeExtensions.cs | 2 +- .../Linter/Rules/ArtifactsParametersRule.cs | 2 +- .../Rules/PreferUnquotedPropertyNamesRule.cs | 13 +- src/Bicep.Core/CodeAction/CodeFixKind.cs | 1 + .../CodeAction/Fixes/CodeFixHelper.cs | 2 +- .../Emit/EmitLimitationCalculator.cs | 2 +- src/Bicep.Core/Emit/EmitterSettings.cs | 2 +- src/Bicep.Core/Emit/TemplateWriter.cs | 4 +- .../Extensions/EnumerableExtensions.cs | 7 + src/Bicep.Core/Extensions/StringExtensions.cs | 27 + src/Bicep.Core/LanguageConstants.cs | 2 +- .../Navigation/SyntaxBaseExtensions.cs | 17 +- src/Bicep.Core/Parsing/BaseParser.cs | 4 +- src/Bicep.Core/Parsing/Parser.cs | 2 +- src/Bicep.Core/Parsing/StringUtils.cs | 17 +- src/Bicep.Core/Parsing/TextSpan.cs | 9 +- .../SyntaxLayouts.SyntaxVisitor.cs | 2 +- src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.cs | 2 +- .../Syntax/ISyntaxHierarchyExtensions.cs | 11 +- src/Bicep.Core/Syntax/ResourceTypeSyntax.cs | 2 +- src/Bicep.Core/Syntax/SyntaxBase.cs | 2 +- src/Bicep.Core/Syntax/SyntaxFactory.cs | 25 + src/Bicep.Core/Syntax/SyntaxStringifier.cs | 2 +- .../Text/TextCoordinateConverter.cs | 18 +- .../TypeSystem/DeclaredTypeManager.cs | 12 +- .../TypeSystem/ObjectTypeNameBuilder.cs | 4 +- .../TypeSystem/TypeAssignmentVisitor.cs | 4 +- src/Bicep.Core/TypeSystem/TypeHelper.cs | 14 +- src/Bicep.Core/packages.lock.json | 2 +- src/Bicep.Decompiler/TemplateConverter.cs | 9 +- src/Bicep.Decompiler/UniqueNamingResolver.cs | 2 +- .../CodeActionTestBase.cs | 190 ++ .../CodeActionTests.cs | 136 +- .../CompletionTests.cs | 4 +- .../ExtractVarAndParamTests.cs | 2363 +++++++++++++++++ .../HoverTests.cs | 2 +- .../TypeStringifierTests.cs | 756 ++++++ .../Completions/BicepCompletionContext.cs | 4 +- .../Completions/BicepCompletionProvider.cs | 23 +- .../Completions/SyntaxMatcher.cs | 16 +- .../Extensions/IPositionableExtensions.cs | 24 +- .../Handlers/BicepCodeActionHandler.cs | 45 +- .../Refactor/ExtractVarAndParam.cs | 432 +++ .../Refactor/TypeStringifier.cs | 283 ++ src/vs-bicep/package-lock.json | 6 + src/vscode-bicep/package.json | 9 + .../src/commands/StartRenameCommand.ts | 16 + src/vscode-bicep/src/commands/build.ts | 29 +- src/vscode-bicep/src/extension.ts | 2 + src/vscode-bicep/src/test/e2e/commands.ts | 2 +- 66 files changed, 5173 insertions(+), 270 deletions(-) create mode 100644 examples.bicep create mode 100644 src/Bicep.Core.UnitTests/Utils/ParserHelperTests.cs create mode 100644 src/Bicep.LangServer.IntegrationTests/CodeActionTestBase.cs create mode 100644 src/Bicep.LangServer.IntegrationTests/ExtractVarAndParamTests.cs create mode 100644 src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs create mode 100644 src/Bicep.LangServer/Refactor/ExtractVarAndParam.cs create mode 100644 src/Bicep.LangServer/Refactor/TypeStringifier.cs create mode 100644 src/vs-bicep/package-lock.json create mode 100644 src/vscode-bicep/src/commands/StartRenameCommand.ts diff --git a/examples.bicep b/examples.bicep new file mode 100644 index 00000000000..3c71e157a39 --- /dev/null +++ b/examples.bicep @@ -0,0 +1,514 @@ +asdfg remove file + +// DECISION: settings3: object? /* actual type: any? */ +// Unsupported union: widen to string? (comment) + +// [Jonny] +// Extracted value is in a var statement and has no declared type: the type will be based on the value. You might get recursive types or unions if the value contains a reference to a parameter, but you can pull the type clause from the parameter declaration. +var blah1 = [{ foo: 'bar' }, { foo: 'baz' }] + +// Extracted value is in a param statement (or something else with an explicit type declaration): you may be able to use the declared type syntax of the enclosing statement rather than working from the type backwards to a declaration. +param p1 { intVal: int } +param p2 object = p1 +param newParameter {} = p2 +var v1 = newParameter + +param newParameter2 [{ foo: string }, { foo: string }] = [{ foo: 'bar' }, { foo: 'baz' }] +var blah = newParameter2 + +// Extracted value is in a resource body: definite possibility of complex structures, recursion, and a few type constructs that aren't fully expressible in Bicep syntax (e.g., "open" enums like 'foo' | 'bar' | string). Resource-derived types might be a good solution here, but they're still behind a feature flag + +resource vmext 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = { + name: 'vmext/extension' + location: 'location' + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') // <<<<<<<<<<<<<<<<<<<<<<<<<<<< ISSUE - expecting string[] + ] + commandToExecute: 'commandToExecute' + } + } +} + +// [Anthony] +// If pulling from a var - do we have any existing logic or heuristic to do this? There may be different expectations - for example, it wouldn't be particularly useful to convert: +// +// var foo = { intVal: 2 } +// to: +// param foo { intVal: 2} + +// more likely the user would instead expect: +// param foo { intVal: int } + +var foo = { intVal: 2 } + +// var blah = [{foo: 'bar'}, {foo: 'baz'}] +// I would expect the user to more likely want: +// param blah {foo: string }[] +// rather than: +// param blah {foo: 'bar' | 'baz'}[] +// or: +// param blah ({foo: 'bar'}|{foo: 'baz'})[] + +var blah = [{ foo: 'bar' }, { foo: 'baz' }] + +// ISSUE - "any" + +var isWindowsOS = true +var provisionExtensions = true +param _artifactsLocation string +@secure() +param _artifactsLocationSasToken string + +param properties { + autoUpgradeMinorVersion: bool? + forceUpdateTag: string? + instanceView: { + name: string? + statuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + substatuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + type: string? + typeHandlerVersion: string? + }? + protectedSettings: any? + provisioningState: string + publisher: string? + settings: any? + type: string? + typeHandlerVersion: string? +}? = { + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< EXTRACT PROPERTIES + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } +} +resource vmextAny 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'vmextAny/extension' + location: 'location' + properties: properties +} + +// ISSUE - unsuppored unions + +param properties2 { + additionalCapabilities: { ultraSSDEnabled: bool? }? + availabilitySet: { id: string? }? + billingProfile: { maxPrice: int? }? + diagnosticsProfile: { bootDiagnostics: { enabled: bool?, storageUri: string? }? }? + evictionPolicy: 'Deallocate' | 'Delete' | string? + hardwareProfile: { + vmSize: + | 'Basic_A0' + | 'Basic_A1' + | 'Basic_A2' + | 'Basic_A3' + | 'Basic_A4' + | 'Standard_A0' + | 'Standard_A1' + | 'Standard_A10' + | 'Standard_A11' + | 'Standard_A1_v2' + | 'Standard_A2' + | 'Standard_A2_v2' + | 'Standard_A2m_v2' + | 'Standard_A3' + | 'Standard_A4' + | 'Standard_A4_v2' + | 'Standard_A4m_v2' + | 'Standard_A5' + | 'Standard_A6' + | 'Standard_A7' + | 'Standard_A8' + | 'Standard_A8_v2' + | 'Standard_A8m_v2' + | 'Standard_A9' + | 'Standard_B1ms' + | 'Standard_B1s' + | 'Standard_B2ms' + | 'Standard_B2s' + | 'Standard_B4ms' + | 'Standard_B8ms' + | 'Standard_D1' + | 'Standard_D11' + | 'Standard_D11_v2' + | 'Standard_D12' + | 'Standard_D12_v2' + | 'Standard_D13' + | 'Standard_D13_v2' + | 'Standard_D14' + | 'Standard_D14_v2' + | 'Standard_D15_v2' + | 'Standard_D16_v3' + | 'Standard_D16s_v3' + | 'Standard_D1_v2' + | 'Standard_D2' + | 'Standard_D2_v2' + | 'Standard_D2_v3' + | 'Standard_D2s_v3' + | 'Standard_D3' + | 'Standard_D32_v3' + | 'Standard_D32s_v3' + | 'Standard_D3_v2' + | 'Standard_D4' + | 'Standard_D4_v2' + | 'Standard_D4_v3' + | 'Standard_D4s_v3' + | 'Standard_D5_v2' + | 'Standard_D64_v3' + | 'Standard_D64s_v3' + | 'Standard_D8_v3' + | 'Standard_D8s_v3' + | 'Standard_DS1' + | 'Standard_DS11' + | 'Standard_DS11_v2' + | 'Standard_DS12' + | 'Standard_DS12_v2' + | 'Standard_DS13' + | 'Standard_DS13-2_v2' + | 'Standard_DS13-4_v2' + | 'Standard_DS13_v2' + | 'Standard_DS14' + | 'Standard_DS14-4_v2' + | 'Standard_DS14-8_v2' + | 'Standard_DS14_v2' + | 'Standard_DS15_v2' + | 'Standard_DS1_v2' + | 'Standard_DS2' + | 'Standard_DS2_v2' + | 'Standard_DS3' + | 'Standard_DS3_v2' + | 'Standard_DS4' + | 'Standard_DS4_v2' + | 'Standard_DS5_v2' + | 'Standard_E16_v3' + | 'Standard_E16s_v3' + | 'Standard_E2_v3' + | 'Standard_E2s_v3' + | 'Standard_E32-16_v3' + | 'Standard_E32-8s_v3' + | 'Standard_E32_v3' + | 'Standard_E32s_v3' + | 'Standard_E4_v3' + | 'Standard_E4s_v3' + | 'Standard_E64-16s_v3' + | 'Standard_E64-32s_v3' + | 'Standard_E64_v3' + | 'Standard_E64s_v3' + | 'Standard_E8_v3' + | 'Standard_E8s_v3' + | 'Standard_F1' + | 'Standard_F16' + | 'Standard_F16s' + | 'Standard_F16s_v2' + | 'Standard_F1s' + | 'Standard_F2' + | 'Standard_F2s' + | 'Standard_F2s_v2' + | 'Standard_F32s_v2' + | 'Standard_F4' + | 'Standard_F4s' + | 'Standard_F4s_v2' + | 'Standard_F64s_v2' + | 'Standard_F72s_v2' + | 'Standard_F8' + | 'Standard_F8s' + | 'Standard_F8s_v2' + | 'Standard_G1' + | 'Standard_G2' + | 'Standard_G3' + | 'Standard_G4' + | 'Standard_G5' + | 'Standard_GS1' + | 'Standard_GS2' + | 'Standard_GS3' + | 'Standard_GS4' + | 'Standard_GS4-4' + | 'Standard_GS4-8' + | 'Standard_GS5' + | 'Standard_GS5-16' + | 'Standard_GS5-8' + | 'Standard_H16' + | 'Standard_H16m' + | 'Standard_H16mr' + | 'Standard_H16r' + | 'Standard_H8' + | 'Standard_H8m' + | 'Standard_L16s' + | 'Standard_L32s' + | 'Standard_L4s' + | 'Standard_L8s' + | 'Standard_M128-32ms' + | 'Standard_M128-64ms' + | 'Standard_M128ms' + | 'Standard_M128s' + | 'Standard_M64-16ms' + | 'Standard_M64-32ms' + | 'Standard_M64ms' + | 'Standard_M64s' + | 'Standard_NC12' + | 'Standard_NC12s_v2' + | 'Standard_NC12s_v3' + | 'Standard_NC24' + | 'Standard_NC24r' + | 'Standard_NC24rs_v2' + | 'Standard_NC24rs_v3' + | 'Standard_NC24s_v2' + | 'Standard_NC24s_v3' + | 'Standard_NC6' + | 'Standard_NC6s_v2' + | 'Standard_NC6s_v3' + | 'Standard_ND12s' + | 'Standard_ND24rs' + | 'Standard_ND24s' + | 'Standard_ND6s' + | 'Standard_NV12' + | 'Standard_NV24' + | 'Standard_NV6' + | string? + }? + host: { id: string? }? + instanceView: { + bootDiagnostics: { + consoleScreenshotBlobUri: string + serialConsoleLogBlobUri: string + status: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + } + }? + computerName: string? + disks: { + encryptionSettings: { + diskEncryptionKey: { secretUrl: string, sourceVault: { id: string? } }? + enabled: bool? + keyEncryptionKey: { keyUrl: string, sourceVault: { id: string? } }? + }[]? + name: string? + statuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + }[]? + extensions: { + name: string? + statuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + substatuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + type: string? + typeHandlerVersion: string? + }[]? + hyperVGeneration: 'V1' | 'V2' | string? + maintenanceRedeployStatus: { + isCustomerInitiatedMaintenanceAllowed: bool? + lastOperationMessage: string? + lastOperationResultCode: 'MaintenanceAborted' | 'MaintenanceCompleted' | 'None' | 'RetryLater'? + maintenanceWindowEndTime: string? + maintenanceWindowStartTime: string? + preMaintenanceWindowEndTime: string? + preMaintenanceWindowStartTime: string? + }? + osName: string? + osVersion: string? + platformFaultDomain: int? + platformUpdateDomain: int? + rdpThumbPrint: string? + statuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + vmAgent: { + extensionHandlers: { + status: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }? + type: string? + typeHandlerVersion: string? + }[]? + statuses: { + code: string? + displayStatus: string? + level: 'Error' | 'Info' | 'Warning'? + message: string? + time: string? + }[]? + vmAgentVersion: string? + }? + } + licenseType: string? + networkProfile: { networkInterfaces: { id: string?, properties: { primary: bool? }? }[]? }? + osProfile: { + adminPassword: string? + adminUsername: string? + allowExtensionOperations: bool? + computerName: string? + customData: string? + linuxConfiguration: { + disablePasswordAuthentication: bool? + provisionVMAgent: bool? + ssh: { publicKeys: { keyData: string?, path: string? }[]? }? + }? + requireGuestProvisionSignal: bool? + secrets: { + sourceVault: { id: string? }? + vaultCertificates: { certificateStore: string?, certificateUrl: string? }[]? + }[]? + windowsConfiguration: { + additionalUnattendContent: { + componentName: string? + content: string? + passName: string? + settingName: 'AutoLogon' | 'FirstLogonCommands'? + }[]? + enableAutomaticUpdates: bool? + provisionVMAgent: bool? + timeZone: string? + winRM: { listeners: { certificateUrl: string?, protocol: 'Http' | 'Https'? }[]? }? + }? + }? + priority: 'Low' | 'Regular' | 'Spot' | string? + provisioningState: string + proximityPlacementGroup: { id: string? }? + storageProfile: { + dataDisks: { + caching: 'None' | 'ReadOnly' | 'ReadWrite'? + createOption: 'Attach' | 'Empty' | 'FromImage' | string + diskIOPSReadWrite: int + diskMBpsReadWrite: int + diskSizeGB: int? + image: { uri: string? }? + lun: int + managedDisk: { + diskEncryptionSet: { id: string? }? + id: string? + storageAccountType: 'Premium_LRS' | 'StandardSSD_LRS' | 'Standard_LRS' | 'UltraSSD_LRS' | string? + }? + name: string? + toBeDetached: bool? + vhd: { uri: string? }? + writeAcceleratorEnabled: bool? + }[]? + imageReference: { + exactVersion: string + id: string? + offer: string? + publisher: string? + sku: string? + version: string? + }? + osDisk: { + caching: 'None' | 'ReadOnly' | 'ReadWrite'? + createOption: 'Attach' | 'Empty' | 'FromImage' | string + diffDiskSettings: { option: 'Local' | string?, placement: 'CacheDisk' | 'ResourceDisk' | string? }? + diskSizeGB: int? + encryptionSettings: { + diskEncryptionKey: { secretUrl: string, sourceVault: { id: string? } }? + enabled: bool? + keyEncryptionKey: { keyUrl: string, sourceVault: { id: string? } }? + }? + image: { uri: string? }? + managedDisk: { + diskEncryptionSet: { id: string? }? + id: string? + storageAccountType: 'Premium_LRS' | 'StandardSSD_LRS' | 'Standard_LRS' | 'UltraSSD_LRS' | string? + }? + name: string? + osType: 'Linux' | 'Windows'? + vhd: { uri: string? }? + writeAcceleratorEnabled: bool? + }? + }? + virtualMachineScaleSet: { id: string? }? + vmId: string +}? = { + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< EXTRACT PROPERTIES + diagnosticsProfile: { + bootDiagnostics: { + storageUri: reference(existingStorageAccount.id, '2018-02-01').primaryEndpoints.blob + } + } +} +resource vmUnsupportedUnion 'Microsoft.Compute/virtualMachines@2019-12-01' = { + name: 'vmUnsupportedUnion' + location: 'eastus' + properties: properties2 +} + +resource existingStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { name: 'storageaccountname' } + +resource vm 'Microsoft.Compute/virtualMachines@2019-12-01' = { + name: 'vm' + location: 'eastus' + properties: { + diagnosticsProfile: { + bootDiagnostics: { + storageUri: reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob + } + } + } +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { name: 'storageaccountname' } + +type superComplexType = { + p: string + i: 123 | 456 +} + +param p { *: superComplexType } = { + a: { p: 'mystring', i: 123 } // <-- want to extract this value as param +} + +param super superComplexType +var v = super + +param pp { *: superComplexType } = { + a: { p: 'mystring', i: 123 } +} diff --git a/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs b/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs index be1d5d0c3fa..fba0c23cf67 100644 --- a/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs +++ b/src/Bicep.Core.IntegrationTests/CompileTimeImportTests.cs @@ -2265,7 +2265,7 @@ public void Tuple_imported_from_json_is_recompiled_to_a_valid_schema() { var typesBicep = """ @export() - type t = { + type testType = { p: { a: [ { diff --git a/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs b/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs index a800bb1555e..df13dc2be8d 100644 --- a/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs +++ b/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs @@ -157,8 +157,8 @@ public async Task SourceMap_maps_json_to_bicep_lines(DataSet dataSet) sourceTextWithSourceMap.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.SourceMap!, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSourceMap), - actualLocation: sourceTextWithSourceMapFileName); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSourceMap), + actualPath: sourceTextWithSourceMapFileName); } [TestMethod] diff --git a/src/Bicep.Core.IntegrationTests/LexerTests.cs b/src/Bicep.Core.IntegrationTests/LexerTests.cs index 47edcc264fd..4c8e3e9759c 100644 --- a/src/Bicep.Core.IntegrationTests/LexerTests.cs +++ b/src/Bicep.Core.IntegrationTests/LexerTests.cs @@ -108,8 +108,8 @@ string getLoggingString(Token token) sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.Tokens, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainTokens), - actualLocation: resultsFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainTokens), + actualPath: resultsFile); lexer.GetTokens().Count(token => token.Type == TokenType.EndOfFile).Should().Be(1, "because there should only be 1 EOF token"); lexer.GetTokens().Last().Type.Should().Be(TokenType.EndOfFile, "because the last token should always be EOF."); diff --git a/src/Bicep.Core.IntegrationTests/ParserTests.cs b/src/Bicep.Core.IntegrationTests/ParserTests.cs index cfd49910d06..73c4c5b1f0b 100644 --- a/src/Bicep.Core.IntegrationTests/ParserTests.cs +++ b/src/Bicep.Core.IntegrationTests/ParserTests.cs @@ -66,8 +66,8 @@ public void Parser_should_produce_expected_syntax(DataSet dataSet) sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.Syntax, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSyntax), - actualLocation: resultsFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSyntax), + actualPath: resultsFile); } [DataTestMethod] diff --git a/src/Bicep.Core.IntegrationTests/PrettyPrint/PrettyPrinterV2Tests.cs b/src/Bicep.Core.IntegrationTests/PrettyPrint/PrettyPrinterV2Tests.cs index a1f54db2e2c..9e008a2a36a 100644 --- a/src/Bicep.Core.IntegrationTests/PrettyPrint/PrettyPrinterV2Tests.cs +++ b/src/Bicep.Core.IntegrationTests/PrettyPrint/PrettyPrinterV2Tests.cs @@ -34,8 +34,8 @@ public void Print_VariousWidths_OptimizesLayoutAccordingly(int width) output.Should().EqualWithLineByLineDiffOutput( TestContext, expected, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, outputFileName), - actualLocation: outputFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, outputFileName), + actualPath: outputFile); AssertConsistentOutput(output, options); } @@ -52,8 +52,8 @@ public void Print_DataSet_ProducesExpectedOutput(DataSet dataSet) output.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.Formatted, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, outputFileName), - actualLocation: outputFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, outputFileName), + actualPath: outputFile); AssertConsistentOutput(output, PrettyPrinterV2Options.Default); } diff --git a/src/Bicep.Core.IntegrationTests/Semantics/SemanticModelTests.cs b/src/Bicep.Core.IntegrationTests/Semantics/SemanticModelTests.cs index 62b79f8f153..1fdef41b421 100644 --- a/src/Bicep.Core.IntegrationTests/Semantics/SemanticModelTests.cs +++ b/src/Bicep.Core.IntegrationTests/Semantics/SemanticModelTests.cs @@ -56,8 +56,8 @@ public async Task ProgramsShouldProduceExpectedDiagnostics(DataSet dataSet) sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.Diagnostics, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainDiagnostics), - actualLocation: resultsFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainDiagnostics), + actualPath: resultsFile); } [TestMethod] @@ -95,8 +95,8 @@ string getLoggingString(DeclaredSymbol symbol) sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.Symbols, - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSymbols), - actualLocation: resultsFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSymbols), + actualPath: resultsFile); } [DataTestMethod] @@ -346,8 +346,8 @@ public async Task ProgramsShouldProduceExpectedIrTree(DataSet dataSet) sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput( TestContext, dataSet.Ir ?? "", - expectedLocation: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainIr), - actualLocation: resultsFile); + expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainIr), + actualPath: resultsFile); } private static List GetAllBoundSymbolReferences(ProgramSyntax program) diff --git a/src/Bicep.Core.UnitTests/Assertions/BaselineHelper.cs b/src/Bicep.Core.UnitTests/Assertions/BaselineHelper.cs index 18e92fe11e5..039ba3a2dbd 100644 --- a/src/Bicep.Core.UnitTests/Assertions/BaselineHelper.cs +++ b/src/Bicep.Core.UnitTests/Assertions/BaselineHelper.cs @@ -18,18 +18,18 @@ public static class BaselineHelper public static bool ShouldSetBaseline(TestContext testContext) => testContext.Properties.Contains(SetBaseLineSettingName) && string.Equals(testContext.Properties[SetBaseLineSettingName] as string, bool.TrueString, StringComparison.OrdinalIgnoreCase); - public static void SetBaseline(string actualLocation, string expectedLocation) + public static void SetBaseline(string actualPath, string expectedPath) { - actualLocation = GetAbsolutePathRelativeToRepoRoot(actualLocation); - expectedLocation = GetAbsolutePathRelativeToRepoRoot(expectedLocation); + actualPath = GetAbsolutePathRelativeToRepoRoot(actualPath); + expectedPath = GetAbsolutePathRelativeToRepoRoot(expectedPath); - if (Path.GetDirectoryName(expectedLocation) is { } parentDir && + if (Path.GetDirectoryName(expectedPath) is { } parentDir && !Directory.Exists(parentDir)) { Directory.CreateDirectory(parentDir); } - File.Copy(actualLocation, expectedLocation, overwrite: true); + File.Copy(actualPath, expectedPath, overwrite: true); } public static string GetAbsolutePathRelativeToRepoRoot(string path) diff --git a/src/Bicep.Core.UnitTests/Assertions/BicepFileAssertions.cs b/src/Bicep.Core.UnitTests/Assertions/BicepFileAssertions.cs index a891fce7cd7..7f140082f6e 100644 --- a/src/Bicep.Core.UnitTests/Assertions/BicepFileAssertions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/BicepFileAssertions.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Bicep.Core.Diagnostics; +using Bicep.Core.Parsing; using Bicep.Core.Workspaces; using FluentAssertions; +using FluentAssertions.Execution; using FluentAssertions.Primitives; namespace Bicep.Core.UnitTests.Assertions @@ -26,7 +28,10 @@ public BicepFileAssertions(BicepFile bicepFile) public AndConstraint HaveSourceText(string expected, string because = "", params object[] becauseArgs) { - Subject.ProgramSyntax.ToString().Should().EqualIgnoringNewlines(expected, because, becauseArgs); + var actual = Subject.ProgramSyntax.ToString().NormalizeNewlines(); + expected = expected.NormalizeNewlines(); + + actual.Should().EqualWithLineByLineDiff(expected, because, becauseArgs); return new AndConstraint(this); } diff --git a/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs b/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs index 4a4a8c510ab..b9eb2d99f1a 100644 --- a/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using Bicep.Core.Parsing; +using Bicep.Core.PrettyPrintV2; +using Bicep.Core.Semantics; using Bicep.Core.UnitTests.Utils; using DiffPlex.DiffBuilder; using DiffPlex.DiffBuilder.Model; @@ -55,7 +59,39 @@ private static string GetDiffMarker(ChangeType type) return string.Join('\n', lineLogs); } - public static AndConstraint EqualWithLineByLineDiffOutput(this StringAssertions instance, TestContext testContext, string expected, string expectedLocation, string actualLocation, string because = "", params object[] becauseArgs) + public static AndConstraint EqualWithLineByLineDiff(this StringAssertions instance, string expected, string because = "", params object[] becauseArgs) + { + var lineDiff = CalculateDiff(expected, instance.Subject); + var hasNewlineDiffsOnly = lineDiff is null && !expected.Equals(instance.Subject, System.StringComparison.Ordinal); + var testPassed = lineDiff is null && !hasNewlineDiffsOnly; + + var output = new StringBuilder(); + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(testPassed) + .FailWith( + """ + Expected strings to be equal{reason}, but they are not. + ===== DIFF (--actual, ++expected) ===== + {4} + ===== ACTUAL (length {0}) ===== + {1} + ===== EXPECTED (length {2}) ===== + {3} + ===== END ===== + """, + instance.Subject.Length, + instance.Subject, + expected.Length, + expected, + lineDiff ?? "differences in newlines only"); + + return new AndConstraint(instance); + } + + public static AndConstraint EqualWithLineByLineDiffOutput(this StringAssertions instance, TestContext testContext, string expected, string expectedPath, string actualPath, string because = "", params object[] becauseArgs) { var lineDiff = CalculateDiff(expected, instance.Subject); var hasNewlineDiffsOnly = lineDiff is null && !expected.Equals(instance.Subject, System.StringComparison.Ordinal); @@ -64,7 +100,7 @@ public static AndConstraint EqualWithLineByLineDiffOutput(this var isBaselineUpdate = !testPassed && BaselineHelper.ShouldSetBaseline(testContext); if (isBaselineUpdate) { - BaselineHelper.SetBaseline(actualLocation, expectedLocation); + BaselineHelper.SetBaseline(actualPath, expectedPath); } Execute.Assertion @@ -73,8 +109,8 @@ public static AndConstraint EqualWithLineByLineDiffOutput(this .FailWith( BaselineHelper.GetAssertionFormatString(isBaselineUpdate), lineDiff ?? "differences in newlines only", - BaselineHelper.GetAbsolutePathRelativeToRepoRoot(actualLocation), - BaselineHelper.GetAbsolutePathRelativeToRepoRoot(expectedLocation)); + BaselineHelper.GetAbsolutePathRelativeToRepoRoot(actualPath), + BaselineHelper.GetAbsolutePathRelativeToRepoRoot(expectedPath)); return new AndConstraint(instance); } @@ -133,6 +169,50 @@ public static AndConstraint EqualIgnoringMinimumIndent(this St return new AndConstraint(instance); } + + /// + /// Compares two strings after normalizing by removing all whitespace + /// + public static AndConstraint EqualIgnoringWhitespace(this StringAssertions instance, string? expected, string because = "", params object[] becauseArgs) + { + var actualStringWithoutWhitespace = instance.Subject is null ? null : new Regex("\\s*").Replace(instance.Subject, ""); + var expectedStringWithoutWhitespace = expected is null ? null : new Regex("\\s*").Replace(expected, ""); + + using (new AssertionScope()) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(string.Equals(expectedStringWithoutWhitespace, actualStringWithoutWhitespace, StringComparison.Ordinal)) + .FailWith("Expected {context:string} to be {0}{reason} when ignoring whitespace, but found {1}. See next message for details.", expected, instance.Subject); + + actualStringWithoutWhitespace.Should().Be(expectedStringWithoutWhitespace); + } + + return new AndConstraint(instance); + } + + /// + /// Compares two strings after normalizing by pretty-printing both strings as Bicep + /// + public static AndConstraint EqualIgnoringBicepFormatting(this StringAssertions instance, string? expected, string because = "", params object[] becauseArgs) + { + var actualStringFormatted = instance.Subject is null ? null : PrettyPrintAsBicep(instance.Subject); + var expectedStringFormatted = expected is null ? null : PrettyPrintAsBicep(expected); + + using (new AssertionScope()) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(string.Equals(expectedStringFormatted, actualStringFormatted, StringComparison.Ordinal)) + .FailWith("Expected {context:string} to be {0}{reason} when ignoring Bicep formatting, but found {1}. See next message for details.", expected, instance.Subject); + + actualStringFormatted.Should().Be(expectedStringFormatted); + } + + return new AndConstraint(instance); + + static string PrettyPrintAsBicep(string s) => PrettyPrinterV2.PrintValid(CompilationHelper.Compile(s).SourceFile.ProgramSyntax, PrettyPrinterV2Options.Default); + } + /// /// Compares two strings after normalizing by removing whitespace from the beginning and ending of all lines /// diff --git a/src/Bicep.Core.UnitTests/AssertionsTests/StringAssertionsExtensionsTests.cs b/src/Bicep.Core.UnitTests/AssertionsTests/StringAssertionsExtensionsTests.cs index d507734e2f0..b91294a5238 100644 --- a/src/Bicep.Core.UnitTests/AssertionsTests/StringAssertionsExtensionsTests.cs +++ b/src/Bicep.Core.UnitTests/AssertionsTests/StringAssertionsExtensionsTests.cs @@ -36,5 +36,56 @@ public void NotContainAny_WithStringComparison(string text, IEnumerable actualMessage.Should().Be(expectedFailureMessage); } + + [TestMethod] + public void EqualWithLineByLineDiff() + { + string s1 = """ + abc + def + ghi + jkl + mno + """; + string s2 = """ + abc + def + jkl + mnop + """; + string expectedMessage = """ + Expected strings to be equal because I said so, but they are not. + ===== DIFF (--actual, ++expected) ===== + "[3] ++ ghi + [] -- mnop + [5] ++ mno" + ===== ACTUAL (length 19) ===== + "abc + def + ghi + jkl + mno" + ===== EXPECTED (length 16) ===== + "abc + def + jkl + mnop" + ===== END ===== + """; + + string? actualMessage; + try + { + s1.Should().EqualWithLineByLineDiff(s2, "because I said so"); + actualMessage = null; + } + catch (Exception ex) + { + actualMessage = ex.Message; + } + + actualMessage.Should().Be(expectedMessage); + + } } } diff --git a/src/Bicep.Core.UnitTests/Baselines/BaselineFile.cs b/src/Bicep.Core.UnitTests/Baselines/BaselineFile.cs index b0425efc615..14b28eb77b7 100644 --- a/src/Bicep.Core.UnitTests/Baselines/BaselineFile.cs +++ b/src/Bicep.Core.UnitTests/Baselines/BaselineFile.cs @@ -29,8 +29,8 @@ public void ShouldHaveExpectedValue() this.ReadFromOutputFolder().Should().EqualWithLineByLineDiffOutput( TestContext, EmbeddedFile.Contents, - expectedLocation: EmbeddedFile.RelativeSourcePath, - actualLocation: OutputFilePath); + expectedPath: EmbeddedFile.RelativeSourcePath, + actualPath: OutputFilePath); } public void ShouldHaveExpectedJsonValue() diff --git a/src/Bicep.Core.UnitTests/Baselines/BaselineFolder.cs b/src/Bicep.Core.UnitTests/Baselines/BaselineFolder.cs index f30c3ee0c38..3345351ae94 100644 --- a/src/Bicep.Core.UnitTests/Baselines/BaselineFolder.cs +++ b/src/Bicep.Core.UnitTests/Baselines/BaselineFolder.cs @@ -79,8 +79,8 @@ public BaselineFile GetFileOrEnsureCheckedIn(string relativePath) "".Should().EqualWithLineByLineDiffOutput( this.EntryFile.TestContext, "", - expectedLocation: embeddedFile.RelativeSourcePath, - actualLocation: outputFile); + expectedPath: embeddedFile.RelativeSourcePath, + actualPath: outputFile); throw new NotImplementedException("Code cannot reach this point as the previous line will always throw"); } } diff --git a/src/Bicep.Core.UnitTests/Text/TextCoordinateConverterTests.cs b/src/Bicep.Core.UnitTests/Text/TextCoordinateConverterTests.cs index 7b2d5b3ed21..18963f10c83 100644 --- a/src/Bicep.Core.UnitTests/Text/TextCoordinateConverterTests.cs +++ b/src/Bicep.Core.UnitTests/Text/TextCoordinateConverterTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Bicep.Core.Parsing; using Bicep.Core.Text; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -146,6 +147,30 @@ public static IEnumerable GetTestDataForGetPosition() yield return new object[] { new[] { 0, 12, 45 }, 45, (2, 0) }; yield return new object[] { new[] { 0, 12, 45 }, 99, (2, 54) }; } + + [DataTestMethod] + [DataRow("\n", new[] { 0, 10, 10, 25, 35, 1, 36, 25 })] + [DataRow("\r\n", new[] { 0, 11, 11, 26, 37, 2, 39, 25 })] + public void GetLineSpan(string newLine, int[] expectedLineSpanStartLengthPairs) + { + var pairsCount = expectedLineSpanStartLengthPairs.Length; + (pairsCount % 2).Should().Be(0, $"{nameof(pairsCount)} should be even"); + var expectedLineSpans = Enumerable.Range(0, pairsCount / 2) + .Select(i => new TextSpan((int)expectedLineSpanStartLengthPairs[i*2], (int)expectedLineSpanStartLengthPairs[i*2+1])) + .ToArray(); + + var program = + "// line 1" + newLine + + "metadata line2 = 'line2'" + newLine + + newLine + + "// previous line is empty"; + var lineStarts = TextCoordinateConverter.GetLineStarts(program); + var lineSpans = Enumerable.Range(0, lineStarts.Length) + .Select(line => TextCoordinateConverter.GetLineSpan(lineStarts, program.Length, line)) + .ToArray(); + + lineSpans.Should().BeEquivalentTo(expectedLineSpans); + } } } diff --git a/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs b/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs index d9e33624317..f8d04172759 100644 --- a/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text; using Bicep.Core.Diagnostics; using Bicep.Core.Parsing; using Bicep.Core.Syntax; @@ -58,12 +59,15 @@ public static ProgramSyntax ParamsParse(string text, out IDiagnosticLookup lexin public static SyntaxBase ParseExpression(string text, ExpressionFlags expressionFlags = ExpressionFlags.AllowComplexLiterals) => new Parser(text).Expression(expressionFlags); - public static (string file, IReadOnlyList cursors) GetFileWithCursors(string fileWithCursors, char cursor = '|') - => GetFileWithCursors(fileWithCursors, cursor.ToString()); + public static (string file, IReadOnlyList cursors) GetFileWithCursors(string fileWithCursors, char cursor = '|', string escapedCursor = "||") + => GetFileWithCursors(fileWithCursors, cursor.ToString(), escapedCursor); - public static (string file, IReadOnlyList cursors) GetFileWithCursors(string fileWithCursors, string cursor) + public static (string file, IReadOnlyList cursors) GetFileWithCursors(string fileWithCursors, string cursor, string escapedCursor) { - var fileWithoutCursors = fileWithCursors.Replace(cursor, ""); + var fileWithoutCursors = fileWithCursors + .Replace(escapedCursor, "<>") + .Replace(cursor, "") + .Replace("<>", ""); var cursors = new List(); var position = 0; @@ -76,16 +80,72 @@ public static (string file, IReadOnlyList cursors) GetFileWithCursors(strin return (fileWithoutCursors, cursors); } - public static (string file, int cursor) GetFileWithSingleCursor(string fileWithCursors, char cursor = '|') - => GetFileWithSingleCursor(fileWithCursors, cursor.ToString()); + public static (string file, int cursor) GetFileWithSingleCursor(string fileWithCursors, char cursor = '|', string escapedCursor = "||") + => GetFileWithSingleCursor(fileWithCursors, cursor.ToString(), escapedCursor); - public static (string file, int cursor) GetFileWithSingleCursor(string fileWithCursors, string cursor) + public static (string file, int cursor) GetFileWithSingleCursor(string fileWithCursors, string cursor, string escapedCursor = "||") { - var (file, cursors) = GetFileWithCursors(fileWithCursors, cursor); + var (file, cursors) = GetFileWithCursors(fileWithCursors, cursor, escapedCursor); cursors.Should().HaveCount(1); return (file, cursors.Single()); } + + public static (string file, TextSpan selection) GetFileWithSingleSelection(string fileWithSelections, string emptySelectionCursor = "|", string escapedCursor = "||", string selectionStartCursor = "<<", string selectionEndCursor = ">>") + { + var (file, selections) = GetFileWithSelections(fileWithSelections, emptySelectionCursor, escapedCursor, selectionStartCursor, selectionEndCursor); + selections.Should().HaveCount(1); + + return (file, selections.Single()); + } + + public static (string file, IReadOnlyList selections) GetFileWithSelections(string fileWithSelections, string emptySelectionCursor = "|", string escapedCursor = "||", string selectionStartCursor = "<<", string selectionEndCursor = ">>") + { + const string SELECTIONSTART = "SELECTIONSTART"; + const string SELECTIONEND = "SELECTIONEND"; + const string ESCAPEDCURSOR = "ESCAPEDCURSOR"; + + if (emptySelectionCursor.Length > 0) + { + fileWithSelections = fileWithSelections + .Replace(escapedCursor, ESCAPEDCURSOR) + .Replace(emptySelectionCursor, SELECTIONSTART + SELECTIONEND) + .Replace(ESCAPEDCURSOR, emptySelectionCursor); + } + + if (selectionStartCursor.Length > 0 && selectionEndCursor.Length > 0) + { + fileWithSelections = fileWithSelections.Replace(selectionStartCursor, SELECTIONSTART) + .Replace(selectionEndCursor, SELECTIONEND); + } + + var fileWithoutSelections = fileWithSelections; + + var selections = new List(); + int startPosition, endPosition; + int nextPosition = 0; + + while ((startPosition = fileWithoutSelections.IndexOf(SELECTIONSTART, nextPosition)) != -1 + && (endPosition = fileWithoutSelections.IndexOf(SELECTIONEND, startPosition)) != -1) + { + var span = new TextSpan(startPosition, endPosition - startPosition - SELECTIONSTART.Length); + + fileWithoutSelections = fileWithoutSelections.Substring(0, startPosition) + + fileWithoutSelections.Substring(startPosition + SELECTIONSTART.Length, span.Length) + + fileWithoutSelections.Substring(endPosition + SELECTIONEND.Length); + + selections.Add(span); + nextPosition = span.Position + span.Length; + } + + if (fileWithoutSelections.IndexOf(SELECTIONSTART) != -1 || fileWithoutSelections.IndexOf(SELECTIONEND) != -1) + { + throw new ArgumentException($"{nameof(GetFileWithSelections)}: Mismatched selection cursors in input string"); + } + + return (fileWithoutSelections, selections); + } + } } diff --git a/src/Bicep.Core.UnitTests/Utils/ParserHelperTests.cs b/src/Bicep.Core.UnitTests/Utils/ParserHelperTests.cs new file mode 100644 index 00000000000..dfe3bd30cfd --- /dev/null +++ b/src/Bicep.Core.UnitTests/Utils/ParserHelperTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Parsing; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.UnitTests.Utils +{ + [TestClass] + public class ParserHelperTests + { + private string GetTextAtSpan(string text, TextSpan span) + { + return text.Substring(span.Position, span.Length); + } + + [TestMethod] + public void EscapedCursor() + { + var (file, selections) = ParserHelper.GetFileWithSelections("This is the |first cursor, an ||escaped cursor, and the |second cursor"); + file.Should().Be("This is the first cursor, an |escaped cursor, and the second cursor"); + selections.Should().HaveCount(2); + + selections[0].Length.Should().Be(0); + GetTextAtSpan(file, new TextSpan(selections[0].Position, selections[0].Length + 5)).Should().Be("first"); + + selections[1].Length.Should().Be(0); + GetTextAtSpan(file, new TextSpan(selections[1].Position, selections[1].Length + 6)).Should().Be("second"); + } + + [TestMethod] + public void StartAndEndCursorsWithDifferentLengths() + { + var (file, selections) = ParserHelper.GetFileWithSelections("This is *a > with >", "*", "**", ">"); + file.Should().Be("This is a file with selections"); + selections.Should().HaveCount(3); + + selections[0].Length.Should().Be(0); + GetTextAtSpan(file, new TextSpan(selections[0].Position, selections[0].Length + 1)).Should().Be("a"); + GetTextAtSpan(file, selections[1]).Should().Be("file"); + GetTextAtSpan(file, selections[2]).Should().Be("selections"); + } + + [TestMethod] + public void SequentialSpans() + { + var (file, selections) = ParserHelper.GetFileWithSelections("This is|<><>"); // Using start/end with different lengths + file.Should().Be("This isafile"); + selections.Should().HaveCount(3); + + selections[0].Length.Should().Be(0); + GetTextAtSpan(file, new TextSpan(selections[0].Position, selections[0].Length + 1)).Should().Be("a"); + GetTextAtSpan(file, selections[1]).Should().Be("a"); + GetTextAtSpan(file, selections[2]).Should().Be("file"); + + string GetTextAtSpan(string text, TextSpan span) + { + return text.Substring(span.Position, span.Length); + } + } + } +} + diff --git a/src/Bicep.Core/Analyzers/Linter/Common/TypeExtensions.cs b/src/Bicep.Core/Analyzers/Linter/Common/TypeExtensions.cs index bf154c0d805..477841fa9ba 100644 --- a/src/Bicep.Core/Analyzers/Linter/Common/TypeExtensions.cs +++ b/src/Bicep.Core/Analyzers/Linter/Common/TypeExtensions.cs @@ -11,7 +11,7 @@ public static class TypeExtensions /// /// True if the given type symbol is a string type (and not "any") /// - public static bool IsString(this TypeSymbol typeSymbol) + public static bool IsString(this TypeSymbol typeSymbol)//asdfg { return typeSymbol is not AnyType && TypeValidator.AreTypesAssignable(typeSymbol, LanguageConstants.String); diff --git a/src/Bicep.Core/Analyzers/Linter/Rules/ArtifactsParametersRule.cs b/src/Bicep.Core/Analyzers/Linter/Rules/ArtifactsParametersRule.cs index f4589385d62..bee7bf2215b 100644 --- a/src/Bicep.Core/Analyzers/Linter/Rules/ArtifactsParametersRule.cs +++ b/src/Bicep.Core/Analyzers/Linter/Rules/ArtifactsParametersRule.cs @@ -216,7 +216,7 @@ private IEnumerable VerifyDefaultValues( return null; } - private static string? GetParameterType(SemanticModel model, ParameterSymbol parameterSymbol) + private static string? GetParameterType(SemanticModel model, ParameterSymbol parameterSymbol) //asdfg? { if (parameterSymbol.DeclaringSyntax is ParameterDeclarationSyntax parameterDeclaration && parameterDeclaration.Type is TypeVariableAccessSyntax typeSyntax) diff --git a/src/Bicep.Core/Analyzers/Linter/Rules/PreferUnquotedPropertyNamesRule.cs b/src/Bicep.Core/Analyzers/Linter/Rules/PreferUnquotedPropertyNamesRule.cs index c886bcbffc5..b664207f14a 100644 --- a/src/Bicep.Core/Analyzers/Linter/Rules/PreferUnquotedPropertyNamesRule.cs +++ b/src/Bicep.Core/Analyzers/Linter/Rules/PreferUnquotedPropertyNamesRule.cs @@ -70,20 +70,15 @@ private void AddCodeFix(TextSpan span, string replacement, string description) private static bool TryGetValidIdentifierToken(SyntaxBase syntax, [NotNullWhen(true)] out string? validToken) { + validToken = null; + if (syntax is StringSyntax stringSyntax && stringSyntax.TryGetLiteralValue() is { } literalValue) { - if (Lexer.IsValidIdentifier(literalValue) && - // exclude non-contextual keywords like 'nul and 'true' - see https://github.com/Azure/bicep/issues/13347. - !LanguageConstants.NonContextualKeywords.ContainsKey(literalValue)) - { - validToken = literalValue; - return true; - } + validToken = StringUtils.EscapeBicepPropertyName(literalValue); } - validToken = null; - return false; + return validToken != null; } } } diff --git a/src/Bicep.Core/CodeAction/CodeFixKind.cs b/src/Bicep.Core/CodeAction/CodeFixKind.cs index bb162fa56da..d4780113529 100644 --- a/src/Bicep.Core/CodeAction/CodeFixKind.cs +++ b/src/Bicep.Core/CodeAction/CodeFixKind.cs @@ -7,5 +7,6 @@ public enum CodeFixKind { QuickFix, Refactor, + RefactorExtract, } } diff --git a/src/Bicep.Core/CodeAction/Fixes/CodeFixHelper.cs b/src/Bicep.Core/CodeAction/Fixes/CodeFixHelper.cs index 647bdf8e4dc..aa3eededba4 100644 --- a/src/Bicep.Core/CodeAction/Fixes/CodeFixHelper.cs +++ b/src/Bicep.Core/CodeAction/Fixes/CodeFixHelper.cs @@ -13,7 +13,7 @@ namespace Bicep.Core.CodeAction.Fixes; public static class CodeFixHelper { - public static CodeFix GetCodeFixForMissingBicepParams(ProgramSyntax program, ImmutableArray missingRequiredParams) + public static CodeFix GetCodeFixForMissingBicepParams(ProgramSyntax program, ImmutableArray missingRequiredParams) //asdfg interesting { var terminatingNewlines = program.Children.LastOrDefault() is Token { Type: TokenType.NewLine } newLineToken ? StringUtils.CountNewlines(newLineToken.Text) : diff --git a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs index 53e70215730..4daf513e0c6 100644 --- a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs +++ b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs @@ -688,7 +688,7 @@ private static void BlockResourceDerivedTypesThatDoNotDereferenceProperties(Sema { static bool IsPermittedResourceDerivedTypeParent(IBinder binder, SyntaxBase? syntax) => syntax switch { - ParenthesizedExpressionSyntax or + ParenthesizedExpressionSyntax or //asdfg? NonNullAssertionSyntax or NullableTypeSyntax => IsPermittedResourceDerivedTypeParent(binder, binder.GetParent(syntax)), TypePropertyAccessSyntax or diff --git a/src/Bicep.Core/Emit/EmitterSettings.cs b/src/Bicep.Core/Emit/EmitterSettings.cs index cb5319f9bf2..154c47ece74 100644 --- a/src/Bicep.Core/Emit/EmitterSettings.cs +++ b/src/Bicep.Core/Emit/EmitterSettings.cs @@ -32,7 +32,7 @@ public EmitterSettings(SemanticModel model) SyntaxAggregator.Aggregate(model.SourceFile.ProgramSyntax, seed: false, function: (hasUserDefinedTypeSyntax, syntax) => hasUserDefinedTypeSyntax || - syntax is ObjectTypeSyntax || + syntax is ObjectTypeSyntax || //asdfg? syntax is ArrayTypeSyntax || syntax is TupleTypeSyntax || syntax is UnionTypeSyntax || diff --git a/src/Bicep.Core/Emit/TemplateWriter.cs b/src/Bicep.Core/Emit/TemplateWriter.cs index 251d842ac5d..89f58c66b91 100644 --- a/src/Bicep.Core/Emit/TemplateWriter.cs +++ b/src/Bicep.Core/Emit/TemplateWriter.cs @@ -686,7 +686,7 @@ private static ObjectExpression TypePropertiesForQualifiedReference(FullyQualifi } private static ObjectPropertyExpression TypeProperty(string typeName, SyntaxBase? sourceSyntax) - => Property(TypePropertyName, new StringLiteralExpression(sourceSyntax, typeName), sourceSyntax); + => Property(TypePropertyName, new StringLiteralExpression(sourceSyntax, typeName), sourceSyntax);//asdfg private static ObjectPropertyExpression AllowedValuesProperty(ArrayExpression allowedValues, SyntaxBase? sourceSyntax) => Property("allowedValues", allowedValues, sourceSyntax); @@ -694,7 +694,7 @@ private static ObjectPropertyExpression AllowedValuesProperty(ArrayExpression al private static ObjectPropertyExpression Property(string name, Expression value, SyntaxBase? sourceSyntax) => ExpressionFactory.CreateObjectProperty(name, value, sourceSyntax); - private static ObjectExpression GetTypePropertiesForResourceType(ResourceTypeExpression expression) + private static ObjectExpression GetTypePropertiesForResourceType(ResourceTypeExpression expression)//asdfg { var typeString = expression.ExpressedResourceType.TypeReference.FormatName(); diff --git a/src/Bicep.Core/Extensions/EnumerableExtensions.cs b/src/Bicep.Core/Extensions/EnumerableExtensions.cs index a7642f2ad63..947630453ef 100644 --- a/src/Bicep.Core/Extensions/EnumerableExtensions.cs +++ b/src/Bicep.Core/Extensions/EnumerableExtensions.cs @@ -67,6 +67,13 @@ public static IEnumerable WhereNotNull(this IEnumerable source) } } + public static IEnumerable TakeWhileNotNull(this IEnumerable source) + where T : class + { + return source.TakeWhile(x => x is not null) + .Cast(); + } + public static T[] ToArrayExcludingNull(this IEnumerable source) where T : class => source.WhereNotNull().ToArray(); diff --git a/src/Bicep.Core/Extensions/StringExtensions.cs b/src/Bicep.Core/Extensions/StringExtensions.cs index 7640e6accd6..6c05ec1bb46 100644 --- a/src/Bicep.Core/Extensions/StringExtensions.cs +++ b/src/Bicep.Core/Extensions/StringExtensions.cs @@ -20,4 +20,31 @@ public static string Rfc6901Decode(this string toDecode) /// The encoded path segment public static string Rfc6901Encode(this string toEncode) => Uri.EscapeDataString(toEncode.Replace("~", "~0").Replace("/", "~1")); + + public static string TruncateWithEllipses(this string input, int maxLength) + { + if (maxLength < 4) + { + throw new ArgumentException("maxLength must be at least 4"); + } + + if (input.Length > maxLength) + { + return input.Substring(0, maxLength - 3) + "..."; + } + + return input; + } + + public static string UppercaseFirstLetter(this string input) + { + if (input.Length > 0) + { + return char.ToUpperInvariant(input[0]) + input[1..]; + }else + { + return input; + } + } + } diff --git a/src/Bicep.Core/LanguageConstants.cs b/src/Bicep.Core/LanguageConstants.cs index 40b07bcc5c5..b9dbaecba37 100644 --- a/src/Bicep.Core/LanguageConstants.cs +++ b/src/Bicep.Core/LanguageConstants.cs @@ -210,7 +210,7 @@ public static class LanguageConstants // the type of the dependsOn property in module and resource bodies public static readonly TypeSymbol ResourceOrResourceCollectionRefArray = new TypedArrayType(ResourceOrResourceCollectionRefItem, TypeSymbolValidationFlags.Default); - public static readonly TypeSymbol String = TypeFactory.CreateStringType(); + public static readonly TypeSymbol String = TypeFactory.CreateStringType(); //asdfg // LooseString should be regarded as equal to the 'string' type, but with different validation behavior public static readonly TypeSymbol LooseString = TypeFactory.CreateStringType(validationFlags: TypeSymbolValidationFlags.AllowLooseAssignment); // SecureString should be regarded as equal to the 'string' type, but with different validation behavior diff --git a/src/Bicep.Core/Navigation/SyntaxBaseExtensions.cs b/src/Bicep.Core/Navigation/SyntaxBaseExtensions.cs index a3db74d4369..d5cb771567a 100644 --- a/src/Bicep.Core/Navigation/SyntaxBaseExtensions.cs +++ b/src/Bicep.Core/Navigation/SyntaxBaseExtensions.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Bicep.Core.Parsing; using Bicep.Core.Syntax; -namespace Bicep.Core.Navigation +namespace Bicep.Core.Navigation //asdfg wrong place for SpanWithTrivia { public static class SyntaxBaseExtensions { @@ -20,6 +21,20 @@ public static class SyntaxBaseExtensions return visitor.Result; } + //asdfg + //public static TextSpan SpanWithTrivia(this SyntaxBase root) + //{ + // if (root is Token token) + // { + // List triviaSpans = [ + // ..token.LeadingTrivia.Select(x => x.Span), + // ..token.TrailingTrivia.Select(x => x.Span)]; + // return triviaSpans.Aggregate(root.Span, TextSpan.Between); + // } + + // return root.Span; + //} + private sealed class NavigationSearchVisitor : CstVisitor { private readonly int offset; diff --git a/src/Bicep.Core/Parsing/BaseParser.cs b/src/Bicep.Core/Parsing/BaseParser.cs index b87fdb3b7b1..6ab343ce3c3 100644 --- a/src/Bicep.Core/Parsing/BaseParser.cs +++ b/src/Bicep.Core/Parsing/BaseParser.cs @@ -1431,7 +1431,7 @@ protected SyntaxBase Type(bool allowOptionalResourceType) protected SyntaxBase TypeExpression() { - // Parse optional leading '|' for union types. + // Parse optional leading '|' for union types. asdfg? List? unionTypeNodes = HasUnionMemberSeparator() ? new(NewLines()) { reader.Read() } : null; @@ -1536,7 +1536,7 @@ protected IEnumerable DecorableSyntaxLeadingNodes() { yield return this.Decorator(); - // All decorators must followed by a newline. + // All decorators must be followed by a newline. yield return this.WithRecovery(this.NewLine, RecoveryFlags.ConsumeTerminator, TokenType.NewLine); diff --git a/src/Bicep.Core/Parsing/Parser.cs b/src/Bicep.Core/Parsing/Parser.cs index c347ef9c646..75adce47bdb 100644 --- a/src/Bicep.Core/Parsing/Parser.cs +++ b/src/Bicep.Core/Parsing/Parser.cs @@ -117,7 +117,7 @@ private SyntaxBase ParameterDeclaration(IEnumerable leadingNodes) { var keyword = ExpectKeyword(LanguageConstants.ParameterKeyword); var name = this.IdentifierWithRecovery(b => b.ExpectedParameterIdentifier(), RecoveryFlags.None, TokenType.Identifier, TokenType.NewLine); - var type = this.WithRecovery(() => Type(allowOptionalResourceType: false), GetSuppressionFlag(name), TokenType.Assignment, TokenType.LeftBrace, TokenType.NewLine); + var type = this.WithRecovery(() => Type(allowOptionalResourceType: false/*asdfg?*/), GetSuppressionFlag(name), TokenType.Assignment, TokenType.LeftBrace, TokenType.NewLine); // TODO: Need a better way to choose the terminating token SyntaxBase? modifier = this.WithRecoveryNullable( diff --git a/src/Bicep.Core/Parsing/StringUtils.cs b/src/Bicep.Core/Parsing/StringUtils.cs index 923aecb5b67..b2b4c97b1c3 100644 --- a/src/Bicep.Core/Parsing/StringUtils.cs +++ b/src/Bicep.Core/Parsing/StringUtils.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Bicep.Core.PrettyPrint.Options; using Bicep.Core.PrettyPrintV2; +using Bicep.Core.TypeSystem.Types; namespace Bicep.Core.Parsing { @@ -13,6 +14,7 @@ public static partial class StringUtils [GeneratedRegex(@"(\r\n|\r|\n)")] private static partial Regex NewLineRegex(); + //asdfg move? public static string EscapeBicepString(string value) => EscapeBicepString(value, "'", "'"); @@ -53,13 +55,26 @@ public static string EscapeBicepString(string value, string startString, string return buffer.ToString(); } + public static bool IsPropertyNameEscapingRequired(string propertyName) => + !Lexer.IsValidIdentifier(propertyName) || LanguageConstants.NonContextualKeywords.ContainsKey(propertyName); + + public static string EscapeBicepPropertyName(string propertyName) + { + return IsPropertyNameEscapingRequired(propertyName) + ? EscapeBicepString(propertyName) + : propertyName; + } + public static int CountNewlines(string value) => NewLineRegex().Matches(value).Count; public static string MatchNewline(string value) => NewLineRegex().Match(value).Value; - public static string ReplaceNewlines(string value, string newlineReplacement) => + public static string ReplaceNewlines(this string value, string newlineReplacement) => NewLineRegex().Replace(value, newlineReplacement); + public static string NormalizeNewlines(this string value) => + ReplaceNewlines(value, "\n"); + public static IEnumerable SplitOnNewLine(string value) => value.Split( new string[] { "\r\n", "\r", "\n" }, diff --git a/src/Bicep.Core/Parsing/TextSpan.cs b/src/Bicep.Core/Parsing/TextSpan.cs index f2af4ab800b..227a929250e 100644 --- a/src/Bicep.Core/Parsing/TextSpan.cs +++ b/src/Bicep.Core/Parsing/TextSpan.cs @@ -45,16 +45,21 @@ public TextSpan(int position, int length) public override string ToString() => $"[{Position}:{Position + Length}]"; + public TextSpan MoveBy(int offset) + { + return new TextSpan(this.Position +offset, this.Length); + } + public bool Contains(int offset) => offset >= this.Position && offset < this.Position + this.Length; public bool ContainsInclusive(int offset) => offset >= this.Position && offset <= this.Position + this.Length; /// - /// Calculates the span from the beginning of the first span to the end of the second span. + /// Calculates the span from the beginning of the first (by coordinate position) span to the end of the last span (i.e., the union) /// /// The first span /// The second span - /// the span from the beginning of the first span to the end of the second span + /// the span from the beginning of the earlier span to the end of the second span public static TextSpan Between(TextSpan a, TextSpan b) { if (a.IsNil || b.IsNil) diff --git a/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.SyntaxVisitor.cs b/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.SyntaxVisitor.cs index aa6fcace16b..0c3b9c9c906 100644 --- a/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.SyntaxVisitor.cs +++ b/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.SyntaxVisitor.cs @@ -211,7 +211,7 @@ private void Apply(TSyntax syntax, SyntaxLayoutSpecifier layou where TSyntax : SyntaxBase { this.current = syntax is ITopLevelDeclarationSyntax && this.context.HasSyntaxError(syntax) - ? TextDocument.From(SyntaxStringifier.Stringify(syntax, this.context.Newline).Trim()) + ? TextDocument.From(SyntaxStringifier.Stringify(syntax, this.context.Newline).Trim())//asdfg : layoutSpecifier(syntax); } } diff --git a/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.cs b/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.cs index 1babb05e5a8..9ae5e5e321b 100644 --- a/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.cs +++ b/src/Bicep.Core/PrettyPrintV2/SyntaxLayouts.cs @@ -792,7 +792,7 @@ private static IEnumerable LayoutWithTrailingTrivia(Token token, IEnum return danglingLeadingTrivia.Any() ? danglingLeadingTrivia.Append(text) : text; } - private static IEnumerable LayoutWithLeadingAndTrailingTrivia(Document text, IEnumerable leadingTrivia, IEnumerable trailingTrivia, SuffixDocument? suffix) + private static IEnumerable LayoutWithLeadingAndTrailingTrivia/*asdfg*/(Document text, IEnumerable leadingTrivia, IEnumerable trailingTrivia, SuffixDocument? suffix) { if (leadingTrivia.Any() || trailingTrivia.Any()) { diff --git a/src/Bicep.Core/Syntax/ISyntaxHierarchyExtensions.cs b/src/Bicep.Core/Syntax/ISyntaxHierarchyExtensions.cs index fd8eee1e8f0..f81e9e0e98d 100644 --- a/src/Bicep.Core/Syntax/ISyntaxHierarchyExtensions.cs +++ b/src/Bicep.Core/Syntax/ISyntaxHierarchyExtensions.cs @@ -10,8 +10,13 @@ public static class ISyntaxHierarchyExtensions /// /// Enumerate ancestor nodes in ascending order. /// - public static IEnumerable EnumerateAncestorsUpwards(this ISyntaxHierarchy hierarchy, SyntaxBase syntax) + public static IEnumerable EnumerateAncestorsUpwards(this ISyntaxHierarchy hierarchy, SyntaxBase syntax, bool includeSelf = false) { + if (includeSelf) + { + yield return syntax; + } + var parent = hierarchy.GetParent(syntax); while (parent is not null) { @@ -42,9 +47,9 @@ public static ImmutableArray GetAllAncestors(this ISyntaxHiera /// The syntax node. /// The type of node to query. /// The nearest ancestor or null. - public static TSyntax? GetNearestAncestor(this ISyntaxHierarchy hierarchy, SyntaxBase syntax) + public static TSyntax? GetNearestAncestor(this ISyntaxHierarchy hierarchy, SyntaxBase syntax, bool includeSelf = false) //adsfg not needed? where TSyntax : SyntaxBase - => EnumerateAncestorsUpwards(hierarchy, syntax) + => EnumerateAncestorsUpwards(hierarchy, syntax, includeSelf) .OfType() .FirstOrDefault(); diff --git a/src/Bicep.Core/Syntax/ResourceTypeSyntax.cs b/src/Bicep.Core/Syntax/ResourceTypeSyntax.cs index 7ec934eeb46..622eeb71187 100644 --- a/src/Bicep.Core/Syntax/ResourceTypeSyntax.cs +++ b/src/Bicep.Core/Syntax/ResourceTypeSyntax.cs @@ -4,7 +4,7 @@ namespace Bicep.Core.Syntax { - public class ResourceTypeSyntax : TypeSyntax + public class ResourceTypeSyntax : TypeSyntax//asdfg { public ResourceTypeSyntax(Token keyword, SyntaxBase? type) { diff --git a/src/Bicep.Core/Syntax/SyntaxBase.cs b/src/Bicep.Core/Syntax/SyntaxBase.cs index 460abf6ea45..f1caa31a956 100644 --- a/src/Bicep.Core/Syntax/SyntaxBase.cs +++ b/src/Bicep.Core/Syntax/SyntaxBase.cs @@ -85,6 +85,6 @@ protected static void AssertSyntaxType(SyntaxBase? syntax, [InvokerParameterName /// public override string ToString() => SyntaxStringifier.Stringify(this); - public string GetDebuggerDisplay() => ToString(); + public string GetDebuggerDisplay() => $"[{GetType().Name}] {ToString()}"; } } diff --git a/src/Bicep.Core/Syntax/SyntaxFactory.cs b/src/Bicep.Core/Syntax/SyntaxFactory.cs index 7126ba5f4a9..65fa4a0a09e 100644 --- a/src/Bicep.Core/Syntax/SyntaxFactory.cs +++ b/src/Bicep.Core/Syntax/SyntaxFactory.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.ComponentModel; using Bicep.Core.Extensions; +using Bicep.Core.Intermediate; using Bicep.Core.Parsing; using Json.Pointer; @@ -389,5 +390,29 @@ public static BinaryOperationSyntax CreateBinaryOperationSyntax(SyntaxBase left, public static ParenthesizedExpressionSyntax CreateParenthesized(SyntaxBase inner) => new(LeftParenToken, inner, RightParenToken); + + public static VariableDeclarationSyntax CreateVariableDeclaration(string name, SyntaxBase value) + => new( + [], + VariableKeywordToken, + CreateIdentifierWithTrailingSpace(name), + AssignmentToken, + value); + + public static ParameterDeclarationSyntax CreateParameterDeclaration(string name, SyntaxBase type, SyntaxBase? defaultValue = null, IEnumerable? leadingNodes = null) + => new( + leadingNodes ?? [], + ParameterKeywordToken, + CreateIdentifierWithTrailingSpace(name), + type, + //asdfg +/* + syntax is ObjectTypeSyntax || //asdfg? + syntax is ArrayTypeSyntax || + syntax is TupleTypeSyntax || + syntax is UnionTypeSyntax || + syntax is NullableTypeSyntax, +*/ + defaultValue is { } ? new ParameterDefaultValueSyntax(AssignmentToken, defaultValue) : null); } } diff --git a/src/Bicep.Core/Syntax/SyntaxStringifier.cs b/src/Bicep.Core/Syntax/SyntaxStringifier.cs index 3ecc6b80e99..3e824ae8068 100644 --- a/src/Bicep.Core/Syntax/SyntaxStringifier.cs +++ b/src/Bicep.Core/Syntax/SyntaxStringifier.cs @@ -27,7 +27,7 @@ public static string Stringify(SyntaxBase syntax, string? newlineReplacement = n return writer.ToString(); } - public static void StringifyTo(TextWriter writer, SyntaxBase syntax, string? newlineReplacement = null) + public static void StringifyTo(TextWriter writer, SyntaxBase syntax, string? newlineReplacement = null)//asdfg { var stringifier = new SyntaxStringifier(writer, newlineReplacement); diff --git a/src/Bicep.Core/Text/TextCoordinateConverter.cs b/src/Bicep.Core/Text/TextCoordinateConverter.cs index eb9c09f81ca..b4b4c65074c 100644 --- a/src/Bicep.Core/Text/TextCoordinateConverter.cs +++ b/src/Bicep.Core/Text/TextCoordinateConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Immutable; +using Bicep.Core.Parsing; namespace Bicep.Core.Text { @@ -30,7 +31,7 @@ public static ImmutableArray GetLineStarts(string text) } } - return [.. lineStarts]; + return [..lineStarts]; } public static (int line, int character) GetPosition(IReadOnlyList lineStarts, int offset) @@ -72,6 +73,21 @@ public static int GetOffset(IReadOnlyList lineStarts, int line, int charact return lineStarts[line] + character; } + public static TextSpan GetLineSpan(IReadOnlyList lineStarts, int programLength, int line) //asdfg test + { + int lineStart = GetOffset(lineStarts, line, 0); + if (line == lineStarts.Count - 1) + { + return new TextSpan(lineStart, programLength - lineStart); + } + else + { + int nextLineStart = GetOffset(lineStarts, line + 1, 0); + return new TextSpan(lineStart, nextLineStart - lineStart); + } + } + + // If the actual line start was not found, returns the 2's-complement of the next line start private static int BinarySearch(IReadOnlyList values, int target) { int start = 0; diff --git a/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs b/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs index 4ab117b8095..7710309ae9f 100644 --- a/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs +++ b/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs @@ -478,7 +478,7 @@ private ITypeReference GetTypeFromTypeSyntax(SyntaxBase syntax) => TryGetTypeFro private ITypeReference? TryGetTypeFromTypeSyntax(SyntaxBase syntax) => TryGetTypeAssignmentFromTypeSyntax(syntax)?.Reference; - private DeclaredTypeAssignment? TryGetTypeAssignmentFromTypeSyntax(SyntaxBase syntax) => declaredTypes.GetOrAdd(syntax, s => + private DeclaredTypeAssignment? TryGetTypeAssignmentFromTypeSyntax(SyntaxBase syntax) => declaredTypes.GetOrAdd(syntax, s =>//asdfgasdfg { RuntimeHelpers.EnsureSufficientExecutionStack(); @@ -1240,7 +1240,7 @@ private DeclaredTypeAssignment GetTestType(TestDeclarationSyntax syntax) return null; } - private DeclaredTypeAssignment? GetPropertyAccessType(DeclaredTypeAssignment baseExpressionAssignment, PropertyAccessSyntax syntax) + private DeclaredTypeAssignment? GetPropertyAccessType(DeclaredTypeAssignment baseExpressionAssignment, PropertyAccessSyntax syntax) //asdfg { if (!syntax.PropertyName.IsValid) { @@ -1287,7 +1287,7 @@ private DeclaredTypeAssignment GetTestType(TestDeclarationSyntax syntax) }; } - private DeclaredTypeAssignment? GetResourceAccessType(ResourceAccessSyntax syntax) + private DeclaredTypeAssignment? GetResourceAccessType(ResourceAccessSyntax syntax)//asdfg { if (!syntax.ResourceName.IsValid) { @@ -1382,9 +1382,9 @@ private DeclaredTypeAssignment GetTestType(TestDeclarationSyntax syntax) return null; } - private DeclaredTypeAssignment? GetAccessExpressionType(AccessExpressionSyntax syntax) + private DeclaredTypeAssignment? GetAccessExpressionType(AccessExpressionSyntax syntax)//asdfg { - Stack chainedAccesses = syntax.ToAccessExpressionStack(); + Stack chainedAccesses = syntax.ToAccessExpressionStack(); //asdfg var baseAssignment = chainedAccesses.Peek() switch { AccessExpressionSyntax access when access.BaseExpression is ForSyntax @@ -1479,7 +1479,7 @@ AccessExpressionSyntax access when access.BaseExpression is ForSyntax return typeAssignment; } - private DeclaredTypeAssignment? GetStringType(StringSyntax syntax) + private DeclaredTypeAssignment? GetStringType(StringSyntax syntax) //asdfg { var parent = this.binder.GetParent(syntax); diff --git a/src/Bicep.Core/TypeSystem/ObjectTypeNameBuilder.cs b/src/Bicep.Core/TypeSystem/ObjectTypeNameBuilder.cs index c2bc5177e55..3b287bbf199 100644 --- a/src/Bicep.Core/TypeSystem/ObjectTypeNameBuilder.cs +++ b/src/Bicep.Core/TypeSystem/ObjectTypeNameBuilder.cs @@ -6,14 +6,14 @@ namespace Bicep.Core.TypeSystem; -internal class ObjectTypeNameBuilder +internal class ObjectTypeNameBuilder //asdfgasdfg { private readonly StringBuilder builder = new("{"); private bool hasProperties = false; private bool finalized = false; internal void AppendProperty(string propertyName, string propertyValue) - => DoAppendProperty(Lexer.IsValidIdentifier(propertyName) ? propertyName : StringUtils.EscapeBicepString(propertyName), propertyValue); + => DoAppendProperty(StringUtils.EscapeBicepPropertyName(propertyName), propertyValue); internal void AppendPropertyMatcher(string matchNotation, string value) => DoAppendProperty(matchNotation, value); diff --git a/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs b/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs index 96b2ac27814..f45b9e54be0 100644 --- a/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs +++ b/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs @@ -21,7 +21,7 @@ namespace Bicep.Core.TypeSystem { - public sealed class TypeAssignmentVisitor : AstVisitor + public sealed class TypeAssignmentVisitor : AstVisitor //asdfgasdfg { private readonly IFeatureProvider features; private readonly ITypeManager typeManager; @@ -652,7 +652,7 @@ public override void VisitUnionTypeSyntax(UnionTypeSyntax syntax) TypeSymbol otherwise => (otherwise, memberSyntax).AsEnumerable(), }; - private static TypeSymbol? GetNonLiteralType(TypeSymbol? type) => type switch + private static TypeSymbol? GetNonLiteralType(TypeSymbol? type) => type switch //asdfgasdfg { StringLiteralType => LanguageConstants.String, IntegerLiteralType => LanguageConstants.Int, diff --git a/src/Bicep.Core/TypeSystem/TypeHelper.cs b/src/Bicep.Core/TypeSystem/TypeHelper.cs index df06dd45307..d80ec2ad501 100644 --- a/src/Bicep.Core/TypeSystem/TypeHelper.cs +++ b/src/Bicep.Core/TypeSystem/TypeHelper.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; +using System.Reflection.Metadata; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; using Bicep.Core.Parsing; @@ -50,7 +51,7 @@ private static TypeProperty GetCombinedTypeProperty(IEnumerable obje { var flags = TypePropertyFlags.None; var types = new List(); - + foreach (var objectType in objectTypes) { if (objectType.Properties.TryGetValue(propertyName) is {} namedProperty) @@ -83,7 +84,7 @@ private static (TypeSymbol type, TypePropertyFlags flags)? TryGetCombinedAdditio var flags = TypePropertyFlags.None; var types = new List(); - + foreach (var objectType in objectTypes) { if (objectType.AdditionalPropertiesType is {} additionalPropertyType) @@ -101,6 +102,11 @@ private static (TypeSymbol type, TypePropertyFlags flags)? TryGetCombinedAdditio return (CreateTypeUnion(types), flags); } + /// + /// Tries to make a type nullable by unioning it with null + /// + public static TypeSymbol MakeNullable(ITypeReference typeReference) => CreateTypeUnion(typeReference, LanguageConstants.Null); //asdfg test + /// /// Converts a set of object types into a single object type with unioned properties /// e.g. { foo: 'abc', bar: 'def' } | { foo: 'ghi', baz: 'jkl' } @@ -150,7 +156,7 @@ public static IEnumerable GetOrderedTypeNames(IEnumerable CreateTypeUnion((IEnumerable)members); - public static bool IsLiteralType(TypeSymbol type) => type switch + public static bool IsLiteralType(TypeSymbol type) => type switch //asdfgasdfg { StringLiteralType or IntegerLiteralType or @@ -246,7 +252,7 @@ public static TypeSymbol GetNamedPropertyType(UnionType unionType, IPositionable } /// - /// Gets the type of the property whose name we can obtain at compile-time. + /// Gets the type of the property whose name we can obtain at compile-time. asdfg?? /// /// The base object type /// The position of the property name expression diff --git a/src/Bicep.Core/packages.lock.json b/src/Bicep.Core/packages.lock.json index 6d3f64bcce9..63afc59983e 100644 --- a/src/Bicep.Core/packages.lock.json +++ b/src/Bicep.Core/packages.lock.json @@ -2728,4 +2728,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Bicep.Decompiler/TemplateConverter.cs b/src/Bicep.Decompiler/TemplateConverter.cs index 24a56b02422..6abd44d51d1 100644 --- a/src/Bicep.Decompiler/TemplateConverter.cs +++ b/src/Bicep.Decompiler/TemplateConverter.cs @@ -944,12 +944,7 @@ private VariableDeclarationSyntax ParseVariable(string name, JToken value, bool variableValue = ParseJToken(value); } - return new VariableDeclarationSyntax( - ImmutableArray.Empty, - SyntaxFactory.VariableKeywordToken, - SyntaxFactory.CreateIdentifierWithTrailingSpace(identifier), - SyntaxFactory.AssignmentToken, - variableValue); + return SyntaxFactory.CreateVariableDeclaration(identifier, variableValue); //asdfg testpoint } private (SyntaxBase moduleFilePathStringLiteral, Uri? jsonTemplateUri) GetModuleFilePath(string templateLink) @@ -1520,7 +1515,7 @@ private SyntaxBase ParseResource(IReadOnlyDictionary copyResourc decoratorsAndNewLines, SyntaxFactory.ResourceKeywordToken, SyntaxFactory.CreateIdentifierWithTrailingSpace(identifier), - SyntaxFactory.CreateStringLiteral($"{typeString}@{apiVersionString}"), + SyntaxFactory.CreateStringLiteral($"{typeString}@{apiVersionString}"),//asdfg null, SyntaxFactory.AssignmentToken, [], diff --git a/src/Bicep.Decompiler/UniqueNamingResolver.cs b/src/Bicep.Decompiler/UniqueNamingResolver.cs index 9d685cc615f..788bcb26415 100644 --- a/src/Bicep.Decompiler/UniqueNamingResolver.cs +++ b/src/Bicep.Decompiler/UniqueNamingResolver.cs @@ -10,7 +10,7 @@ namespace Bicep.Decompiler { - public class UniqueNamingResolver : INamingResolver + public class UniqueNamingResolver : INamingResolver //asdfgasdfg { private readonly Dictionary> assignedNames = new(StringComparer.OrdinalIgnoreCase); diff --git a/src/Bicep.LangServer.IntegrationTests/CodeActionTestBase.cs b/src/Bicep.LangServer.IntegrationTests/CodeActionTestBase.cs new file mode 100644 index 00000000000..afc0344da60 --- /dev/null +++ b/src/Bicep.LangServer.IntegrationTests/CodeActionTestBase.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Bicep.Core.CodeAction; +using Bicep.Core.Diagnostics; +using Bicep.Core.Extensions; +using Bicep.Core.Parsing; +using Bicep.Core.Samples; +using Bicep.Core.Syntax; +using Bicep.Core.Text; +using Bicep.Core.UnitTests; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.PrettyPrintV2; +using Bicep.Core.UnitTests.Serialization; +using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Workspaces; +using Bicep.LangServer.IntegrationTests.Helpers; +using Bicep.LanguageServer.Extensions; +using Bicep.LanguageServer.Utils; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; + +namespace Bicep.LangServer.IntegrationTests +{ + [TestClass] + public partial class CodeActionTestBase + { + protected static ServiceBuilder Services => new(); + + protected static readonly SharedLanguageHelperManager DefaultServer = new(); + + protected static readonly SharedLanguageHelperManager ServerWithFileResolver = new(); + + protected static readonly SharedLanguageHelperManager ServerWithBuiltInTypes = new(); + + protected static readonly SharedLanguageHelperManager ServerWithNamespaceProvider = new(); + + [NotNull] + public TestContext? TestContext { get; set; } + + [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)] + public static void ClassInitialize(TestContext testContext) + { + DefaultServer.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext)); + + ServerWithFileResolver.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext)); + + ServerWithBuiltInTypes.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext, services => services.WithNamespaceProvider(BuiltInTestTypes.Create()))); + + ServerWithNamespaceProvider.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext, services => services.WithNamespaceProvider(BicepTestConstants.NamespaceProvider))); + } + + [ClassCleanup(InheritanceBehavior.BeforeEachDerivedClass)] + public static async Task ClassCleanup() + { + await DefaultServer.DisposeAsync(); + await ServerWithFileResolver.DisposeAsync(); + await ServerWithBuiltInTypes.DisposeAsync(); + await ServerWithNamespaceProvider.DisposeAsync(); + } + + protected async Task<(IEnumerable codeActions, BicepFile bicepFile)> GetCodeActionsForSyntaxTest(string fileWithCursors, char emptyCursor = '|', string escapedCursor = "||", MultiFileLanguageServerHelper? server = null) + { + Trace.WriteLine("Input bicep:\n" + fileWithCursors + "\n"); + + var (file, selection) = ParserHelper.GetFileWithSingleSelection(fileWithCursors, emptyCursor.ToString(), escapedCursor); + var bicepFile = SourceFileFactory.CreateBicepFile(new Uri($"file://{TestContext.TestName}_{Guid.NewGuid():D}/main.bicep"), file); + + server ??= await DefaultServer.GetAsync(); + await server.OpenFileOnceAsync(TestContext, file, bicepFile.FileUri); + + var codeActions = await RequestCodeActions(server.Client, bicepFile, selection); + return (codeActions, bicepFile); + } + + protected static IEnumerable GetOverlappingSpans(TextSpan span) + { + // NOTE: These code assumes there are no errors in the code that are exactly adject to each other or that overlap + + // Same span. + yield return span; + + // Adjacent spans before. + int startOffset = Math.Max(0, span.Position - 1); + yield return new TextSpan(startOffset, 1); + yield return new TextSpan(span.Position, 0); + + // Adjacent spans after. + yield return new TextSpan(span.GetEndPosition(), 1); + yield return new TextSpan(span.GetEndPosition(), 0); + + // Overlapping spans. + yield return new TextSpan(startOffset, 2); + yield return new TextSpan(span.Position + 1, span.Length); + yield return new TextSpan(startOffset, span.Length + 1); + } + + protected static async Task> RequestCodeActions(ILanguageClient client, BicepFile bicepFile, TextSpan span) //asdfg extract + { + var startPosition = TextCoordinateConverter.GetPosition(bicepFile.LineStarts, span.Position); + var endPosition = TextCoordinateConverter.GetPosition(bicepFile.LineStarts, span.Position + span.Length); + endPosition.Should().BeGreaterThanOrEqualTo(startPosition); + + var result = await client.RequestCodeAction(new CodeActionParams + { + TextDocument = new TextDocumentIdentifier(bicepFile.FileUri), + Range = new Range(startPosition, endPosition), + }); + + return result!.Select(x => x.CodeAction).WhereNotNull(); + } + + protected static BicepFile ApplyCodeAction(BicepFile bicepFile, CodeAction codeAction) //asdfg extract + { + // only support a small subset of possible edits for now - can always expand this later on + codeAction.Edit!.Changes.Should().NotBeNull(); + codeAction.Edit.Changes.Should().HaveCount(1); + codeAction.Edit.Changes.Should().ContainKey(bicepFile.FileUri); + + var bicepText = bicepFile.ProgramSyntax.ToString(); + var changes = codeAction.Edit.Changes![bicepFile.FileUri].ToArray(); + + for (int i = 0; i < changes.Length; ++i) + { + for (int j = i + 1; j < changes.Length; ++j) + { + Range.AreIntersecting(changes[i].Range, changes[j].Range).Should().BeFalse("Edits must be non-overlapping (https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit)"); + } + } + + // Convert to Bicep coordinates + var lineStarts = TextCoordinateConverter.GetLineStarts(bicepText); + var convertedChanges = changes.Select(c => + (NewText: c.NewText, Span: c.Range.ToTextSpan(lineStarts))) + .ToArray(); + + for (var i = 0; i < changes.Length; ++i) + { + var replacement = convertedChanges[i]; + + var start = replacement.Span.Position; + var end = replacement.Span.Position + replacement.Span.Length; + var textToInsert = replacement.NewText; + + // the handler can contain tabs. convert to double space to simplify printing. + textToInsert = textToInsert.Replace("\t", " "); + + bicepText = bicepText.Substring(0, start) + textToInsert + bicepText.Substring(end); + + // Adjust indices for the remaining changes to account for this replacement + int replacementOffset = textToInsert.Length - (end - start); + for (int j = i + 1; j < changes.Length; ++j) + { + if (convertedChanges[j].Span.Position >= replacement.Span.Position) + { + convertedChanges[j].Span = convertedChanges[j].Span.MoveBy(replacementOffset); + } + } + } + + var command = codeAction.Command; + command.Should().NotBeNull(); + command!.Name.Should().Be("bicep.internal.startRename"); + command.Arguments.Should().NotBeNull(); + command.Arguments!.Should().BeOfType(); + var argsArray = ((JArray)command.Arguments!); + var args = (argsArray[0].ToString(), argsArray[1]); + args.Item1.Should().StartWith("file://"); + var positionObject = (JObject)args.Item2; + var (line, character) = (positionObject.GetValue("line")!.Value(), positionObject.GetValue("character")!.Value()); + var modifiedLineStarts = TextCoordinateConverter.GetLineStarts(bicepText); + var renameOffset = TextCoordinateConverter.GetOffset(modifiedLineStarts, line, character); + var possibleVarKeyword = renameOffset >= "var ".Length ? bicepText.Substring(renameOffset - "var ".Length, "var ".Length) : null; + var possibleParamKeyword = renameOffset >= "param ".Length ? bicepText.Substring(renameOffset - "param ".Length, "param ".Length) : null; + (possibleVarKeyword == "var " || possibleParamKeyword == "param ").Should().BeTrue( + "Rename should be positioned on the new identifier right after 'var ' or 'param '"); + + return SourceFileFactory.CreateBicepFile(bicepFile.FileUri, bicepText); + } + } +} diff --git a/src/Bicep.LangServer.IntegrationTests/CodeActionTests.cs b/src/Bicep.LangServer.IntegrationTests/CodeActionTests.cs index d77ede97a71..98331122c57 100644 --- a/src/Bicep.LangServer.IntegrationTests/CodeActionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CodeActionTests.cs @@ -1,14 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Bicep.Core.CodeAction; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; using Bicep.Core.Parsing; using Bicep.Core.Samples; +using Bicep.Core.Syntax; using Bicep.Core.Text; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.PrettyPrintV2; +using Bicep.Core.UnitTests.Serialization; using Bicep.Core.UnitTests.Utils; using Bicep.Core.Workspaces; using Bicep.LangServer.IntegrationTests.Helpers; @@ -26,10 +30,8 @@ namespace Bicep.LangServer.IntegrationTests { [TestClass] - public class CodeActionTests + public partial class CodeActionTests : CodeActionTestBase { - private static ServiceBuilder Services => new(); - private const string SecureTitle = "Add @secure"; private const string DescriptionTitle = "Add @description"; private const string AllowedTitle = "Add @allowed"; @@ -41,38 +43,6 @@ public class CodeActionTests private const string RemoveUnusedParameterTitle = "Remove unused parameter"; private const string RemoveUnusedVariableTitle = "Remove unused variable"; - private static readonly SharedLanguageHelperManager DefaultServer = new(); - - private static readonly SharedLanguageHelperManager ServerWithFileResolver = new(); - - private static readonly SharedLanguageHelperManager ServerWithBuiltInTypes = new(); - - private static readonly SharedLanguageHelperManager ServerWithNamespaceProvider = new(); - - [NotNull] - public TestContext? TestContext { get; set; } - - [ClassInitialize] - public static void ClassInitialize(TestContext testContext) - { - DefaultServer.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext)); - - ServerWithFileResolver.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext)); - - ServerWithBuiltInTypes.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext, services => services.WithNamespaceProvider(BuiltInTestTypes.Create()))); - - ServerWithNamespaceProvider.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(testContext, services => services.WithNamespaceProvider(BicepTestConstants.NamespaceProvider))); - } - - [ClassCleanup] - public static async Task ClassCleanup() - { - await DefaultServer.DisposeAsync(); - await ServerWithFileResolver.DisposeAsync(); - await ServerWithBuiltInTypes.DisposeAsync(); - await ServerWithNamespaceProvider.DisposeAsync(); - } - [DataTestMethod] [DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] public async Task RequestingCodeActionWithFixableDiagnosticsShouldProduceQuickFixes(DataSet dataSet) @@ -482,7 +452,7 @@ public async Task Parameter_decorators_are_not_suggested_for_unsupported_type(st [DataTestMethod] public async Task Unused_existing_resource_actions_are_suggested(string fileWithCursors, string expectedText) { - (var codeActions, var bicepFile) = await RunSyntaxTest(fileWithCursors, '|'); + (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(fileWithCursors, '|'); codeActions.Should().Contain(x => x.Title.StartsWith(RemoveUnusedExistingResourceTitle)); codeActions.First(x => x.Title.StartsWith(RemoveUnusedExistingResourceTitle)).Kind.Should().Be(CodeActionKind.QuickFix); @@ -517,7 +487,7 @@ public async Task Unused_existing_resource_actions_are_suggested(string fileWith [DataTestMethod] public async Task Unused_variable_actions_are_suggested(string fileWithCursors, string expectedText) { - (var codeActions, var bicepFile) = await RunSyntaxTest(fileWithCursors, '|'); + (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(fileWithCursors, '|'); codeActions.Should().Contain(x => x.Title.StartsWith(RemoveUnusedVariableTitle)); codeActions.First(x => x.Title.StartsWith(RemoveUnusedVariableTitle)).Kind.Should().Be(CodeActionKind.QuickFix); @@ -536,7 +506,7 @@ param fo|o string [DataTestMethod] public async Task Unused_parameter_actions_are_suggested(string fileWithCursors, string expectedText) { - (var codeActions, var bicepFile) = await RunSyntaxTest(fileWithCursors, '|'); + (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(fileWithCursors, '|'); codeActions.Should().Contain(x => x.Title.StartsWith(RemoveUnusedParameterTitle)); codeActions.First(x => x.Title.StartsWith(RemoveUnusedParameterTitle)).Kind.Should().Be(CodeActionKind.QuickFix); @@ -551,7 +521,7 @@ public async Task Provider_codefix_works() TestContext, services => services.WithFeatureOverrides(new(TestContext, ExtensibilityEnabled: true))); - (var codeActions, var bicepFile) = await RunSyntaxTest(@" + (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(@" imp|ort 'br:example.azurecr.io/test/radius:1.0.0' ", server: server); @@ -560,7 +530,7 @@ public async Task Provider_codefix_works() extension 'br:example.azurecr.io/test/radius:1.0.0' "); - (codeActions, bicepFile) = await RunSyntaxTest(@" + (codeActions, bicepFile) = await GetCodeActionsForSyntaxTest(@" pro|vider 'br:example.azurecr.io/test/radius:1.0.0' ", server: server); @@ -575,7 +545,7 @@ public async Task Provider_codefix_works() [DataTestMethod] public async Task Unused_variable_actions_are_not_suggested_for_invalid_variables(string fileWithCursors) { - var (codeActions, _) = await RunSyntaxTest(fileWithCursors, '|'); + var (codeActions, _) = await GetCodeActionsForSyntaxTest(fileWithCursors, '|'); codeActions.Should().NotContain(x => x.Title.StartsWith(RemoveUnusedVariableTitle)); } @@ -584,7 +554,7 @@ public async Task Unused_variable_actions_are_not_suggested_for_invalid_variable [DataTestMethod] public async Task Unused_parameter_actions_are_not_suggested_for_invalid_parameters(string fileWithCursors) { - var (codeActions, _) = await RunSyntaxTest(fileWithCursors, '|'); + var (codeActions, _) = await GetCodeActionsForSyntaxTest(fileWithCursors, '|'); codeActions.Should().NotContain(x => x.Title.StartsWith(RemoveUnusedParameterTitle)); } @@ -600,41 +570,9 @@ param fo|o {paramType} param fo|o {paramType} "; } - return await RunSyntaxTest(fileWithCursors, '|'); - } - - private async Task<(IEnumerable codeActions, BicepFile bicepFile)> RunSyntaxTest(string fileWithCursors, char cursor = '|', MultiFileLanguageServerHelper? server = null) - { - var (file, cursors) = ParserHelper.GetFileWithCursors(fileWithCursors, cursor); - var bicepFile = SourceFileFactory.CreateBicepFile(new Uri($"file://{TestContext.TestName}_{Guid.NewGuid():D}/main.bicep"), file); - server ??= await DefaultServer.GetAsync(); - await server.OpenFileOnceAsync(TestContext, file, bicepFile.FileUri); - - var codeActions = await RequestCodeActions(server.Client, bicepFile, cursors.Single()); - return (codeActions, bicepFile); - } - - private static IEnumerable GetOverlappingSpans(TextSpan span) - { - // NOTE: These code assumes there are no errors in the code that are exactly adject to each other or that overlap - - // Same span. - yield return span; - - // Adjacent spans before. - int startOffset = Math.Max(0, span.Position - 1); - yield return new TextSpan(startOffset, 1); - yield return new TextSpan(span.Position, 0); - - // Adjacent spans after. - yield return new TextSpan(span.GetEndPosition(), 1); - yield return new TextSpan(span.GetEndPosition(), 0); - - // Overlapping spans. - yield return new TextSpan(startOffset, 2); - yield return new TextSpan(span.Position + 1, span.Length); - yield return new TextSpan(startOffset, span.Length + 1); + fileWithCursors.Should().NotBeNull("should contain an extract to variable action"); + return await GetCodeActionsForSyntaxTest(fileWithCursors, '|'); } private static IEnumerable GetData() @@ -642,52 +580,6 @@ private static IEnumerable GetData() return DataSets.NonStressDataSets.ToDynamicTestData(); } - private static async Task> RequestCodeActions(ILanguageClient client, BicepFile bicepFile, int cursor) - { - var startPosition = TextCoordinateConverter.GetPosition(bicepFile.LineStarts, cursor); - var endPosition = TextCoordinateConverter.GetPosition(bicepFile.LineStarts, cursor); - - var result = await client.RequestCodeAction(new CodeActionParams - { - TextDocument = new TextDocumentIdentifier(bicepFile.FileUri), - Range = new Range(startPosition, endPosition), - }); - - return result!.Select(x => x.CodeAction).WhereNotNull(); - } - - private static BicepFile ApplyCodeAction(BicepFile bicepFile, CodeAction codeAction, params string[] tabStops) - { - // only support a small subset of possible edits for now - can always expand this later on - codeAction.Edit!.Changes.Should().NotBeNull(); - codeAction.Edit.Changes.Should().HaveCount(1); - codeAction.Edit.Changes.Should().ContainKey(bicepFile.FileUri); - - var changes = codeAction.Edit.Changes![bicepFile.FileUri]; - changes.Should().HaveCount(1); - - var replacement = changes.Single(); - - var start = PositionHelper.GetOffset(bicepFile.LineStarts, replacement.Range.Start); - var end = PositionHelper.GetOffset(bicepFile.LineStarts, replacement.Range.End); - var textToInsert = replacement.NewText; - - // the handler can contain tabs. convert to double space to simplify printing. - textToInsert = textToInsert.Replace("\t", " "); - - var originalFile = bicepFile.ProgramSyntax.ToString(); - var replaced = originalFile.Substring(0, start) + textToInsert + originalFile.Substring(end); - - return SourceFileFactory.CreateBicepFile(bicepFile.FileUri, replaced); - } - - private static CodeAction GetSingleCodeAction(IEnumerable codeActions, string codeActionName) - { - codeActions.Should().ContainSingle(x => x.Title == codeActionName); - - return codeActions.Single(x => x.Title == codeActionName); - } - private static async Task FormatDocument(ILanguageClient client, BicepFile bicepFile) { var textEditContainer = await client.TextDocument.RequestDocumentFormatting(new DocumentFormattingParams diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index a82b3cb9eec..af08b753b1f 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -191,8 +191,8 @@ public async Task ValidateSnippetCompletionAfterPlaceholderReplacements(Completi sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput( TestContext, File.Exists(combinedFileName) ? (await File.ReadAllTextAsync(combinedFileName)) : string.Empty, - expectedLocation: combinedSourceFileName, - actualLocation: combinedFileName + ".actual"); + expectedPath: combinedSourceFileName, + actualPath: combinedFileName + ".actual"); } } diff --git a/src/Bicep.LangServer.IntegrationTests/ExtractVarAndParamTests.cs b/src/Bicep.LangServer.IntegrationTests/ExtractVarAndParamTests.cs new file mode 100644 index 00000000000..10faecadd6c --- /dev/null +++ b/src/Bicep.LangServer.IntegrationTests/ExtractVarAndParamTests.cs @@ -0,0 +1,2363 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Bicep.Core.CodeAction; +using Bicep.Core.Diagnostics; +using Bicep.Core.Extensions; +using Bicep.Core.Parsing; +using Bicep.Core.Samples; +using Bicep.Core.Syntax; +using Bicep.Core.Text; +using Bicep.Core.UnitTests; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.PrettyPrintV2; +using Bicep.Core.UnitTests.Serialization; +using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Workspaces; +using Bicep.LangServer.IntegrationTests.Helpers; +using Bicep.LanguageServer.Extensions; +using Bicep.LanguageServer.Utils; +using FluentAssertions; +using FluentAssertions.Execution; +using Humanizer; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using static Google.Protobuf.Reflection.SourceCodeInfo.Types; +using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; + + +//asdfg test rename position + + +/* asdfg + +type myMixedTypeArrayType = ('fizz' | 42 | {an: 'object'} | null)[] + + +asdfg handle inside a module + + + +type negativeIntLiteral = -10 +type negatedIntReference = -negativeIntLiteral +type negatedBoolLiteral = !true +type negatedBoolReference = !negatedBoolLiteral +type t = { + a: negativeIntLiteral + b: negatedIntReference + c: negatedBoolLiteral + d: negatedBoolReference +} +param p t = { + a: -10 + b: 10 + c: false + d: true +} + + +extract sku: - end up with 'string'|string, and also required 'tier' +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = { + name: storageAccountConfig.name + location: location + sku: { + name: storageAccountConfig.sku + } + kind: 'StorageV2' +} + + +https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/data-types#custom-tagged-union-data-type + + + + + +type anObject = { + property: string + optionalProperty: string? +} + +param aParameter anObject = { + property: 'value' + otionalProperty: 'value' +} + + + +module type appears (dependsOn) + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = <<{ + name: storageAccountName + location: location + sku: { + name: storageAccountSKU + } + kind: 'StorageV2' +}>> + + + +type invalidRecursiveObjectType = { + level1: { + level2: { + level3: { + level4: { + level5: invalidRecursiveObjectType + } + } + } + } +} +param p invalidRecursiveObjectType = { + level1: { + level2: { + level3: { + level4: { + level5: null + } + } + } + } + } + + + +type obj = { + @description('The object ID') + id: int + + @description('Additional properties') + @minLength(10) + *: string +} + + + +var blah1 = [<<{ foo: 'bar' }>>, { foo: 'baz' }] +why isn't this extractding just the object? + + +fileUris should be string[] not [string] + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + + +// ======================= ISSUE CRASHES +// type TFoo = { +// property: TFoo? +// } +// param pfoo TFoo +// var fv = pfoo + + + + type foo = { + property: foo? +} + +bad: +param <> int = 2 + + +type recursive1 = [string, recursive1?] +param p1 recursive1 = ['a', ['b', ['c', ['d', null]]]] +var a1 = p1 + + + */ + +//asdfg move new parameter/var to top +//asdfg rename +// asdfg multi-line formatting + + + +namespace Bicep.LangServer.IntegrationTests; + +[TestClass] +public class ExtractVarAndParamTests : CodeActionTestBase +{ + private const string ExtractToVariableTitle = "Extract variable"; + private const string ExtractToParameterTitle = "Extract parameter"; + + //asdfg param p2 'foo' | 'bar' | string = 'bar' + // asdfg nullable + + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow( + """ + type superComplexType = { + p: string + i: 123 || 456 + } + + param p { *: superComplexType } = { + a: <<{ p: 'mystring', i: 123 }>> + } + """, + """ + param a { i: int, p: string } = { p: 'mystring', i: 123 } // asdfg would prefer param superComplexType = { p: 'mystring', i: 123 } + param p { *: superComplexType } = { + a: a + } + """)] + + //asdfg BUG: + /* + param p1 { intVal: int } + param p2 object = p1 + var v1 = p2 + => + param newParameter { } = p2 +var v1 = newParameter + + */ + + [DataRow( + """ + var blah = |[{foo: 'bar'}, {foo: 'baz'}] + """, + """ + asdfg + """)] + + + + + //asdfg TODO: + // what should behavior be? + [DataRow( + """ + param p1 { intVal: int} = { intVal:123} + output o object = <> + """, + """ + param p1 { intVal: int} = { intVal:123} + param newParameter { intVal: int } = p1 + output o object = newParameter + """)] + // param p2 {a: string} + // param v1 object = p2 + // CURRENTLY IT'S: (seems reasonable?) + /* + param p2 {a: string} + param newParameter { a: string } = p2 + param v1 object = newParameter + */ + + + //asdfg TODO + // param p2 'foo' | 'bar' + // param v1 string = p2 + // What should type of new parameter be? Currently it's unknown + + + //Extracted value is in a var statement and has no declared type: the type will be based on the value. You might get recursive types or unions if the value contains a reference to a parameter, but you can pull the type clause from the parameter declaration. + //Extracted value is in a param statement (or something else with an explicit type declaration): you may be able to use the declared type syntax of the enclosing statement rather than working from the type backwards to a declaration. + //Extracted value is in a resource body: definite possibility of complex structures, recursion, and a few type constructs that aren't fully expressible in Bicep syntax (e.g., "open" enums like 'foo' | 'bar' | string). Resource-derived types might be a good solution here, but they're still behind a feature flag + + // Extracted value is in a var statement and has no declared type: the type will be based on the value. + // You might get recursive types or unions if the value contains a reference to a parameter, but you can + // pull the type clause from the parameter declaration. + [DataRow( + """ + var foo = <<{ intVal: 2 }>> + """, + """ + param newParameter { intVal: int } = { intVal: 2 } + var foo = newParameter + """)] + + //Extracted value is in a param statement (or something else with an explicit type declaration) + // you may be able to use the declared type syntax of the enclosing statement rather than working + // from the type backwards to a declaration. + [DataRow( + """ + param p1 { intVal: int} + output o = <> + """, + """ + param p1 { intVal: int} + param newParameter { intVal: int } = p1 + output o = newParameter + """)] + + [DataRow( + """ + var isWindowsOS = true + var provisionExtensions = true + param _artifactsLocation string + @secure() + param _artifactsLocationSasToken string + + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'cse-windows/extension' + location: 'location' + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + setting|s: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + } + } + """, +//asdfg we don't have strongly typed array? fileUris: [string]? + """ + var isWindowsOS = true + var provisionExtensions = true + param _artifactsLocation string + @secure() + param _artifactsLocationSasToken string + + param settings { commandToExecute: string, fileUris: array } = { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'cse-windows/extension' + location: 'location' + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: settings + } + } + """)] + [DataRow( + """ + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = { + name: 'cse/windows' + location: 'location' + |properties: { + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + } + } + """, + """ + asdfg TODO: getting some unknowns and readonly types + param properties { autoUpgradeMinorVersion: bool, forceUpdateTag: string, instanceView: { name: string, statuses: array, substatuses: array, type: string, typeHandlerVersion: string }, protectedSettings: unknown, publisher: string, settings: unknown, type: string, typeHandlerVersion: string } = { + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + } + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = { + name: 'cse/windows' + location: 'location' + properties: properties + } + """)] + [DataRow( + """ + param p2 'foo' || 'bar' + var v1 = <> + """, + """ + param p2 'foo' | 'bar' + param newParameter 'bar' | 'foo' = p2 + var v1 = newParameter + """)] + [DataRow( + // rhs is more strictly typed than lhs + // medium picks up strict type, loose just object + // asdfg why isn't it picking up declared type of object?? + """ + param p1 { intVal: int} = { intVal:123} + output o object = <> + """, + """ + param p1 { intVal: int} = { intVal:123} + param newParameter { intVal: int } = p1 + output o object = newParameter + """)] + [DataRow( + // TODO: generates incorrect code + """ + param p { a: { 'a b': string } } + var v = p + """, + """ + param p { a: { 'a b': string } } + param newParameter { a: { 'a b': string } } = p + var v = newParameter + """)] + // recursive types + [DataRow( + """ + type foo = { + property: foo? + } + param pfoo foo + var v = <> + """, + """ + // Currently gives asdfg + param pfoo foo + param newParameter { property: unknown } = pfoo + var v = newParameter + """)] + // named types + [DataRow( + """ + type foo = { + property: string + } + type foo2 = { + property: foo + } + param pfoo2 foo2 + var v = pfoo2 + """, + """ + // Currently gives asdfg + type foo = { + property: string + } + type foo2 = { + property: foo + } + param pfoo2 foo2 + param newParameter { property: { property: string } } = pfoo2 + // EXPECTED: + param newParameter { property: foo } = pfoo2 + var v = newParameter + """)] + + [DataRow( + """ + param p1 {a: string || int} + var v1 = <> + """, + """ + param p1 {a: string | int} + param newParameter object = p1 + var v1 = newParameter + """, + """ + param p1 {a: string | int} + param newParameter { a: int | string } = p1 + var v1 = newParameter + """)] + public async Task BicepDiscussion(string fileWithSelection, string expectedLooseParamText, string expectedMediumParamText) + { + await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow( + """ + var a = '|b' + """, + """ + var newVariable = 'b' + var a = newVariable + """, + """ + param newParameter string = 'b' + var a = newParameter + """)] + [DataRow( + """ + var a = 'a' + var b = '|b' + var c = 'c' + """, + """ + var a = 'a' + var newVariable = 'b' + var b = newVariable + var c = 'c' + """, + """ + var a = 'a' + param newParameter string = 'b' + var b = newParameter + var c = 'c' + """)] + [DataRow( + """ + var a = 1 + |2 + """, + """ + var newVariable = 2 + var a = 1 + newVariable + """, + """ + param newParameter int = 2 + var a = 1 + newParameter + """)] + [DataRow( + """ + var a = <<1 + 2>> + """, + """ + var newVariable = 1 + 2 + var a = newVariable + """, + """ + param newParameter int = 1 + 2 + var a = newParameter + """)] + [DataRow( + """ + var a = <<1 +>> 2 + """, + """ + var newVariable = 1 + 2 + var a = newVariable + """, + "IGNORE")] + [DataRow( + """ + var a = 1 |+ 2 + """, + """ + var newVariable = 1 + 2 + var a = newVariable + """, + "IGNORE")] + [DataRow( + """ + var a = 1 <<+ 2 + 3 >>+ 4 + """, + """ + var newVariable = 1 + 2 + 3 + 4 + var a = newVariable + """, + "IGNORE")] + [DataRow( + """ + param p1 int = 1 + |2 + """, + """ + var newVariable = 2 + param p1 int = 1 + newVariable + """, + "IGNORE")] + [DataRow( + """ + var a = 1 + 2 + var b = '${a}|{a}' + """, + """ + var a = 1 + 2 + var newVariable = '${a}{a}' + var b = newVariable + """, + """ + var a = 1 + 2 + var newParameter string = '${a}{a}' + var b = newParameter + """, + DisplayName = "Full interpolated string")] + [DataRow( + """ + // comment 1 + @secure + // comment 2 + param a = '|a' + """, + """ + // comment 1 + var newVariable = 'a' + @secure + // comment 2 + param a = newVariable + """, + """ + // comment 1 + var newParameter string = 'a' + @secure + // comment 2 + param a = newParameter + """, + DisplayName = "Preceding lines")] + [DataRow( + """ + var a = 1 + var b = [ + 'a' + 1 + <<2>> + 'c' + ] + """, + """ + var a = 1 + var newVariable = 2 + var b = [ + 'a' + 1 + newVariable + 'c' + ] + """, + """ + var a = 1 + param newParameter int = 2 + var b = [ + 'a' + 1 + newParameter + 'c' + ] + """, + DisplayName = "Inside a data structure")] + [DataRow( + """ + // My comment here + resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: |'westus' + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + } + """, + """ + // My comment here + var location = 'westus' + resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + } + """, + """ + // My comment here + @description('Required. Gets or sets the location of the resource. This will be one of the supported and registered Azure Geo Regions (e.g. West US, East US, Southeast Asia, etc.). The geo region of a resource cannot be changed once it is created, but if an identical geo region is specified on update, the request will succeed.') + param location string = 'westus' + resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { + name: 'name' + location: location + kind: 'StorageV2' + sku: { + name: 'Premium_LRS' + } + } + """)] + public async Task Basics(string fileWithSelection, string? expectedVarText, string? expectedLooseParamText = null, string? expectedMediumParamText = null) + { + await RunExtractToVariableAndParameterTest(fileWithSelection, expectedVarText, expectedLooseParamText, expectedMediumParamText); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow( + """ + var a = '|b' + """, + """ + param newParameter string = 'b' + var a = newParameter + """, + null // no second option + )] + [DataRow( + """ + var a = |{a: 'b'} + """, + """ + param newParameter object = { a: 'b' } + var a = newParameter + """, + """ + param newParameter { a: string } = { a: 'b' } + var a = newParameter + """)] + public async Task ShouldOfferTwoParameterExtractions_IffTheExtractedTypesAreDifferent(string fileWithSelection, string? expectedLooseParamText, string? expectedMediumParamText) + { + await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow( + """ + var newVariable = 'newVariable' + param newVariable2 string = '|newVariable2' + """, + """ + var newVariable = 'newVariable' + var newVariable3 = 'newVariable2' + param newVariable2 string = newVariable3 + """, + DisplayName = "Simple naming conflict") + ] + [DataRow( + """ + var id = [1, 2, 3] + param id2 string = 'hello' + resource id6 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [ + for (id3, id4) in id: { + name: 'subnet${id3}' + properties: { + addressPrefix: '10.0.${id4}.0/24' + natGateway: { + id: '|gatewayId' + } + } + } + ] + output id5 string = id2 + """, + """ + var id = [1, 2, 3] + var id7 = 'gatewayId' + param id2 string = 'hello' + resource id6 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [ + for (id3, id4) in id: { + name: 'subnet${id3}' + properties: { + addressPrefix: '10.0.${id4}.0/24' + natGateway: { + id: id7 + } + } + } + ] + output id5 string = id2 + """, + DisplayName = "Complex naming conflicts")] + public async Task ShouldRenameToAvoidConflicts(string fileWithSelection, string expectedText) + { + await RunExtractToVariableTest(fileWithSelection, expectedText); + } + + //////////////////////////////////////////////////////////////////// + + [TestMethod] + public async Task ShouldHandleArrays() + { + await RunExtractToVariableAndParameterTest( + """ + resource subnets 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [ + for (item, index) in <<[1, 2, 3]>>: { + name: 'subnet${index}' + properties: { + addressPrefix: '10.0.${index}.0/24' + } + } + ] + """, + """ + var newVariable = [1, 2, 3] + resource subnets 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [ + for (item, index) in newVariable: { + name: 'subnet${index}' + properties: { + addressPrefix: '10.0.${index}.0/24' + } + } + ] + """, + """ + param newParameter array = [1, 2, 3] + resource subnets 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [ + for (item, index) in newParameter: { + name: 'subnet${index}' + properties: { + addressPrefix: '10.0.${index}.0/24' + } + } + ] + """, + """ + param newParameter int[] = [1, 2, 3] + resource subnets 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [ + for (item, index) in newParameter: { + name: 'subnet${index}' + properties: { + addressPrefix: '10.0.${index}.0/24' + } + } + ] + """); + } + + //////////////////////////////////////////////////////////////////// + + [TestMethod] + public async Task ShouldHandleObjects() + { + await RunExtractToVariableAndParameterTest(""" + param _artifactsLocation string + param _artifactsLocationSasToken string + + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + parent: vmName_resource + name: 'cse-windows' + location: location + properties: <<{ + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: commandToExecute + } + }>> + } + """, + """ + param _artifactsLocation string + param _artifactsLocationSasToken string + + var properties = { + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: commandToExecute + } + } + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + parent: vmName_resource + name: 'cse-windows' + location: location + properties: properties + } + """, + """ + param _artifactsLocation string + param _artifactsLocationSasToken string + @description('Describes the properties of a Virtual Machine Extension.') + param properties object = { + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: commandToExecute + } + } + + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + parent: vmName_resource + name: 'cse-windows' + location: location + properties: properties + } + """, + """ + param _artifactsLocation string + param _artifactsLocationSasToken string + @description('Describes the properties of a Virtual Machine Extension.') + param properties { autoUpgradeMinorVersion: bool?, forceUpdateTag: string?, instanceView: { name: string?, statuses: { code: string?, displayStatus: string?, level: ('Error' | 'Info' | 'Warning')?, message: string?, time: string? }[]?, substatuses: { code: string?, displayStatus: string?, level: ('Error' | 'Info' | 'Warning')?, message: string?, time: string? }[]?, type: string?, typeHandlerVersion: string? }?, protectedSettings: object? /* any */, publisher: string?, settings: object? /* any */, type: string?, typeHandlerVersion: string? } = { + // Entire properties object selected + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: commandToExecute + } + } + + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + parent: vmName_resource + name: 'cse-windows' + location: location + properties: properties + } + """); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + ////asdfg + //[DataRow(""" + // var i = <<1>> + // """, + // """ + // param newParameter int = 1 + // var i = newParameter + // """, + // DisplayName = "Literal integer")] + //[DataRow(""" + // param i int = 1 + // var j = <> + 1 + // """, + // """ + // param i int = 1 + // param newParameter int = i + // var j = newParameter + 1 + // """, + // DisplayName = "int parameter reference")] + //[DataRow(""" + // param i int = 1 + // var j = <> + // """, + // """ + // param i int = 1 + // param newParameter int = i + 1 + // var j = newParameter + // """, + // DisplayName = "int expression with param")] + //[DataRow(""" + // param i string + // var j = <> + // """, + // """ + // param i string + // param newParameter string = concat(i, i) + // var j = newParameter + // """, + // DisplayName = "strings concatenated")] + //[DataRow(""" + // param i string = 'i' + // var i2 = 'i2' + // var j = <<'{i}{i2}'>> + // """, + // """ + // param i string = 'i' + // var i2 = 'i2' + // param newParameter string = '{i}{i2}' + // var j = newParameter + // """, + // DisplayName = "strings concatenated")] + //[DataRow(""" + // var p = <<[ 1, 2, 3 ]>> + // """, + // """ + // param newParameter array = [1, 2, 3] + // var p = newParameter + // """, + // DisplayName = "array literal")] + //[DataRow(""" + // var p = <<{ a: 1, b: 'b' }>> + // """, + // """ + // param newParameter { a: int, b: string } = { a: 1, b: 'b' } + // var p = newParameter + // """, + // DisplayName = "object literal with literal types")] + //[DataRow(""" + // var p = { a: <<1>>, b: 'b' } + // """, + // """ + // param a int = 1 + // var p = { a: a, b: 'b' } + // """, + // DisplayName = "property value from object literal")] + //[DataRow(""" + // var o1 = { a: 1, b: 'b' } + // var a = <> + // """, + // """ + // var o1 = { a: 1, b: 'b' } + // param o1A int = o1.a + // var a = o1A + // """, + // DisplayName = "referenced property value from object literal")] + //[DataRow(""" + // param p 'a'||'b' = 'a' + // var v = <

> + // """, + // """ + // param p 'a'|'b' = 'a' + // param newParameter 'a' | 'b' = p + // var v = newParameter + // """, + // DisplayName = "string literal type")] //asdfg correct behavior? + //[DataRow(""" + // var a = { + // int: 1 + // } + // var b = a.|int + // """, + // """ + // var a = { + // int: 1 + // } + // param aInt int = a.int + // var b = aInt + // """, + // DisplayName = "object properties 1")] + //[DataRow(""" + // var a = { + // int: 1 + // } + // var b = |a.int + // """, + // """ + // var a = { + // int: 1 + // } + // param newParameter object = a + // var b = newParameter.int + // """, + //DisplayName = "object properties 2")] + //[DataRow(""" + // var a = { + // sku: { + // name: 'Standard_LRS' + // } + // } + // var b = a.|sku + // """, + // """ + // var a = { + // sku: { + // name: 'Standard_LRS' + // } + // } + // param aSku object = a.sku + // var b = aSku + // """, + // DisplayName = "object properties 3")] + //[DataRow(""" + // param p { + // i: int + // o: { + // i2: int + // } + // } = { i:1, o: { i2: 2} } + // var v = <

>.o.i2 + // """, + // """ + // param p { + // i: int + // o: { + // i2: int + // } + // } = { i:1, o: { i2: 2} } + // param newParameter { i: int, o: { i2: int } } = p + // var v = newParameter.o.i2 + // """, + // DisplayName = "custom object type, whole object")] + //[DataRow(""" + // param p { + // i: int + // o: { + // i2: int + // } + // } = { i:1, o: { i2: 2} } + // var v = p.|o.i2 + // """, + // """ + // param p { + // i: int + // o: { + // i2: int + // } + // } = { i:1, o: { i2: 2} } + // param pO { i2: int } = p.o + // var v = pO.i2 + // """, + // DisplayName = "custom object type, partial")] + //[DataRow(""" + // resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { + // unknownProperty: |123 + // } + // """, + // """ + // param unknownProperty int = 123 + // resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { + // unknownProperty: unknownProperty + // } + // """, + // DisplayName = "resource types undefined 1")] + //[DataRow(""" + // param p1 'abc'||'def' + // resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { + // unknownProperty: |p1 + // } + // """, + // """ + // param p1 'abc'|'def' + // param unknownProperty 'abc' | 'def' = p1 + // resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { + // unknownProperty: unknownProperty + // } + // """, + // DisplayName = "resource properties unknown property, follows expression's inferred type")] + //[DataRow(""" + // var foo = <<{ intVal: 2 }>> + // """, + // """ + // param { intVal: int } = { intVal: 2 } + // """)] + + ////asdf TODO(??) + ////[DataRow(""" + //// var a = <> + //// resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { } + //// """, + //// """ + //// param newParameter resource 'Microsoft.ContainerService/managedClusters@2021-03-01' = aksCluster + //// var a = newParameter + //// resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = { } + //// """, + //// DisplayName = "resource type")] + + + //[DataRow( + // """ + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: 'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: true + // remoteVirtualNetwork: { + // id: |'virtualNetworksId' + // } + // } + // } + // """, + // """ + // param id string = 'virtualNetworksId' + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: 'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: true + // remoteVirtualNetwork: { + // id: id + // } + // } + // } + // """, + // DisplayName = "resource types 3 asdfg")] + //[DataRow( + // """ + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: 'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: true + // remoteVirtualNetwork: |{ + // id: virtualNetworksId + // } + // } + // } + // """, + // """ + // param remoteVirtualNetwork object = { + // id: virtualNetworksId + // } + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: 'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: true + // remoteVirtualNetwork: remoteVirtualNetwork + // } + // } + // """, + // DisplayName = "resource types - SubResource")] + //[DataRow( + // """ + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: 'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: |true + // remoteVirtualNetwork: { + // id: virtualNetworksId' + // } + // } + // } + // """, + // """ + // param allowVirtualNetworkAccess bool = true + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: 'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: allowVirtualNetworkAccess + // remoteVirtualNetwork: { + // id: virtualNetworksId' + // } + // } + // } + // """, + // DisplayName = "resource types 5 asdfg")] + ////asdfg param ought to be named peeringName instead of name + //[DataRow( + // """ + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: |'virtualNetwork/name' + // properties: { + // allowVirtualNetworkAccess: true + // remoteVirtualNetwork: { + // id: virtualNetworksId + // } + // } + // } + // """, + // """ + // param name string = 'virtualNetwork/name' + // resource peering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = { + // name: name + // properties: { + // allowVirtualNetworkAccess: true + // remoteVirtualNetwork: { + // id: virtualNetworksId + // } + // } + // } + // """, + // DisplayName = "resource types - string property")] + //[DataRow( + // """ + // resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { + // name: 'name' + // location: 'location' + // kind: 'StorageV2' + // sku: { + // name: |'Premium_LRS' + // } + // } + // """, + // """ + // param name 'Premium_LRS' | 'Premium_ZRS' | 'Standard_GRS' | 'Standard_GZRS' | 'Standard_LRS' | 'Standard_RAGRS' | 'Standard_RAGZRS' | 'Standard_ZRS' | string = 'Premium_LRS' + // resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { + // name: 'name' + // location: 'location' + // kind: 'StorageV2' + // sku: { + // name: name + // } + // } + // """, + // DisplayName = "resource properties - string union")] + //[DataRow( + // """ + // param p int? + // var v = |p + // """, + // """ + // param p int? + // param newParameter int? = p + // var v = newParameter + // """, + // DisplayName = "nullable types")] + //[DataRow( + // """ + // param whoops int = 'not an int' + // var v = <

> + // """, + // """ + // param whoops int = 'not an int' + // param newParameter unknown = p + 1 + // var v = newParameter + // """, + // DisplayName = "error types")] + //[DataRow( + // """ + // param p1 { a: { b: string } } + // var v = p1 + // """, + // """ + // param p1 { a: { b: string } } + // param newParameter { a: { b: string } } = p1 + // var v = newParameter + // """ + // )] + + //asdfg TODO: secure types + //[DataRow(""" TODO: asdfg + // @secure() + // param i string = "secure" + // var j = <> + // """, + // """ + // param i string = "secure" + // @secure() + // param newParameter string = i + // var j = newParameter + // """, + // DisplayName = "secure string param reference")] + //asdfg TODO: secure types + //[DataRow(""" + // @secure() + // param i string = "secure" + // var j = <> + // """, + // """ + // param i string = "secure" + // @secure() + // param newParameter string = i + // var j = newParameter + // """, + // DisplayName = "expression with secure string param reference")] + //public async Task Params_InferType(string fileWithSelection, string expectedText) + //{ + // //asdfg await RunExtractToParameterTest(fileWithSelection, expectedText); + //} + + //////////////////////////////////////////////////////////////////// + + [TestMethod] + public async Task IfJustPropertyNameSelected_ThenExtractPropertyValue() + { + await RunExtractToParameterTest(""" + var isWindowsOS = true + var provisionExtensions = true + param _artifactsLocation string + @secure() + param _artifactsLocationSasToken string + + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'cse-windows/extension' + location: 'location' + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + setting|s: { // Property key selected - extract just the value + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + } + } + """, + """ + var isWindowsOS = true + var provisionExtensions = true + param _artifactsLocation string + @secure() + param _artifactsLocationSasToken string + @description('Json formatted public settings for the extension.') + param settings object = { + // Property key selected - extract just the value + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + + resource resourceWithProperties 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'cse-windows/extension' + location: 'location' + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: settings + } + } + """, + "IGNORE"); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow( + """ + resource vmName_resource 'Microsoft.Compute/virtualMachines@2019-12-01' = { + name: vmName + location: location + properties: { + osProfile: { + computerName: vmName + myproperty: { + abc: [ + { + def: [ + 'ghi' + '|jkl' + ] + } + ] + } + } + } + } + """, + """ + var newVariable = 'jkl' + resource vmName_resource 'Microsoft.Compute/virtualMachines@2019-12-01' = { + name: vmName + location: location + properties: { + osProfile: { + computerName: vmName + myproperty: { + abc: [ + { + def: [ + 'ghi' + newVariable + ] + } + ] + } + } + } + } + """, + DisplayName = "Array element, don't pick up property name")] + [DataRow( + """ + resource vmName_resource 'Microsoft.Compute/virtualMachines@2019-12-01' = { + name: vmName + location: location + properties: { + osProfile: { + computerName: vmName + myproperty: { + abc: <<[ + { + def: [ + 'ghi' + 'jkl' + ] + } + ]>> + } + } + } + } + """, + """ + var abc = [ + { + def: [ + 'ghi' + 'jkl' + ] + } + ] + resource vmName_resource 'Microsoft.Compute/virtualMachines@2019-12-01' = { + name: vmName + location: location + properties: { + osProfile: { + computerName: vmName + myproperty: { + abc: abc + } + } + } + } + """, + DisplayName = "Full property value as array, pick up property name")] + public async Task ShouldPickUpPropertyName_ButOnlyIfFullPropertyValue(string fileWithSelection, string? expectedVarText) + { + await RunExtractToVariableTest(fileWithSelection, expectedVarText); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow("var a = resourceGroup().locati|on", + """ + var resourceGroupLocation = resourceGroup().location + var a = resourceGroupLocation + """)] + [DataRow("var a = abc|().bcd", + """ + var newVariable = abc() + var a = newVariable.bcd + """, + null)] + [DataRow("var a = abc.bcd.|def", + """ + var bcdDef = abc.bcd.def + var a = bcdDef + """, + null)] + [DataRow("var a = abc.b|cd", + """ + var abcBcd = abc.bcd + var a = abcBcd + """, + null)] + [DataRow("var a = abc.bc|d", + """ + var abcBcd = abc.bcd + var a = abcBcd + """, + null)] + [DataRow("var a = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob|", + """ + var primaryEndpointsBlob = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob + var a = primaryEndpointsBlob + """, + null)] + [DataRow("var a = reference(storageAccount.id, '2018-02-01').prim|aryEndpoints.blob", + """ + var referencePrimaryEndpoints = reference(storageAccount.id, '2018-02-01').primaryEndpoints + var a = referencePrimaryEndpoints.blob + """)] + [DataRow("var a = a.b.|c.d.e", + """ + var bC = a.b.c + var a = bC.d.e + """, + null)] + public async Task PickUpNameFromPropertyAccess_UpToTwoLevels(string fileWithSelection, string? expectedVariableText) + { + await RunExtractToVariableTest(fileWithSelection, expectedVariableText); + } + + //////////////////////////////////////////////////////////////////// + + //asdfg + //[DataTestMethod] + //// + //// Closest ancestor expression is the top-level expression itself -> offer to update full expression + //// + //[DataRow( + // "storageUri:| reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // null, + // "storageUri: storageUri" + // )] + //[DataRow( + // "storageUri: reference(storageAccount.id, '2018-02-01').primaryEndpoints.|blob", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // null, + // "storageUri: storageUri" + // )] + //[DataRow( + // "storageUri: reference(storageAccount.id, '2018-02-01').primaryEndpoints.<>b", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // null, + // "storageUri: storageUri" + // )] + //// + //// Cursor is inside the property name -> offer full expression + //// + //[DataRow( + // "storageUri|: reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // null, + // "storageUri: storageUri" + // )] + //[DataRow( + // "<>ference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // null, + // "storageUri: storageUri" + // )] + //[DataRow( + // "<>", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // null, + // "storageUri: storageUri" + // )] + //// + //// Cursor is inside a subexpression -> only offer to extract that specific subexpression + //// + //// ... reference() call + //[DataRow( + // "storageUri: reference(storageAccount.id, '2018-02-01').|primaryEndpoints.blob", + // "var referencePrimaryEndpoints = reference(storageAccount.id, '2018-02-01').primaryEndpoints", + // null, + // "storageUri: referencePrimaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference|(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "var newVariable = reference(storageAccount.id, '2018-02-01')", + // null, + // "storageUri: newVariable.primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: refere<>8-02-01').primaryEndpoints.blob", + // "var newVariable = reference(storageAccount.id, '2018-02-01')", + // null, + // "storageUri: newVariable.primaryEndpoints.blob" + // )] + //// ... '2018-02-01' + //[DataRow( + // "storageUri: reference(storageAccount.id, |'2018-02-01').primaryEndpoints.blob", + // "var newVariable = '2018-02-01'", + // null, + // "storageUri: reference(storageAccount.id, newVariable).primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference(storageAccount.id, '2018-02-01|').primaryEndpoints.blob", + // "var newVariable = '2018-02-01'", + // null, + // "storageUri: reference(storageAccount.id, newVariable).primaryEndpoints.blob" + // )] + //// ... storageAccount.id + //[DataRow( + // "storageUri: reference(storageAccount.|id, '2018-02-01').primaryEndpoints.blob", + // "var storageAccountId = storageAccount.id", + // null, + // "storageUri: reference(storageAccountId, '2018-02-01').primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference(storageAccount.i|d, '2018-02-01').primaryEndpoints.blob", + // "var storageAccountId = storageAccount.id", + // null, + // "storageUri: reference(storageAccountId, '2018-02-01').primaryEndpoints.blob" + // )] + //// ... storageAccount + //[DataRow( + // "storageUri: reference(storageAc|count.id, '2018-02-01').primaryEndpoints.blob", + // "var newVariable = storageAccount", + // null, + // "storageUri: reference(newVariable.id, '2018-02-01').primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference(storageAc|count.id, '2018-02-01').primaryEndpoints.blob", + // "var newVariable = storageAccount", + // null, + // "storageUri: reference(newVariable.id, '2018-02-01').primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference(storageAc|count.id, '2018-02-01').primaryEndpoints.blob", + // "var newVariable = storageAccount", + // null, + // "storageUri: reference(newVariable.id, '2018-02-01').primaryEndpoints.blob" + // )] + //// ... inside reference(x, y) but not inside x or y -> closest enclosing expression is the reference() + //[DataRow( + // "storageUri: reference(storageAccount.id,| '2018-02-01').primaryEndpoints.blob", + // "var newVariable = reference(storageAccount.id, '2018-02-01')", + // null, + // "storageUri: newVariable.primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference(storageAccount.id, '2018-02-01' |).primaryEndpoints.blob", + // "var newVariable = reference(storageAccount.id, '2018-02-01')", + // null, + // "storageUri: newVariable.primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: reference|(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "var newVariable = reference(storageAccount.id, '2018-02-01')", + // null, + // "storageUri: newVariable.primaryEndpoints.blob" + // )] + //public async Task ShouldExpandSelectedExpressionsInALogicalWay(string lineWithSelection, string? expectedNewVarDeclaration, string? expectedNewParamDeclaration, string expectedModifiedLine) + //{ + // await RunExtractToVarAndOrParamOnSingleLineTest( + // inputTemplateWithSelection: """ + // resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { name: 'storageaccountname' } + + // resource vm 'Microsoft.Compute/virtualMachines@2019-12-01' = { name: 'vm', location: 'eastus' + // properties: { + // diagnosticsProfile: { + // bootDiagnostics: { + // LINEWITHSELECTION + // } + // } + // } + // } + // """, + // expectedOutputTemplate: """ + // resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { name: 'storageaccountname' } + + // EXPECTEDNEWDECLARATION + // resource vm 'Microsoft.Compute/virtualMachines@2019-12-01' = { name: 'vm', location: 'eastus' + // properties: { + // diagnosticsProfile: { + // bootDiagnostics: { + // EXPECTEDMODIFIEDLINE + // } + // } + // } + // } + // """, + // lineWithSelection, + // expectedNewVarDeclaration, + // expectedNewParamDeclaration, + // expectedModifiedLine); + //} + + //////////////////////////////////////////////////////////////////// + + //asdfg + //[DataTestMethod] + //[DataRow( + // "storageUri: reference(stora<>d, '2018-02-01').primaryEndpoints.blob", + // "var storageAccountId = storageAccount.id", + // "param storageAccountId string = storageAccount.id", + // "storageUri: reference(storageAccountId, '2018-02-01').primaryEndpoints.blob" + // )] + //[DataRow( + // "storageUri: refer<>ob", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // """ + // @description('Uri of the storage account to use for placing the console output and screenshot.') + // param storageUri object? /* unknown */ = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob + // """, + // "storageUri: storageUri" + // )] + //[DataRow( + // "storageUri: reference(storageAccount.id, '2018-02-01').primar<>", + // "var storageUri = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "param storageUri unknown = reference(storageAccount.id, '2018-02-01').primaryEndpoints.blob", + // "storageUri: storageUri" + // )] + //public async Task IfThereIsASelection_ThenPickUpEverythingInTheSelection_AfterExpanding(string lineWithSelection, string expectedNewVarDeclaration, string expectedNewParamDeclaration, string expectedModifiedLine) + //{ + // await RunExtractToVarAndOrParamOnSingleLineTest( + // inputTemplateWithSelection: """ + // resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { name: 'storageaccountname' } + + // resource vm 'Microsoft.Compute/virtualMachines@2019-12-01' = { name: 'vm', location: 'eastus' + // properties: { + // diagnosticsProfile: { + // bootDiagnostics: { + // LINEWITHSELECTION + // } + // } + // } + // } + // """, + // expectedOutputTemplate: """ + // resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { name: 'storageaccountname' } + + // EXPECTEDNEWDECLARATION + // resource vm 'Microsoft.Compute/virtualMachines@2019-12-01' = { name: 'vm', location: 'eastus' + // properties: { + // diagnosticsProfile: { + // bootDiagnostics: { + // EXPECTEDMODIFIEDLINE + // } + // } + // } + // } + // """, + // lineWithSelection, + // expectedNewVarDeclaration, + // expectedNewParamDeclaration, + // expectedModifiedLine); + //} + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + [DataRow( + """ + // My comment here + resource cassandraKeyspace 'Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2021-06-15' = { + name: 'testResource/cassandraKeyspace' + properties: { + resource: { + id: 'id' + } + <>: {} + } + } + """, + """ + // My comment here + @description('A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request.') + param options object = {} + resource cassandraKeyspace 'Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2021-06-15' = { + name: 'testResource/cassandraKeyspace' + properties: { + resource: { + id: 'id' + } + options: options + } + } + """, + """ + // My comment here + @description('A key-value pair of options to be applied for the request. This corresponds to the headers sent with the request.') + param options { autoscaleSettings: { maxThroughput: int? }?, throughput: int? } = {} + resource cassandraKeyspace 'Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2021-06-15' = { + name: 'testResource/cassandraKeyspace' + properties: { + resource: { + id: 'id' + } + options: options + } + } + """, + DisplayName = "Resource property description")] + [DataRow( + """ + type t = { + @description('My string\'s field') + myString: string + + @description(''' + My int's field + is very long + ''') + myInt: int + } + + param p t = { + myString: |'hello' + myInt: 42 + } + """, + """ + type t = { + @description('My string\'s field') + myString: string + + @description(''' + My int's field + is very long + ''') + myInt: int + } + + @description('My string\'s field') + param myString string = 'hello' + param p t = { + myString: myString + myInt: 42 + } + """, + "SAME", + DisplayName = "Apostrophe in description")] + [DataRow( + """ + type t = { + @description('My string\'s field') + myString: string + + @description(''' + My int's field + is very long + ''') + myInt: int + } + + param p t = { + myString: 'hello' + myInt: |42 + } + """, + """ + type t = { + @description('My string\'s field') + myString: string + + @description(''' + My int's field + is very long + ''') + myInt: int + } + + @description('My int\'s field\nis very long\n') + param myInt int = 42 + param p t = { + myString: 'hello' + myInt: myInt + } + """, + "SAME", + DisplayName = "multiline description")] + public async Task Params_ShouldPickUpDescriptions(string fileWithSelection, string expectedLooseParamText, string? expectedMediumParamText) + { + await RunExtractToParameterTest(fileWithSelection, expectedLooseParamText, expectedMediumParamText); + } + + //////////////////////////////////////////////////////////////////// + + [DataTestMethod] + //asdfg + //[DataRow( + // """ + // var v = <<1>> + // """, + // """ + // var newVariable = 1 + // var v = newVariable + // """, + // """ + // param newParameter int = 1 + // var v = newParameter + // """, + // DisplayName = "Extracting at top of file -> insert at top")] + //[DataRow( + // """ + // metadata firstLine = 'first line' + // metadata secondLine = 'second line' + + // // Some comments + // var v = <<1>> + // """, + // """ + // metadata firstLine = 'first line' + // metadata secondLine = 'second line' + + // // Some comments + // var newVariable = 1 + // var v = newVariable + // """, + // """ + // metadata firstLine = 'first line' + // metadata secondLine = 'second line' + + // // Some comments + // param newParameter int = 1 + // var v = newParameter + // """, + // DisplayName = "No existing params/vars above -> insert right before extraction line")] + [DataRow( + """ + param location string + param resourceGroup string + var simpleCalculation = 1 + 1 + var complexCalculation = simpleCalculation * 2 + + metadata line = 'line' + + var v = <<1>> + """, + """ + param location string + param resourceGroup string + var simpleCalculation = 1 + 1 + var complexCalculation = simpleCalculation * 2 + var newVariable = 1 + + metadata line = 'line' + + var v = newVariable + """, + """ + param location string + param resourceGroup string + param newParameter int = 1 + var simpleCalculation = 1 + 1 + var complexCalculation = simpleCalculation * 2 + + metadata line = 'line' + + var v = newParameter + """, + DisplayName = "Existing params and vars at top of file -> param and var inserted after their corresponding existing declarations")] + //[DataRow(//asdfg not handling comments before line as part of the line + // """ + // // location comment + // param location string + // // rg comment + // param resourceGroup string + // var simpleCalculation = 1 + 1 + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // var v = <<1>> + // """, + // """ + // // location comment + // param location string + // // rg comment + // param resourceGroup string + // var simpleCalculation = 1 + 1 + // var complexCalculation = simpleCalculation * 2 + // var newVariable = 1 + + // metadata line = 'line' + + // var v = newVariable + // """, + // """ + // // location comment + // param location string + // // rg comment + // param resourceGroup string + // param newParameter int = 1 + // var simpleCalculation = 1 + 1 + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // var v = newParameter + // """, + // DisplayName = "Existing params and vars at top of file -> param and var inserted after their corresponding existing declarations")] + //[DataRow( + // //asdfg + // """ + // // location comment + // param location string + + // // rg comment + // param resourceGroup string + + // var simpleCalculation = 1 + 1 + + // @export() + // @description('this still counts as having an empty line beforehand') + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // var v = <<1>> + // """, + // """ + // // location comment + // param location string + + // // rg comment + // param resourceGroup string + + // var simpleCalculation = 1 + 1 + + // @export() + // @description('this still counts as having an empty line beforehand') + // var complexCalculation = simpleCalculation * 2 + + // var newVariable = 1 + + // metadata line = 'line' + + // var v = newVariable + // """, + // """ + // // location comment + // param location string + + // // rg comment + // param resourceGroup string + + // param newParameter int = 1 + + // var simpleCalculation = 1 + 1 + + // @export() + // @description('this still counts as having an empty line beforehand') + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // var v = newParameter + // """, + // DisplayName = "If closest existing declaration has a blank line before it, insert a blank line above the new declaration")] + //[DataRow( + // """ + // param location string + // param resourceGroup string + // var simpleCalculation = 1 + 1 + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // param location2 string + // param resourceGroup2 string + // var simpleCalculation2 = 1 + 1 + // var complexCalculation2 = simpleCalculation * 2 + + // metadata line2 = 'line2' + + // var v = <<1>> + + // param location3 string + // param resourceGroup3 string + // var simpleCalculation3 = 1 + 1 + // var complexCalculation3 = simpleCalculation * 2 + // """, + // """ + // param location string + // param resourceGroup string + // var simpleCalculation = 1 + 1 + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // param location2 string + // param resourceGroup2 string + // var simpleCalculation2 = 1 + 1 + // var complexCalculation2 = simpleCalculation * 2 + // var newVariable = 1 + + // metadata line2 = 'line2' + + // var v = newVariable + + // param location3 string + // param resourceGroup3 string + // var simpleCalculation3 = 1 + 1 + // var complexCalculation3 = simpleCalculation * 2 + // """, + // """ + // param location string + // param resourceGroup string + // var simpleCalculation = 1 + 1 + // var complexCalculation = simpleCalculation * 2 + + // metadata line = 'line' + + // param location2 string + // param resourceGroup2 string + // param newParameter int = 1 + // var simpleCalculation2 = 1 + 1 + // var complexCalculation2 = simpleCalculation * 2 + + // metadata line2 = 'line2' + + // var v = newParameter + + // param location3 string + // param resourceGroup3 string + // var simpleCalculation3 = 1 + 1 + // var complexCalculation3 = simpleCalculation * 2 + // """, + // DisplayName = "Existing params and vars in multiple places in file -> insert after closest existing declarations above extraction")] + //[DataRow( + // """ + // param location string + + // resource virtualMachine 'Microsoft.Compute/virtualMachines@2020-12-01' = { + // name: 'name' + // location: location + // } + + // resource windowsVMExtensions 'Microsoft.Compute/virtualMachines/extensions@2020-12-01' = { + // parent: virtualMachine + // name: 'name' + // location: location + // properties: { + // publisher: 'Microsoft.Compute' + // type: 'CustomScriptExtension' + // typeHandlerVersion: '1.10' + // autoUpgradeMinorVersion: true + // settings: { + // fileUris: [ + // 'fileUris' + // ] + // } + // <>: { + // commandToExecute: 'loadTextContent(\'files/my script.ps1\')' + // } + // } + // } + // """, + // "IGNORE", + // """ + // param location string + // @description('The extension can contain either protectedSettings or protectedSettingsFromKeyVault or no protected settings at all.') + // param protectedSettings object = { + // commandToExecute: 'loadTextContent(\'files/my script.ps1\')' + // } + + // resource virtualMachine 'Microsoft.Compute/virtualMachines@2020-12-01' = { + // name: 'name' + // location: location + // } + + // resource windowsVMExtensions 'Microsoft.Compute/virtualMachines/extensions@2020-12-01' = { + // parent: virtualMachine + // name: 'name' + // location: location + // properties: { + // publisher: 'Microsoft.Compute' + // type: 'CustomScriptExtension' + // typeHandlerVersion: '1.10' + // autoUpgradeMinorVersion: true + // settings: { + // fileUris: [ + // 'fileUris' + // ] + // } + // protectedSettings: protectedSettings + // } + // } + // """, + // DisplayName = "get the rename position correct")] + public async Task VarsAndParams_InsertAfterExistingDeclarations(string fileWithSelection, string expectedVarText, string? expectedParamText) + { + await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\n"), expectedVarText, expectedParamText, "IGNORE"); + await RunExtractToVariableAndParameterTest(fileWithSelection.ReplaceNewlines("\r\n"), expectedVarText, expectedParamText, "IGNORE"); + } + + #region Support + + //asdfg + //private async Task RunExtractToVarAndOrParamOnSingleLineTest( + // string inputTemplateWithSelection, + // string expectedOutputTemplate, + // string lineWithSelection, + // string? expectedNewVarDeclaration, + // string? expectedNewParamDeclaration, + // string expectedModifiedLine + // ) + //{ + // await RunExtractToVariableTestIf( + // expectedNewVarDeclaration is { }, + // inputTemplateWithSelection.Replace("LINEWITHSELECTION", lineWithSelection), + // expectedOutputTemplate.Replace("EXPECTEDNEWDECLARATION", expectedNewVarDeclaration) + // .Replace("EXPECTEDMODIFIEDLINE", expectedModifiedLine)); + + // await RunExtractToParameterTestIf( + // expectedNewParamDeclaration is { }, + // inputTemplateWithSelection.Replace("LINEWITHSELECTION", lineWithSelection), + // expectedOutputTemplate.Replace("EXPECTEDNEWDECLARATION", expectedNewParamDeclaration) + // .Replace("EXPECTEDMODIFIEDLINE", expectedModifiedLine)); + //} + + //private async Task RunExtractToVariableAndOrParameterTest(string fileWithSelection, string expectedTextTemplate, string? expectedNewVarDeclaration, string? expectedNewParamDeclaration) + //{ + // await RunExtractToVariableTestIf( + // expectedNewVarDeclaration is { }, + // fileWithSelection, + // expectedTextTemplate.Replace("EXPECTEDNEWDECLARATION", expectedNewVarDeclaration)); + // await RunExtractToParameterTestIf( + // expectedNewParamDeclaration is { }, + // fileWithSelection, + // expectedTextTemplate.Replace("EXPECTEDNEWDECLARATION", expectedNewParamDeclaration)); + //} + + private async Task RunExtractToVariableAndParameterTest(string fileWithSelection, string? expectedVariableText, string? expectedLooseParamText, string? expectedMediumParamText) + { + await RunExtractToVariableTest( + fileWithSelection, + expectedVariableText); + await RunExtractToParameterTest( + fileWithSelection, + expectedLooseParamText, + expectedMediumParamText); + } + + //private async Task RunExtractToVariableTestIf(bool condition, string fileWithSelection, string? expectedText) //asdfg remove + //{ + // if (condition) + // { + // using (new AssertionScope("extract to var test")) + // { + // await RunExtractToVariableTest(fileWithSelection, expectedText); + // } + // } + //} + + //private async Task RunExtractToParameterTestIf(bool condition, string fileWithSelection, string? expectedText)//asdfg remove + //{ + // if (condition) + // { + // using (new AssertionScope("extract to param test")) + // { + // await RunExtractToParameterTest(fileWithSelection, expectedText); + // } + // } + //} + + private async Task RunExtractToVariableTest(string fileWithSelection, string? expectedText) + { + (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(fileWithSelection); + var extractedVar = codeActions.FirstOrDefault(x => x.Title.StartsWith(ExtractToVariableTitle)); + + if (expectedText == null) + { + extractedVar.Should().BeNull("expected no code action for extract var"); + } + else if (expectedText != "IGNORE") + { + extractedVar.Should().NotBeNull("expected an action to extract to variable"); + extractedVar!.Kind.Should().Be(CodeActionKind.RefactorExtract); + + var updatedFile = ApplyCodeAction(bicepFile, extractedVar); + updatedFile.Should().HaveSourceText(expectedText, "extract to variable should match expected outcome"); + } + } + + // expectedMediumParameterText can be "SAME" or "IGNORE" + private async Task RunExtractToParameterTest(string fileWithSelection, string? expectedLooseParameterText, string? expectedMediumParameterText) + { + if (expectedMediumParameterText == "SAME") + { + expectedMediumParameterText = expectedLooseParameterText; + } + + (var codeActions, var bicepFile) = await GetCodeActionsForSyntaxTest(fileWithSelection); + var extractedParamFixes = codeActions.Where(x => x.Title.StartsWith(ExtractToParameterTitle)).ToArray(); + extractedParamFixes.Should().HaveCountLessThanOrEqualTo(2); + + if (expectedLooseParameterText == null) + { + extractedParamFixes.Should().BeEmpty("expected no code actions to extract parameter"); + expectedMediumParameterText.Should().BeNull(); + } + else + { + if (expectedLooseParameterText != "IGNORE") + { + extractedParamFixes.Should().HaveCountGreaterThanOrEqualTo(1).Should().NotBeNull("expected at least one code action to extract to parameter"); + var looseFix = extractedParamFixes[0]; + looseFix.Kind.Should().Be(CodeActionKind.RefactorExtract); + + var updatedFileLoose = ApplyCodeAction(bicepFile, looseFix); + updatedFileLoose.Should().HaveSourceText(expectedLooseParameterText, "extract to param with loose typing should match expected outcome"); + } + + if (expectedMediumParameterText == null) + { + extractedParamFixes.Length.Should().Be(1, "expected only one code action to extract parameter (as loosely typed - which means the medium-strict version was the same as the loose version)"); + } + else + { + if (expectedMediumParameterText != "IGNORE") + { + extractedParamFixes.Should().HaveCountGreaterThanOrEqualTo(2, "expected a second option to extract to parameter"); + + var mediumFix = extractedParamFixes[1]; + mediumFix.Kind.Should().Be(CodeActionKind.RefactorExtract); + + var updatedFileMedium = ApplyCodeAction(bicepFile, mediumFix); + updatedFileMedium.Should().HaveSourceText(expectedMediumParameterText, "extract to param with medium-strict typing should match expected outcome"); + } + } + } + } +} + +#endregion diff --git a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs index d3c063e7b3f..4bbc3d0b076 100644 --- a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs @@ -1033,7 +1033,7 @@ There might also be a link to something [link](www.google.com) public async Task Hovers_are_displayed_on_type_property_access() { var (text, cursors) = ParserHelper.GetFileWithCursors(""" - type t = { + type testType = { @description('A named property') property: string *: int diff --git a/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs b/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs new file mode 100644 index 00000000000..5018e27e835 --- /dev/null +++ b/src/Bicep.LangServer.IntegrationTests/TypeStringifierTests.cs @@ -0,0 +1,756 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bicep.Core.Extensions; +using Bicep.Core.Parsing; +using Bicep.Core.PrettyPrintV2; +using Bicep.Core.Semantics; +using Bicep.Core.Syntax; +using Bicep.Core.TypeSystem; +using Bicep.Core.TypeSystem.Types; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using Bicep.LanguageServer.Refactor; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.LangServer.IntegrationTests; + +[TestClass] +public class TypeStringifierTests +{ + private static bool debugPrintAllSyntaxNodeTypes = false; + + // asdfg allowed values + // @secure + // asdfg string interpolations? + + [DataTestMethod] + [DataRow( + "type testType = int", + "type loose = int", + "type medium = int", + "type strict = int")] + [DataRow( + "type testType = string", + "type loose = string", + "type medium = string", + "type strict = string")] + [DataRow( + "type testType = bool", + "type loose = bool", + "type medium = bool", + "type strict = bool")] + [DataRow( + "type testType = 'abc'?", + "type loose = string?", + "type medium = string?", + "type strict = 'abc'?")] + public void SimpleTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = 123", + "type loose = int", + "type medium = int", + "type strict = 123")] + [DataRow( + "type testType = 'abc'", + "type loose = string", + "type medium = string", + "type strict = 'abc'")] + [DataRow( + "type testType = true", + "type loose = bool", + "type medium = bool", + "type strict = true")] + public void LiteralTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = 123|234", + "type loose = int", + "type medium = 123 | 234", + "type strict = 123 | 234")] + [DataRow( + "type testType = 'abc' | 'def' | null", + "type loose = string?", + "type medium = ('abc' | 'def')?", + "type strict = ('abc' | 'def')?")] + [DataRow( + "type testType = ('abc' | 'def')[]", + "type loose = array", + "type medium = ('abc' | 'def')[]", + "type strict = ('abc' | 'def')[]")] + [DataRow( + "type testType = { a: 'abc' | 'def' }", + "type loose = object", + "type medium = { a: 'abc' | 'def' }", + "type strict = { a: 'abc' | 'def' }")] + [DataRow( + "type testType = 123|null", + "type loose = int?", + "type medium = int?", // I think "123?" would also be acceptable, it's not obvious which is better + "type strict = 123?")] + public void DontWidenLiteralTypesWithMediumWhenPartOfAUnion(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = object", + "type loose = object", + "type medium = object", + "type strict = object")] + [DataRow( + "type testType = {}", + "type loose = object", + "type medium = object", + "type strict = { }")] + [DataRow( + "type testType = { empty: { } }", + "type loose = object", + "type medium = { empty: object }", + "type strict = { empty: { } }")] + [DataRow( + "type testType = {a:123,b:'abc'}", + "type loose = object", + "type medium = { a: int, b: string }", + "type strict = { a: 123, b: 'abc' }")] + [DataRow( + "type testType = { 'my type': 'my string' }", + "type loose = object", + "type medium = { 'my type': string }", + "type strict = { 'my type': 'my string' }")] + [DataRow( + "type testType = { 'true': true }", + "type loose = object", + "type medium = { 'true': bool }", + "type strict = { 'true': true }")] + public void ObjectTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = 'abc' | 'def' | 'ghi'", + "type loose = string", + "type medium = 'abc' | 'def' | 'ghi'", + "type strict = 'abc' | 'def' | 'ghi'")] + [DataRow( + "type testType = 1 | 2 | 3 | -1", + "type loose = int", + "type medium = -1 | 1 | 2 | 3", + "type strict = -1 | 1 | 2 | 3")] + [DataRow( + "type testType = true|false", + "type loose = bool", + "type medium = false | true", + "type strict = false | true")] + [DataRow( + "type testType = null|true|false", + "type loose = bool?", + "type medium = (false | true)?", + "type strict = (false | true)?")] + [DataRow( + "type testType = { a: 'a'|null, b: 'a'|'b'|null, c: 'a'|'b'|'c'|null }", + "type loose = object", + "type medium = { a: string?, b: ('a'|'b')?, c: ('a'|'b'|'c')? }", + "type strict = { a: 'a'?, b: ('a'|'b')?, c: ('a'|'b'|'c')? }")] + public void UnionTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = [1, 2, 3]", + "type loose = array", + "type medium = int[]", + "type strict = [1, 2, 3]")] + [DataRow( + "type testType = [ object, array, {}, [] ]", + "type loose = array", + "type medium = [ object, array, object, array ]", + "type strict = [ object, array, {}, [] ]")] + [DataRow( + "type testType = [int, string]", + "type loose = array", + "type medium = [int, string]", + "type strict = [int, string]")] + [DataRow( + "type testType = [123, 'abc' | 'def']", + "type loose = array", + "type medium = [int, 'abc' | 'def']", + "type strict = [123, 'abc' | 'def']")] + // Bicep infers a type from literals like "['abc', 'def'] as typed tuples, the user more likely wants "string[]" if all the items are of the same type + [DataRow( + "type testType = int[]", + "type loose = array", + "type medium = int[]", + "type strict = int[]")] + [DataRow( + "type testType = int[][]", + "type loose = array", + "type medium = int[][]", + "type strict = int[][]")] + [DataRow( + "type testType = [ int ]", + "type loose = array", + "type medium = int[]", + "type strict = [ int ]")] + [DataRow( + "type testType = [ int, int, int ]", + "type loose = array", + "type medium = int[]", + "type strict = [ int, int, int ]")] + [DataRow( + "type testType = [ int?, int?, int? ]", + "type loose = array", + "type medium = (int?)[]", + "type strict = [ int?, int?, int? ]")] + [DataRow( + "type testType = [ int?, int, int? ]", + "type loose = array", + "type medium = [ int?, int, int? ]", + "type strict = [ int?, int, int? ]")] + [DataRow( + "type testType = [ 'abc'|'def', 'abc'|'def' ]", + "type loose = array", + "type medium = ('abc'|'def')[]", + "type strict = [ 'abc'|'def', 'abc'|'def' ]")] + [DataRow( + "type testType = [ 'abc'|'def', 'def'|'ghi' ]", + "type loose = array", + "type medium = [ 'abc'|'def', 'def'|'ghi' ]", + "type strict = [ 'abc'|'def', 'def'|'ghi' ]")] + public void TupleTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = string[]", + "type loose = array", + "type medium = string[]", + "type strict = string[]")] + [DataRow( + "type testType = (string?)[]", + "type loose = array", + "type medium = (string?)[]", + "type strict = (string?)[]")] + [DataRow( + "type testType = 'abc'[]", + "type loose = array", + "type medium = string[]", + "type strict = 'abc'[]")] + [DataRow( + "type testType = ('abc'|'def')[]", + "type loose = array", + "type medium = ('abc' | 'def')[]", + "type strict = ('abc' | 'def')[]")] + public void TypedArrays(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = array", + "type loose = array", + "type medium = array", + "type strict = array")] + public void ArrayType(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = []", + "type loose = array", + // Bicep infers an empty array with no items from "[]", the user more likely wants "array" + "type medium = array", + "type strict = []")] + public void EmptyArray(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = {}", + "type loose = object", + "type medium = object", + "type strict = { }")] + public void EmptyObject(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = []", + "type loose = array", + "type medium = array", + "type strict = []")] + public void EmptyArrays(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = [testType?]", + "type loose = array", + "type medium = (object /* recursive */?)[]", // CONSIDER: question mark before the comment would be better + "type strict = [object /* recursive */?]")] + [DataRow( + "type testType = [string, testType?]", + "type loose = array", + "type medium = [string, object /* recursive */?]", + "type strict = [string, object /* recursive */?]")] + [DataRow( + "type testType = [string, testType]?", + "type loose = array?", + "type medium = [string, object /* recursive */]?", + "type strict = [string, object /* recursive */]?")] + [DataRow( + "type testType = {t: testType?, a: [testType, testType?]?}", + "type loose = object", + "type medium = {a: [object /* recursive */, object /* recursive */?]?, t: object /* recursive */?}", + "type strict = {a: [object /* recursive */, object /* recursive */?]?, t: object /* recursive */?}")] + public void RecursiveTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + "type testType = string?", + "type loose = string?", + "type medium = string?", + "type strict = string?")] + [DataRow( + "type testType = string?", + "type loose = string?", + "type medium = string?", + "type strict = string?")] + [DataRow( + "type testType = null|true", + "type loose = bool?", + "type medium = bool?", + "type strict = true?")] + [DataRow( + "type testType = null|true|false", + "type loose = bool?", + "type medium = (false | true)?", + "type strict = (false | true)?")] + [DataRow( + "type testType = (null|true)|null", + "type loose = bool?", + "type medium = bool?", + "type strict = true?")] + [DataRow( + "type testType = (null|'a')|null|'a'", + "type loose = string?", + "type medium = string?", + "type strict = 'a'?")] + [DataRow( + "type testType = (null|'a'|'b')|null|'c'", + "type loose = string?", + "type medium = ('a' | 'b' | 'c')?", + "type strict = ('a' | 'b' | 'c')?")] + [DataRow( + "type testType = null|(1|2)", + "type loose = int?", + "type medium = (1 | 2)?", + "type strict = (1 | 2)?")] + [DataRow( + "type testType = null|['a', 'b']", + "type loose = array?", + "type medium = string[]?", + "type strict = ['a', 'b']?")] + [DataRow( + "type testType = null|{a: 'a', b: 1234?}", + "type loose = object?", + "type medium = { a: string, b: int? }?", + "type strict = { a: 'a', b: 1234? }?")] + [DataRow( + "type testType = {a: 'a', b: 1234?}?", + "type loose = object?", + "type medium = { a: string, b: int? }?", + "type strict = { a: 'a', b: 1234? }?")] + [DataRow( + "type testType = array?", + "type loose = array?", + "type medium = array?", + "type strict = array?")] + [DataRow( + "type testType = []?", + "type loose = array?", + "type medium = array?", + "type strict = []?")] + [DataRow( + "type testType = object?", + "type loose = object?", + "type medium = object?", + "type strict = object?")] + [DataRow( + "type testType = {}?", + "type loose = object?", + "type medium = object?", + "type strict = {}?")] + [DataRow( + """ + type testType = { a: 'a' | null, b: 'a' | 'b' | null, c: 'a' | 'b' | 'c' | null }? + """, + "type loose = object?", + "type medium = { a: string?, b: ('a' | 'b')?, c: ('a' | 'b' | 'c')? }?", + "type strict = { a: 'a'?, b: ('a' | 'b')?, c: ('a' | 'b' | 'c')? }?")] + public void NullableTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + //asdfg + // + // "fileUris" property + // + //[DataRow( + // """ + // var _artifactsLocation = 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/azuredeploy.json' + // var _artifactsLocationSasToken = '?sas=abcd' + // var commandToExecute = 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1' + + // resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = { + // properties: { + // publisher: 'Microsoft.Compute' + // type: 'CustomScriptExtension' + // typeHandlerVersion: '1.8' + // autoUpgradeMinorVersion: true + // settings: { + // fileUris: [ + // uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + // ] + // commandToExecute: commandToExecute + // } + // } + // } + // """, + // "fileUris", + // "type loose = array", + // "type medium = string[]", + // "type strict = ['https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/writeblob.ps1?sas=abcd']", + // DisplayName = "virtual machine extensions fileUris property")] + //// + //// "settings" property + //// + //[DataRow( + // """ + // var _artifactsLocation = 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/azuredeploy.json' + // var _artifactsLocationSasToken = '?sas=abcd' + // var commandToExecute = 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1' + + // resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = { + // properties: { + // publisher: 'Microsoft.Compute' + // type: 'CustomScriptExtension' + // typeHandlerVersion: '1.8' + // autoUpgradeMinorVersion: true + // settings: { + // fileUris: [ + // uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + // ] + // commandToExecute: commandToExecute + // } + // } + // } + // """, + // "settings", + // "type loose = object", + // "type medium = { commandToExecute: string, fileUris: string[] }", + // "type strict = { commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1', fileUris: ['https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-windows/writeblob.ps1?sas=abcd'] }", + // DisplayName = "virtual machine extensions settings property")] + // + // "properties" property + // + [DataRow( + """ + var isWindowsOS = true + var provisionExtensions = true + param _artifactsLocation string + @secure() + param _artifactsLocationSasToken string + + resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = if (isWindowsOS && provisionExtensions) { + name: 'cse-windows/extension' + location: 'location' + properties: { + publisher: 'Microsoft.Compute' + type: 'CyustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: 'commandToExecute' + } + } + } + """, + "properties", + "type loose = object", + // asdfg object? /* any */ ?? + // asdfg so many "?"'s + """ + type medium = { + autoUpgradeMinorVersion: bool? + forceUpdateTag: string? + instanceView: { + name: string? + statuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + substatuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + type: string? + typeHandlerVersion: string? + }? + protectedSettings: object? /* any */ + publisher: string? + settings: object? /* any */ + type: string? + typeHandlerVersion: string? + } + """, + """ + type strict = { + autoUpgradeMinorVersion: bool? + forceUpdateTag: string? + instanceView: { + name: string? + statuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + substatuses: { + code: string? + displayStatus: string? + level: ('Error' | 'Info' | 'Warning')? + message: string? + time: string? + }[]? + type: string? + typeHandlerVersion: string? + }? + protectedSettings: object? /* any */ + publisher: string? + settings: object? /* any */ + type: string? + typeHandlerVersion: string? + } + """, + DisplayName = "virtual machine extensions properties")] + public void ResourcePropertyTypes(string resourceDeclaration, string resourcePropertyName, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromResourceProperty(resourceDeclaration, resourcePropertyName, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + [DataTestMethod] + [DataRow( + """ + type t1 = { abc: int } + type testType = t1 + """, + "type loose = object", + "type medium = { abc: int }", // TODO: better would be "type medium = t1" but Bicep type system doesn't currently support it + "type strict = { abc: int }" // TODO: better would be "type strict = t1" but Bicep type system doesn't currently support it + )] + [DataRow( + """ + type t1 = { + a: string + b: string + } + type t2 = t1[] + type t3 = { + t1Property: t1 + t2Property: t2 + } + type testType = [t3] + """, + "type loose = array", + "type medium = { t1Property: { a: string, b: string }, t2Property: { a: string, b: string }[] }[]", // TODO: better would be "type medium = t3[]" but Bicep type system doesn't currently support it + "type strict = [ { t1Property: { a: string, b: string }, t2Property: { a: string, b: string }[] } ]" // TODO: better would be "type strict = [ t3 ]" but Bicep type system doesn't currently support it + )] + [DataRow( + """ + type t1 = { a: 'abc', b: 123 } + type testType = { a: t1, b: [t1, t1] } + """, + "type loose = object", // TODO: better: "{ a: t1, b: [t1, t1] }" + "type medium = { a: { a: string, b: int }, b: { a: string, b: int }[] }", // TODO: better: "{ a: t1, b: [t1, t1] }" + "type strict = { a: { a: 'abc', b: 123 }, b: [{ a: 'abc', b: 123 }, { a: 'abc', b: 123 }] }")] + public void NamedTypes(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + RunTestFromTypeDeclaration(typeDeclaration, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + #region Support + + // input is a type declaration statement for type "testType", e.g. "type testType = int" + private static void RunTestFromTypeDeclaration(string typeDeclaration, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + var compilationResult = CompilationHelper.Compile(typeDeclaration); + var semanticModel = compilationResult.Compilation.GetEntrypointSemanticModel(); + var declarationSyntax = semanticModel.Root.TypeDeclarations[0].DeclaringSyntax; + var declaredType = semanticModel.GetDeclaredType(semanticModel.Root.TypeDeclarations.Single(t => t.Name == "testType").Value); + declaredType.Should().NotBeNull(); + + RunTestHelper(null, declaredType!, semanticModel, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + // input is a resource declaration for resource "testResource" and a property name such as "properties" that is exposed anywhere on the resource + private static void RunTestFromResourceProperty(string resourceDeclaration, string resourcePropertyName, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + var compilationResult = CompilationHelper.Compile(resourceDeclaration); + var semanticModel = compilationResult.Compilation.GetEntrypointSemanticModel(); + var resourceSyntax = semanticModel.Root.ResourceDeclarations.Single(r => r.Name == "testResource").DeclaringResource; + + var properties = GetAllSyntaxOfType(resourceSyntax); + var matchingProperty = properties.Single(p => p.Key is IdentifierSyntax id && id.NameEquals(resourcePropertyName)); + + var inferredType = semanticModel.GetTypeInfo(matchingProperty.Value); + var declaredType = semanticModel.GetDeclaredType(matchingProperty); + var matchingPropertyType = declaredType is AnyType || declaredType == null ? inferredType : declaredType; + matchingPropertyType.Should().NotBeNull(); + + RunTestHelper(null, matchingPropertyType!, semanticModel, expectedLooseSyntax, expectedMediumStrictSyntax, expectedStrictSyntax); + } + + private static void RunTestHelper(TypeProperty? typeProperty, TypeSymbol typeSymbol, SemanticModel semanticModel, string expectedLooseSyntax, string expectedMediumStrictSyntax, string expectedStrictSyntax) + { + if (debugPrintAllSyntaxNodeTypes) + { + DebugPrintAllSyntaxNodeTypes(semanticModel); + } + + var looseSyntax = TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Loose); + var mediumStrictSyntax = TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Medium); + var strictSyntax = TypeStringifier.Stringify(typeSymbol, typeProperty, TypeStringifier.Strictness.Strict); + + using (new AssertionScope()) + { + CompilationHelper.Compile(expectedLooseSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected loose syntax should be error-free"); + CompilationHelper.Compile(expectedMediumStrictSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected medium strictness syntax should be error-free"); + CompilationHelper.Compile(expectedStrictSyntax).Diagnostics.Should().NotHaveAnyDiagnostics("expected strict syntax should be error-free"); + } + + using (new AssertionScope()) + { + var actualLooseSyntaxType = $"type loose = {looseSyntax}"; + actualLooseSyntaxType.Should().EqualIgnoringBicepFormatting(expectedLooseSyntax); + + string actualMediumLooseSyntaxType = $"type medium = {mediumStrictSyntax}"; + actualMediumLooseSyntaxType.Should().EqualIgnoringBicepFormatting(expectedMediumStrictSyntax); + + string actualStrictSyntaxType = $"type strict = {strictSyntax}"; + actualStrictSyntaxType.Should().EqualIgnoringBicepFormatting(expectedStrictSyntax); + } + + // asdfg verify resulting types compile + } + + private static IEnumerable GetAllSyntaxOfType(SyntaxBase syntax) where TSyntax : SyntaxBase + => SyntaxVisitor.GetAllSyntaxOfType(syntax); + + private class SyntaxVisitor : CstVisitor + { + private readonly List syntaxList = new(); + + private SyntaxVisitor() + { + } + + public static IEnumerable GetAllSyntaxOfType(SyntaxBase syntax) where TSyntax : SyntaxBase + { + var visitor = new SyntaxVisitor(); + visitor.Visit(syntax); + + return visitor.syntaxList.OfType(); + } + + protected override void VisitInternal(SyntaxBase syntax) + { + syntaxList.Add(syntax); + base.VisitInternal(syntax); + } + + } + + private static void DebugPrintAllSyntaxNodeTypes(SemanticModel semanticModel) + { + var allSyntaxNodes = GetAllSyntaxNodesVisitor.Build(semanticModel.Root.Syntax); + foreach (var node in allSyntaxNodes.Where(s => s is not Token && s is not IdentifierSyntax)) + { + Trace.WriteLine($"** {node.GetDebuggerDisplay().ReplaceNewlines(" ").TruncateWithEllipses(150)}"); + Trace.WriteLine($" ... type info: {semanticModel.GetTypeInfo(node).Name}"); + Trace.WriteLine($" ... declared type: {semanticModel.GetDeclaredType(node)?.Name}"); + } + } + + private class GetAllSyntaxNodesVisitor : CstVisitor + { + private readonly List syntaxList = new(); + + public static ImmutableArray Build(SyntaxBase syntax) + { + var visitor = new GetAllSyntaxNodesVisitor(); + visitor.Visit(syntax); + + return [..visitor.syntaxList]; + } + + protected override void VisitInternal(SyntaxBase syntax) + { + syntaxList.Add(syntax); + base.VisitInternal(syntax); + } + } + + #endregion Support +} + diff --git a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs index f27914a3686..16249912762 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs @@ -976,6 +976,8 @@ private static bool IsUnionTypeMemberContext(List matchingNodes, int private static IndexedSyntaxContext? TryGetTypeArgumentContext(List matchingNodes, int offset) { + //asdfg use IsTailMatch like this?? + // someType<|> // abc.someType(|) if (SyntaxMatcher.IsTailMatch(matchingNodes, (instantiation, token) => token == instantiation.OpenChevron)) @@ -1368,7 +1370,7 @@ private static bool CanInsertChildNodeAtOffset(ArraySyntax arraySyntax, int offs }; } - private class ActiveScopesVisitor : SymbolVisitor + public/*asdfg was private - extract*/ class ActiveScopesVisitor : SymbolVisitor { private readonly int offset; diff --git a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs index b6d2c413c23..8bcb5e84708 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs @@ -172,7 +172,7 @@ private IEnumerable GetDeclarationCompletions(SemanticModel mode { string prefix = resourceSnippet.Prefix; BicepTelemetryEvent telemetryEvent = BicepTelemetryEvent.CreateTopLevelDeclarationSnippetInsertion(prefix); - var command = TelemetryHelper.CreateCommand + var command = TelemetryHelper.CreateCommand //asdfgasdfg ( title: "top level snippet completion", name: TelemetryConstants.CommandName, @@ -1597,20 +1597,7 @@ private TextSpan GetNextLineSpan(ImmutableArray lineStarts, int line, Progr var nextLine = line + 1; if (lineStarts.Length > nextLine) { - var nextLineStart = lineStarts[nextLine]; - - int nextLineEnd; - - if (lineStarts.Length > nextLine + 1) - { - nextLineEnd = lineStarts[nextLine + 1] - 1; - } - else - { - nextLineEnd = programSyntax.GetEndPosition(); - } - - return new TextSpan(nextLineStart, nextLineEnd - nextLineStart); + return TextCoordinateConverter.GetLineSpan(lineStarts, programSyntax.GetEndPosition(), nextLine); } return TextSpan.Nil; @@ -1665,7 +1652,7 @@ private static CompletionItem CreatePropertyNameCompletion(TypeProperty property { var required = TypeHelper.IsRequired(property); - var escapedPropertyName = IsPropertyNameEscapingRequired(property) ? StringUtils.EscapeBicepString(property.Name) : property.Name; + var escapedPropertyName = StringUtils.EscapeBicepPropertyName(property.Name); var suffix = includeColon ? ":" : string.Empty; return CompletionItemBuilder.Create(CompletionItemKind.Property, property.Name) // property names that match Bicep keywords or contain non-identifier chars need to be escaped @@ -2135,10 +2122,10 @@ private static CompletionItemKind GetCompletionItemKind(SymbolKind symbolKind) = }; private static bool IsPropertyNameEscapingRequired(TypeProperty property) => - !Lexer.IsValidIdentifier(property.Name) || LanguageConstants.NonContextualKeywords.ContainsKey(property.Name); + StringUtils.IsPropertyNameEscapingRequired(property.Name); private static string FormatPropertyDetail(TypeProperty property) => - TypeHelper.IsRequired(property) + TypeHelper.IsRequired(property) //asdfg ? $"{property.Name} (Required)" : property.Name; diff --git a/src/Bicep.LangServer/Completions/SyntaxMatcher.cs b/src/Bicep.LangServer/Completions/SyntaxMatcher.cs index dce77ed88b1..4c1cfd3baf3 100644 --- a/src/Bicep.LangServer/Completions/SyntaxMatcher.cs +++ b/src/Bicep.LangServer/Completions/SyntaxMatcher.cs @@ -7,7 +7,7 @@ using Bicep.Core.Parsing; using Bicep.Core.Syntax; -namespace Bicep.LanguageServer.Completions +namespace Bicep.LanguageServer.Completions //asdfg rename? { public static class SyntaxMatcher { @@ -174,16 +174,17 @@ public static List FindNodesMatchingOffset(SyntaxBase syntax, int of return nodes; } - public static ImmutableArray FindNodesInRange(ProgramSyntax syntax, int startOffset, int endOffset) + // Finds syntax nodes that encompass the entire range (i.e. are found at both the start and end of + // the range) + public static List FindNodesSpanningRange(ProgramSyntax syntax, int startOffset, int endOffset) { - var startNodes = FindNodesMatchingOffset(syntax, startOffset); + var startNodes = FindNodesMatchingOffset(syntax, startOffset); // in order of least specific (ProgramSyntax) to most specific var endNodes = FindNodesMatchingOffset(syntax, endOffset); return startNodes .Zip(endNodes, (x, y) => object.ReferenceEquals(x, y) ? x : null) - .TakeWhile(x => x is not null) - .WhereNotNull() - .ToImmutableArray(); + .TakeWhileNotNull() + .ToList(); } public static List FindNodesMatchingOffsetExclusive(ProgramSyntax syntax, int offset) @@ -205,5 +206,8 @@ public static (TResult? node, int index) FindLastNodeOfType return (node, index); } + + public static (TPredicate? node, int index) FindLastNodeOfType(List matchingNodes) where TPredicate : SyntaxBase //asdfg refactor to use + => FindLastNodeOfType(matchingNodes); } } diff --git a/src/Bicep.LangServer/Extensions/IPositionableExtensions.cs b/src/Bicep.LangServer/Extensions/IPositionableExtensions.cs index 30b37d4fc92..6c978f402e9 100644 --- a/src/Bicep.LangServer/Extensions/IPositionableExtensions.cs +++ b/src/Bicep.LangServer/Extensions/IPositionableExtensions.cs @@ -3,9 +3,11 @@ using System.Collections.Immutable; using Bicep.Core.Extensions; using Bicep.Core.Parsing; +using Bicep.Core.Text; using Bicep.LanguageServer.Utils; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; +using OmniSharpRange = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; +using OmniSharpPosition = OmniSharp.Extensions.LanguageServer.Protocol.Models.Position; namespace Bicep.LanguageServer.Extensions { @@ -20,14 +22,14 @@ public static TextSpan TryGetSpanSlice(this IPositionable positionable, int star Math.Min(start, positionable.Span.Length), Math.Min(length, positionable.Span.Length - start)); - public static Range ToRange(this IPositionable positionable, ImmutableArray lineStarts) => + public static OmniSharpRange ToRange(this IPositionable positionable, ImmutableArray lineStarts) => new() { Start = PositionHelper.GetPosition(lineStarts, positionable.Span.Position), End = PositionHelper.GetPosition(lineStarts, positionable.GetEndPosition()) }; - public static IEnumerable ToRangeSpanningLines(this IPositionable positionable, ImmutableArray lineStarts) + public static IEnumerable ToRangeSpanningLines(this IPositionable positionable, ImmutableArray lineStarts) { var start = PositionHelper.GetPosition(lineStarts, positionable.Span.Position); var end = PositionHelper.GetPosition(lineStarts, positionable.GetEndPosition()); @@ -35,13 +37,23 @@ public static IEnumerable ToRangeSpanningLines(this IPositionable positio while (start.Line < end.Line) { var lineEnd = PositionHelper.GetPosition(lineStarts, lineStarts[start.Line + 1] - 1); - yield return new Range(start, lineEnd); + yield return new OmniSharpRange(start, lineEnd); start = new Position(start.Line + 1, 0); } - yield return new Range(start, end); + yield return new OmniSharpRange(start, end); + } + + public static int ToTextOffset(this OmniSharpPosition position, ImmutableArray lineStarts) + { + return TextCoordinateConverter.GetOffset(lineStarts, position.Line, position.Character); + } + + public static TextSpan ToTextSpan(this OmniSharpRange range, ImmutableArray lineStarts) + { + var start = range.Start.ToTextOffset(lineStarts); + return new(start, range.End.ToTextOffset(lineStarts) - start); } } } - diff --git a/src/Bicep.LangServer/Handlers/BicepCodeActionHandler.cs b/src/Bicep.LangServer/Handlers/BicepCodeActionHandler.cs index 97ef1c1cb5c..cfd07bb1268 100644 --- a/src/Bicep.LangServer/Handlers/BicepCodeActionHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepCodeActionHandler.cs @@ -1,21 +1,31 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Xml; using Bicep.Core; using Bicep.Core.Analyzers; using Bicep.Core.CodeAction; using Bicep.Core.CodeAction.Fixes; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Navigation; using Bicep.Core.Parsing; +using Bicep.Core.PrettyPrintV2; using Bicep.Core.Semantics; +using Bicep.Core.Syntax; using Bicep.Core.Text; +using Bicep.Core.TypeSystem; +using Bicep.Core.TypeSystem.Types; using Bicep.Core.Workspaces; using Bicep.LanguageServer.CompilationManager; using Bicep.LanguageServer.Completions; using Bicep.LanguageServer.Extensions; using Bicep.LanguageServer.Providers; +using Bicep.LanguageServer.Refactor; using Bicep.LanguageServer.Telemetry; using Bicep.LanguageServer.Utils; using Newtonsoft.Json.Linq; @@ -23,6 +33,8 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using static Bicep.LanguageServer.Completions.BicepCompletionContext; +using DiagnosticSource = Bicep.Core.Diagnostics.DiagnosticSource; using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; namespace Bicep.LanguageServer.Handlers @@ -54,7 +66,7 @@ public BicepCodeActionHandler(ICompilationManager compilationManager, IClientCap return null; } - var requestStartOffset = PositionHelper.GetOffset(compilationContext.LineStarts, request.Range.Start); + var requestStartOffset = PositionHelper.GetOffset(compilationContext.LineStarts, request.Range.Start); //asdfg refactor (and below) var requestEndOffset = request.Range.Start != request.Range.End ? PositionHelper.GetOffset(compilationContext.LineStarts, request.Range.End) : requestStartOffset; @@ -69,7 +81,7 @@ public BicepCodeActionHandler(ICompilationManager compilationManager, IClientCap fixable.Span.ContainsInclusive(requestEndOffset) || (requestStartOffset <= fixable.Span.Position && fixable.GetEndPosition() <= requestEndOffset)) .OfType() - .SelectMany(fixable => fixable.Fixes.Select(fix => CreateCodeFix(request.TextDocument.Uri, compilationContext, fix))); + .SelectMany(fixable => fixable.Fixes.Select(fix => CreateCodeAction(request.TextDocument.Uri, compilationContext, fix))); List commandOrCodeActions = new(); @@ -112,12 +124,16 @@ public BicepCodeActionHandler(ICompilationManager compilationManager, IClientCap commandOrCodeActions.AddRange(editLinterRuleActions); } - var matchingNodes = SyntaxMatcher.FindNodesInRange(compilationContext.ProgramSyntax, requestStartOffset, requestEndOffset); + var nodesInRange = SyntaxMatcher.FindNodesSpanningRange(compilationContext.ProgramSyntax, requestStartOffset, requestEndOffset); var codeFixes = GetDecoratorCodeFixProviders(semanticModel) - .SelectMany(provider => provider.GetFixes(semanticModel, matchingNodes)) - .Select(fix => CreateCodeFix(request.TextDocument.Uri, compilationContext, fix)); + .SelectMany(provider => provider.GetFixes(semanticModel, nodesInRange)) + .Select(fix => CreateCodeAction(request.TextDocument.Uri, compilationContext, fix)); commandOrCodeActions.AddRange(codeFixes); + var refactoringFixes = ExtractVarAndParam.GetRefactoringFixes(compilationContext, compilation, semanticModel, nodesInRange) + .Select(fix => CreateCodeAction(documentUri, compilationContext, fix.fix, fix.renamePosition)); + commandOrCodeActions.AddRange(refactoringFixes); + return new(commandOrCodeActions); } @@ -209,12 +225,13 @@ public override Task Handle(CodeAction request, CancellationToken ca return Task.FromResult(request); } - private static CommandOrCodeAction CreateCodeFix(DocumentUri uri, CompilationContext context, CodeFix fix) + private static CommandOrCodeAction CreateCodeAction(DocumentUri uri, CompilationContext context, CodeFix fix, (int line, int character)? renamePosition = null) { var codeActionKind = fix.Kind switch { CodeFixKind.QuickFix => CodeActionKind.QuickFix, CodeFixKind.Refactor => CodeActionKind.Refactor, + CodeFixKind.RefactorExtract => CodeActionKind.RefactorExtract, _ => CodeActionKind.Empty, }; @@ -233,7 +250,21 @@ private static CommandOrCodeAction CreateCodeFix(DocumentUri uri, CompilationCon NewText = replacement.Text }) } - } + }, + Command = !renamePosition.HasValue ? null : + new Command() + { + Name = "bicep.internal.startRename", + Title = "Rename new identifier" + } + .WithArguments( + uri.ToString(), //asdfg with spaces? + new + { + line = renamePosition.Value.line, + character = renamePosition.Value.character, + } + ) }; } diff --git a/src/Bicep.LangServer/Refactor/ExtractVarAndParam.cs b/src/Bicep.LangServer/Refactor/ExtractVarAndParam.cs new file mode 100644 index 00000000000..78f8e89b14b --- /dev/null +++ b/src/Bicep.LangServer/Refactor/ExtractVarAndParam.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Drawing.Text; +using System.Text; +using System.Text.RegularExpressions; +using Azure.Core.GeoJson; +using Bicep.Core; +using Bicep.Core.CodeAction; +using Bicep.Core.Extensions; +using Bicep.Core.Navigation; +using Bicep.Core.Parsing; +using Bicep.Core.PrettyPrintV2; +using Bicep.Core.Semantics; +using Bicep.Core.Syntax; +using Bicep.Core.Text; +using Bicep.Core.TypeSystem; +using Bicep.Core.TypeSystem.Types; +using Bicep.LanguageServer.CompilationManager; +using Bicep.LanguageServer.Completions; +using Google.Protobuf.WellKnownTypes; +using Grpc.Net.Client.Balancer; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using static Bicep.LanguageServer.Completions.BicepCompletionContext; +using static Bicep.LanguageServer.Refactor.TypeStringifier; +using static Google.Protobuf.Reflection.ExtensionRangeOptions.Types; + +namespace Bicep.LanguageServer.Refactor; + +/*asdfg +* + * A command this code action executes. If a code action + * provides an edit and a command, first the edit is + * executed and then the command. + * + command?: Command; + + +asdfg DecoratorCodeFixProvider +*/ + +// asdfg Convert var to param + +/*asdfg + + * type myMixedTypeArrayType = ('fizz' | 42 | {an: 'object'} | null)[] + * + * + +Nullable-typed parameters may not be assigned default values. They have an implicit default of 'null' that cannot be overridden.bicep(BCP326): + + var commandToExecute = 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1' +resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' { + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') + ] + commandToExecute: commandToExecute + } + } + } + +EXTRACT settings: + + var commandToExecute = 'powershell -ExecutionPolicy Unrestricted -File writeblob.ps1' + +param settings { commandToExecute: string, fileUris: string[] }? = { +fileUris: [ +uri(_artifactsLocation, 'writeblob.ps1${_artifactsLocationSasToken}') +] +commandToExecute: commandToExecute +} +resource testResource 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' { + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.8' + autoUpgradeMinorVersion: true + settings: settings + } + } + +*/ + +// Provides code actions/fixes for a range in a Bicep document +public static class ExtractVarAndParam +{ + private const int MaxExpressionLengthInCodeAction = 35; + private static Regex regexCompactWhitespace = new("\\s+"); + + static string NewLine(SemanticModel semanticModel) => semanticModel.Configuration.Formatting.Data.NewlineKind.ToEscapeSequence(); + + public static IEnumerable<(CodeFix fix, (int line, int character) renamePosition)> GetRefactoringFixes( + CompilationContext compilationContext, + Compilation compilation, + SemanticModel semanticModel, + List nodesInRange) + { + if (SyntaxMatcher.FindLastNodeOfType(nodesInRange) is not (ExpressionSyntax expressionSyntax, _)) + { + yield break; + } + + TypeProperty? typeProperty = null; // asdfg better name + string? defaultNewName = null; + string newLine = NewLine(semanticModel); //asdfg still useful? + + // Pick a semi-intelligent default name for the new param and variable. + // Also, adjust the expression we're replacing if a property itself has been selected. + + if (semanticModel.Binder.GetParent(expressionSyntax) is ObjectPropertySyntax propertySyntax + && propertySyntax.TryGetKeyText() is string propertyName) + { + // `{ objectPropertyName: <> }` // entire property value expression selected + // -> default to the name "objectPropertyName" + defaultNewName = propertyName; + typeProperty = propertySyntax.TryGetTypeProperty(semanticModel); //asdfg testpoint + } + else if (expressionSyntax is ObjectPropertySyntax propertySyntax2 //asdfg rename + && propertySyntax2.TryGetKeyText() is string propertyName2) + { + // `{ <>: expression }` // property itself is selected + // -> default to the name "objectPropertyName" + defaultNewName = propertyName2; + + // The expression we want to replace is the property value, not the property syntax + var propertyValueSyntax = propertySyntax2.Value as ExpressionSyntax; + if (propertyValueSyntax != null) + { + expressionSyntax = propertyValueSyntax; + typeProperty = propertySyntax2.TryGetTypeProperty(semanticModel); //asdfg testpoint + } + else + { + yield break; + } + } + else if (expressionSyntax is PropertyAccessSyntax propertyAccessSyntax) + { + // `object.topPropertyName.propertyName` + // -> default to the name "topPropertyNamePropertyName" + // + // `object.topPropertyName.propertyName` + // -> default to the name "propertyName" + // + // More than two levels is less likely to be desirable + + string lastPartName = propertyAccessSyntax.PropertyName.IdentifierName; + var parent = propertyAccessSyntax.BaseExpression; + string? firstPartName = parent switch + { + PropertyAccessSyntax propertyAccess => propertyAccess.PropertyName.IdentifierName, + VariableAccessSyntax variableAccess => variableAccess.Name.IdentifierName, + FunctionCallSyntax functionCall => functionCall.Name.IdentifierName, + _ => null + }; + + defaultNewName = firstPartName is { } ? firstPartName + lastPartName.UppercaseFirstLetter() : lastPartName; + } + + if (semanticModel.Binder.GetNearestAncestor(expressionSyntax) is not StatementSyntax statementWithExtraction) + { + yield break; + } + + var newVarName = FindUnusedName(compilation, expressionSyntax.Span.Position, defaultNewName ?? "newVariable"); + var newParamName = FindUnusedName(compilation, expressionSyntax.Span.Position, defaultNewName ?? "newParameter"); + + var (newVarInsertionOffset, insertBlankLineBeforeNewVar) = FindOffsetToInsertNewDeclarationOfType(compilationContext, statementWithExtraction.Span.Position); + + //asdfg create CreateExtractParameterCodeFix for var? + var newVarDeclarationSyntax = SyntaxFactory.CreateVariableDeclaration(newVarName, expressionSyntax); + var newVarDeclarationText = PrettyPrinterV2.PrintValid(newVarDeclarationSyntax, PrettyPrinterV2Options.Default) + NewLine(semanticModel); + var identifierOffsetAsdfg = newVarDeclarationText.IndexOf("var " + newVarName); + Debug.Assert(identifierOffsetAsdfg >= 0); + identifierOffsetAsdfg += "var ".Length; + + var newVarDeclaration = new DeclarationASdfg( + newVarDeclarationText, + identifierOffsetAsdfg); + if (insertBlankLineBeforeNewVar) + { + newVarDeclaration.Prepend(newLine); + } + + //asdfg combine with params + var definitionInsertionPositionAsdfg = TextCoordinateConverter.GetPosition(compilationContext.LineStarts, newVarInsertionOffset); + var declarationLineStartsAsdfg = TextCoordinateConverter.GetLineStarts(newVarDeclaration.Text); + var renameRelativePositionAsdfg = TextCoordinateConverter.GetPosition(declarationLineStartsAsdfg, newVarDeclaration.RenameOffset); + var renamePositionAsdfg = definitionInsertionPositionAsdfg with + { + line = definitionInsertionPositionAsdfg.line + renameRelativePositionAsdfg.line, + character = definitionInsertionPositionAsdfg.character + renameRelativePositionAsdfg.character + }; + + var varFix = new CodeFix( //asdfg extract common with params + $"Extract variable", + isPreferred: false, + CodeFixKind.RefactorExtract, + new CodeReplacement(new TextSpan(newVarInsertionOffset, 0), newVarDeclaration.Text), + new CodeReplacement(expressionSyntax.Span, newVarName)); + Debug.Assert(varFix.Replacements.First().Text.Substring(newVarDeclaration.RenameOffset - "var ".Length, "var ".Length) == "var ", "Rename is set at the wrong position"); //asdfg remove these + yield return (varFix, renamePositionAsdfg); + + // For the new param's type, try to use the declared type if there is one (i.e. the type of + // what we're assigning to), otherwise use the actual calculated type of the expression + var inferredType = semanticModel.GetTypeInfo(expressionSyntax); + var declaredType = semanticModel.GetDeclaredType(expressionSyntax); + var newParamType = NullIfErrorOrAny(declaredType) ?? NullIfErrorOrAny(inferredType); + + // Don't create nullable params - they're not allowed to have default values + var ignoreTopLevelNullability = true; + + // Strict typing for the param doesn't appear useful, providing only loose and medium at the moment + var stringifiedNewParamTypeLoose = Stringify(newParamType, typeProperty, Strictness.Loose, ignoreTopLevelNullability); + var stringifiedNewParamTypeMedium = Stringify(newParamType, typeProperty, Strictness.Medium, ignoreTopLevelNullability); + + var multipleParameterTypesAvailable = !string.Equals(stringifiedNewParamTypeLoose, stringifiedNewParamTypeMedium, StringComparison.Ordinal); + + var (newParamInsertionOffset, insertBlankLineBeforeNewParam) = FindOffsetToInsertNewDeclarationOfType(compilationContext, statementWithExtraction.Span.Position); + + var (looseFix, looseRenamePosition) = CreateExtractParameterCodeFix( + multipleParameterTypesAvailable + ? $"Extract parameter of type {GetQuotedText(stringifiedNewParamTypeLoose)}" + : "Extract parameter", + semanticModel, compilationContext.LineStarts, typeProperty, stringifiedNewParamTypeLoose, newParamName, newParamInsertionOffset, expressionSyntax, Strictness.Loose, insertBlankLineBeforeNewParam); + //Debug.Assert(looseFix.Replacements.First().Text.Substring(looseRenameOffset - newParamInsertionOffset - "param ".Length, "param ".Length) == "param ", "Rename is set at the wrong position"); //asdfg remove these + yield return (looseFix, looseRenamePosition); + + if (multipleParameterTypesAvailable) + { + var (mediumFix, mediumRenamePosition) = CreateExtractParameterCodeFix( + $"Extract parameter of type {GetQuotedText(stringifiedNewParamTypeMedium)}", + semanticModel, compilationContext.LineStarts, typeProperty, stringifiedNewParamTypeMedium, newParamName, newParamInsertionOffset, expressionSyntax, Strictness.Medium, insertBlankLineBeforeNewParam); + //Debug.Assert(mediumFix.Replacements.First().Text.Substring(mediumRenameOffset - newParamInsertionOffset - "param ".Length, "param ".Length) == "param ", "Rename is set at the wrong position"); //asdfg remove these + yield return (mediumFix, mediumRenamePosition); + } + } + + private class DeclarationASdfg //asdfg can I do this by creating a syntax tree instead? + { + public string Text { get; private set; } + public int RenameOffset { get; private set; } //asdfg private? + //asdfg public (int line, int character) RenamePosition => TextCoordinateConverter.GetPosition(_lineStarts, RenameOffset); + + public DeclarationASdfg(string declarationText, int renameOffset) + { + Text = declarationText; + RenameOffset = renameOffset; + } + + public void Prepend(string s) + { + Text = s + Text; + RenameOffset += s.Length; + } + } + + private static (CodeFix, (int line, int character) renamePosition)/*asdfg type?*/ CreateExtractParameterCodeFix( + string title, + SemanticModel semanticModel, + ImmutableArray lineStarts, + TypeProperty? typeProperty, + string stringifiedNewParamType, + string newParamName, + int definitionInsertionOffset, + ExpressionSyntax expressionSyntax, + Strictness strictness, + bool blankLineBefore) + { + var declaration = CreateNewParameterDeclaration(semanticModel, typeProperty, stringifiedNewParamType, newParamName, expressionSyntax, strictness); + + if (blankLineBefore) + { + //asdfg move to CreateNewParameterDeclaration? + declaration.Prepend(NewLine(semanticModel)); + } + + //asdfg + var definitionInsertionPosition = TextCoordinateConverter.GetPosition(lineStarts, definitionInsertionOffset); + var declarationLineStarts = TextCoordinateConverter.GetLineStarts(declaration.Text); + var renameRelativePosition = TextCoordinateConverter.GetPosition(declarationLineStarts, declaration.RenameOffset); + var renamePosition = definitionInsertionPosition with + { + line = definitionInsertionPosition.line + renameRelativePosition.line, + character = definitionInsertionPosition.character + renameRelativePosition.character + }; + + var fix = new CodeFix( + title, + isPreferred: false, + CodeFixKind.RefactorExtract, + new CodeReplacement(new TextSpan(definitionInsertionOffset, 0), declaration.Text), + new CodeReplacement(expressionSyntax.Span, newParamName)); + return (fix, renamePosition); + } + + private static DeclarationASdfg CreateNewParameterDeclaration( + SemanticModel semanticModel, + TypeProperty? typeProperty, + string stringifiedNewParamType, + string newParamName, + SyntaxBase defaultValueSyntax, + Strictness strictness) + { + var newParamTypeIdentifier = SyntaxFactory.CreateIdentifierWithTrailingSpace(stringifiedNewParamType); + + var description = typeProperty?.Description; + SyntaxBase[]? leadingNodes = description == null //asdfg extract + ? null + : [ + SyntaxFactory.CreateDecorator( + "description"/*asdfg what about sys.?*/, + SyntaxFactory.CreateStringLiteral(description)), + SyntaxFactory.GetNewlineToken() + ]; + + var paramDeclarationSyntax = SyntaxFactory.CreateParameterDeclaration( + newParamName, + new TypeVariableAccessSyntax(newParamTypeIdentifier), + defaultValueSyntax, + leadingNodes); + var paramDeclarationText = PrettyPrinterV2.PrintValid(paramDeclarationSyntax, PrettyPrinterV2Options.Default) + NewLine(semanticModel); + //asdfg better way to do this? what if param name contains weird characters, requires '@'? + var identifierOffset = paramDeclarationText.IndexOf("param " + newParamName) + "param ".Length; // asdfg what if -1? + return new DeclarationASdfg(paramDeclarationText, identifierOffset); + } + + private static TypeSymbol? NullIfErrorOrAny(TypeSymbol? type) => type is ErrorType || type is AnyType ? null : type; + + private static string FindUnusedName(Compilation compilation, int offset, string preferredName) //asdfg + { + var activeScopes = ActiveScopesVisitor.GetActiveScopes(compilation.GetEntrypointSemanticModel().Root, offset); + for (int i = 1; i < int.MaxValue; ++i) + { + var tryingName = $"{preferredName}{(i < 2 ? "" : i)}"; + if (!activeScopes.Any(s => s.GetDeclarationsByName(tryingName).Any())) + { + preferredName = tryingName; + break; + } + } + + return preferredName; + } + + private static string GetQuotedText(string text) + { + return "\"" + + regexCompactWhitespace.Replace(text, " ") + .TruncateWithEllipses(MaxExpressionLengthInCodeAction) + .Trim() + + "\""; + } + + private static (int offset, bool insertBlankLineBefore) FindOffsetToInsertNewDeclarationOfType(CompilationContext compilationContext, int extractionOffset) + where T : StatementSyntax, ITopLevelNamedDeclarationSyntax + { + //asdfg more testing? Make sure can't crash + ImmutableArray lineStarts = compilationContext.LineStarts; + + var extractionLine = GetPosition(extractionOffset).line; + var startSearchingAtLine = extractionLine - 1; + + for (int line = startSearchingAtLine; line >= 0; --line) + { + var existingDeclarationStatement = StatementOfTypeAtLine(line); + if (existingDeclarationStatement != null) + { + // Insert on the line right after the existing declaration + var insertionLine = line + 1; + + // Is there a blank line above this existing statement that we found (excluding its leading nodes)? + // If so, assume user probably wants one after as well. + var beginningOffsetOfExistingDeclaration = existingDeclarationStatement.Span.Position; + var beginningLineOfExistingDeclaration = + GetPosition(beginningOffsetOfExistingDeclaration) + .line; + var insertBlankLineBeforeNewDeclaration = IsLineBeforeEmpty(beginningLineOfExistingDeclaration); + + return (GetOffset(insertionLine, 0), insertBlankLineBeforeNewDeclaration); + } + } + + // If no existing declarations of the desired type, insert right before the statement/asdfg containing the extraction expression + return (GetOffset(extractionLine, 0), false); + + StatementSyntax? StatementOfTypeAtLine(int line) //asdfg does this work if there's whitespace at the beginning of the line? + { + var lineOffset = GetOffset(line, 0); + var statementAtLine = SyntaxMatcher.FindNodesSpanningRange(compilationContext.ProgramSyntax, lineOffset, lineOffset).OfType().FirstOrDefault(); + return statementAtLine; + } + + bool IsLineBeforeEmpty(int line) + { + if (line == 0) + { + return false; + } + + return IsLineEmpty(line - 1); + } + + bool IsLineEmpty(int line) //asdfg rewrite properly + { + var lineSpan = TextCoordinateConverter.GetLineSpan(lineStarts, compilationContext.ProgramSyntax.GetEndPosition(), line); + for (int i = lineSpan.Position; i <= lineSpan.Position + lineSpan.Length - 1; ++i) + { + // asdfg handle inside other scopes e.g. user functions + if (SyntaxMatcher.FindNodesMatchingOffset(compilationContext.ProgramSyntax, i) + .Where(x => x is not ProgramSyntax) + .Where(x => x is not Token t || !string.IsNullOrWhiteSpace(t.Text)) + .Any()) + { + return false; + } + } + return true; + } + + (int line, int character) GetPosition(int offset) => TextCoordinateConverter.GetPosition(lineStarts, offset); + int GetOffset(int line, int character) => TextCoordinateConverter.GetOffset(lineStarts, line, character); + } +} diff --git a/src/Bicep.LangServer/Refactor/TypeStringifier.cs b/src/Bicep.LangServer/Refactor/TypeStringifier.cs new file mode 100644 index 00000000000..e2deae09f7c --- /dev/null +++ b/src/Bicep.LangServer/Refactor/TypeStringifier.cs @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core; +using Bicep.Core.Parsing; +using Bicep.Core.Syntax; +using Bicep.Core.TypeSystem; +using Bicep.Core.TypeSystem.Types; + +namespace Bicep.LanguageServer.Refactor; + +//asdfg discriminated object type +//asdfg TypeKind.Never +//asdfg experimental resource types + +public static class TypeStringifier +{ + private const string UnknownTypeName = "object? /* unknown */"; + private const string AnyTypeName = "object? /* any */"; + private const string RecursiveTypeName = "object /* recursive */"; + private const string ErrorTypeName = "object /* error */"; + //asdfg private const string NeverTypeName = "object /* never */"; + + public enum Strictness + { + ///

+ /// Create syntax representing the exact type, e.g. `{ p1: 123, p2: 'abc' | 'def' }` + /// + Strict, + + /// + /// Widen literal types when not part of a union, e.g. => `{ p1: int, p2: 'abc' | 'def' }`, empty arrays/objects, tuples, etc, hopefully more in line with user needs + /// + Medium, + + /// + /// Widen everything to basic types only, e.g. => `object` + /// + Loose, + } + + // Note: This is "best effort" code for now. Ideally we should handle this exactly, but Bicep doesn't support expressing all the types it actually supports + // Note: Returns type as a single line + //asdfg consider better solution than ignoreTopLevelNullability, like removing the nullability before passing it in + public static string Stringify(TypeSymbol? type, TypeProperty? typeProperty, Strictness strictness, bool removeTopLevelNullability = false) + { + return StringifyHelper(type, typeProperty, strictness, [], removeTopLevelNullability); + } + + private static string StringifyHelper(TypeSymbol? type, TypeProperty? typeProperty, Strictness strictness, TypeSymbol[] visitedTypes, bool removeTopLevelNullability = false) + { + //asdfg why are we never calling StringifyHelper with a non-null typeProperty during recursion?. Should we? + + // asdfg also check stack depth + if (type == null) + { + return UnknownTypeName; //asdfg test + } + + if (visitedTypes.Contains(type)) + { + return RecursiveTypeName; + } + + TypeSymbol[] previousVisitedTypes = visitedTypes; + visitedTypes = [..previousVisitedTypes, type]; + + type = Widen(type, strictness); + + // If from an object property that is implicitly allowed to be null (like for many resource properties) + if (!removeTopLevelNullability && typeProperty?.Flags.HasFlag(TypePropertyFlags.AllowImplicitNull) == true) + { + // Won't recursive forever because now typeProperty = null + // Note though that because this is by nature recursive with the same type, we must pass in previousVisitedTypes + return StringifyHelper(TypeHelper.MakeNullable(type), null, strictness, previousVisitedTypes); + } + + // Displayable nullable types (always represented as a union type containing "null" as a member") + // as "type?" rather than "type|null" + if (TypeHelper.TryRemoveNullability(type) is TypeSymbol nonNullableType) + { + if (removeTopLevelNullability) + { + return StringifyHelper(nonNullableType, null, strictness, visitedTypes);//asdfg testpoint + } + else + { + return Nullableify(nonNullableType, strictness, visitedTypes);//asdfg testpoint + } + } + + switch (type) + { + // Literal types - keep as is if strict asdfg?? + case StringLiteralType + or IntegerLiteralType + or BooleanLiteralType + when strictness == Strictness.Strict: + return type.Name; + // ... otherwise widen to simple type + case StringLiteralType: + return LanguageConstants.String.Name; + case IntegerLiteralType: + return LanguageConstants.Int.Name; + case BooleanLiteralType: + return LanguageConstants.Bool.Name; + + // Tuple types + case TupleType tupleType: + if (strictness == Strictness.Loose) + { + return LanguageConstants.Array.Name; + } + else if (strictness == Strictness.Medium) + { + var widenedTypes = tupleType.Items.Select(t => Widen(t.Type, strictness)).ToArray(); + var firstItemType = widenedTypes.FirstOrDefault()?.Type; + if (firstItemType == null) + { + // Empty tuple - use "array" to allow items + return LanguageConstants.Array.Name; + } + else if (widenedTypes.All(t => t.Type.Name == firstItemType.Name)) + { + // Bicep infers a tuple type from literals such as "[1, 2]", turn these + // into the more likely intended int[] if all the members are of the same type + return Arrayify(widenedTypes[0], strictness, visitedTypes); + } + } + + return $"[{string.Join(", ", tupleType.Items.Select(tt => StringifyHelper(tt.Type, null, strictness, visitedTypes)))}]"; + + // e.g. "int[]" + case TypedArrayType when strictness == Strictness.Loose: + return LanguageConstants.Array.Name; + case TypedArrayType typedArrayType: + return Arrayify(typedArrayType.Item.Type, strictness, visitedTypes); + + // plain old "array" + case ArrayType: + return LanguageConstants.Array.Name; + + //asdfg // Nullable types are union types with one of the members being the null type TypeHelper.IsNullable asdfg + //UnionType unionType when strictness != Strictness.Loose && TryRemoveNullFromTwoMemberUnion(unionType) is TypeSymbol nullableUnionMember => + // $"{GetSyntaxStringForType(null, nullableUnionMember, strictness)}?", + + case UnionType unionType when strictness == Strictness.Loose: + // Widen to the first non-null member type (which are all supposed to be literal types of the same type) asdfg? + var itemType = Widen(FirstNonNullUnionMember(unionType) ?? unionType.Members.FirstOrDefault()?.Type ?? LanguageConstants.Null, strictness); + if (TypeHelper.IsNullable(unionType)) + { + itemType = TypeHelper.MakeNullable(itemType); //asdfg??? + } + return StringifyHelper(itemType, null, Strictness.Loose, visitedTypes); + + case UnionType: + return type.Name; + + case BooleanType: + return LanguageConstants.Bool.Name; + case IntegerType: + return LanguageConstants.Int.Name; + case StringType: + return LanguageConstants.String.Name; + case NullType: + return LanguageConstants.Null.Name; + + case ObjectType objectType: + if (strictness == Strictness.Loose) + { + return LanguageConstants.Object.Name; + } + // strict: {} with additional properties allowed should be "object" not "{}" + // medium: Bicep infers {} with no allowable members from the literal "{}", the user more likely wants to allow members + else if (objectType.Properties.Count == 0 && + (strictness == Strictness.Medium || !IsObjectLiteral(objectType))) + { + return "object"; + } + + return $"{{ { + string.Join(", ", objectType.Properties + .Where(p => !p.Value.Flags.HasFlag(TypePropertyFlags.ReadOnly)) + .Select(p => GetFormattedTypeProperty(p.Value, strictness, visitedTypes))) + } }}"; + + case AnyType: + return AnyTypeName; + case ErrorType: + return ErrorTypeName; //asdfg test + + // Anything else we don't know about + //asdfg _ => type.Name, //asdfg? + default: + return $"object? /* {type.Name} */"; //asdfg? + }; + } + + private static string Arrayify(TypeSymbol type, Strictness strictness, TypeSymbol[] visitedTypes) + { + string stringifiedType = StringifyHelper(type, null, strictness, visitedTypes); + bool needsParentheses = NeedsParentheses(type, strictness); + + return needsParentheses ? $"({stringifiedType})[]" : $"{stringifiedType}[]"; + } + + private static string Nullableify(TypeSymbol type, Strictness strictness, TypeSymbol[] visitedTypes) + { + string stringifiedType = StringifyHelper(type, null, strictness, visitedTypes); + bool needsParentheses = NeedsParentheses(type, strictness); + + return needsParentheses ? $"({stringifiedType})?" : $"{stringifiedType}?"; + } + + private static bool NeedsParentheses(TypeSymbol type, Strictness strictness) + { + // If the type is '1|2', with loose/medium, we need to check whether 'int' needs parentheses, not '1|2' + // Therefore, widen first + bool needsParentheses = Widen(type, strictness) switch + { + UnionType { Members.Length: > 1 } => true, // also works for nullable types + _ => false + }; + return needsParentheses; + } + + private static TypeSymbol Widen(TypeSymbol type, Strictness strictness) + { + if (strictness == Strictness.Strict) + { + return type; + } + + if (type is UnionType unionType && strictness == Strictness.Loose) + { + // Widen non-null members to a single type (which are all supposed to be literal types of the same type) asdfg + var widenedType = Widen( + FirstNonNullUnionMember(unionType) ?? unionType.Members.FirstOrDefault()?.Type ?? LanguageConstants.Null, + strictness); + if (TypeHelper.IsNullable(unionType)) + { + // If it had "|null" before, add it back + widenedType = TypeHelper.MakeNullable(widenedType); //asdfg??? testpoint + } + return widenedType; + } + + // ... otherwise widen to simple types + return type switch + { + StringLiteralType => LanguageConstants.String, + IntegerLiteralType => LanguageConstants.Int, + BooleanLiteralType => LanguageConstants.Bool, + _ => type, + }; + } + + private static TypeSymbol? FirstNonNullUnionMember(UnionType unionType) => + unionType.Members.FirstOrDefault(m => m.Type is not NullType)?.Type; + + // True if "{}" (which allows no additional properties) instead of "object" + private static bool IsObjectLiteral(ObjectType objectType) + { + return objectType.Properties.Count == 0 && !objectType.HasExplicitAdditionalPropertiesType; + } + + // asdfg?? + //type.AdditionalPropertiesFlags + // type.AdditionalPropertiesType + // type.UnwrapArrayType + + //private static bool IsTypeStringNullable(string typeString) //asdfg + //{ + // var commentsRemoved = new Regex("/\\*[^*/]*\\*/\\s*$").Replace(typeString, ""); + // return commentsRemoved.TrimEnd().EndsWith('?'); + //} + + private static string GetFormattedTypeProperty(TypeProperty property, Strictness strictness, TypeSymbol[] visitedTypes) + { + return + $"{StringUtils.EscapeBicepPropertyName(property.Name)}: {StringifyHelper(property.TypeReference.Type, property, strictness, visitedTypes)}"; + } +} diff --git a/src/vs-bicep/package-lock.json b/src/vs-bicep/package-lock.json new file mode 100644 index 00000000000..03f2d63ab8d --- /dev/null +++ b/src/vs-bicep/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "vs-bicep", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/vscode-bicep/package.json b/src/vscode-bicep/package.json index 30ff0743180..4b8028f7bf3 100644 --- a/src/vscode-bicep/package.json +++ b/src/vscode-bicep/package.json @@ -283,6 +283,11 @@ "title": "(Show a source file from a module)", "category": "Bicep Internal" }, + { + "command": "bicep.internal.startRename", + "title": "(Renme identifier at given location)", + "category": "Bicep Internal" + }, { "$section": "================== Walkthrough commands (not visible to users) ==================", "command": "bicep.gettingStarted.createBicepFile", @@ -627,6 +632,10 @@ { "command": "bicep.internal.showModuleSourceFile", "when": "never" + }, + { + "command": "bicep.internal.startRename", + "when": "never" } ] }, diff --git a/src/vscode-bicep/src/commands/StartRenameCommand.ts b/src/vscode-bicep/src/commands/StartRenameCommand.ts new file mode 100644 index 00000000000..33568983afc --- /dev/null +++ b/src/vscode-bicep/src/commands/StartRenameCommand.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { Uri, Position, commands } from "vscode"; +import { Command } from "./types"; +import { integer } from "vscode-languageclient"; + +export class StartRenameCommand implements Command { + public readonly id = "bicep.internal.startRename"; + + public async execute(_context: IActionContext, _: Uri, targetUri: string, position: {line:integer,character: integer}): Promise { + const uri = Uri.parse(targetUri, true); + await commands.executeCommand("editor.action.rename", [uri, new Position(position.line, position.character)]); + } +} diff --git a/src/vscode-bicep/src/commands/build.ts b/src/vscode-bicep/src/commands/build.ts index 7c34efef03b..d44b516b0ed 100644 --- a/src/vscode-bicep/src/commands/build.ts +++ b/src/vscode-bicep/src/commands/build.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { IActionContext, parseError } from "@microsoft/vscode-azext-utils"; +import { IActionContext } from "@microsoft/vscode-azext-utils"; import vscode from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { OutputChannelManager } from "../utils/OutputChannelManager"; @@ -11,24 +11,31 @@ export class BuildCommand implements Command { public readonly id = "bicep.build"; public constructor( private readonly client: LanguageClient, - private readonly outputChannelManager: OutputChannelManager, + private readonly _outputChannelManager: OutputChannelManager, ) {} public async execute(context: IActionContext, documentUri?: vscode.Uri | undefined): Promise { + let a:object = this.client; + let b: object= this._outputChannelManager; + a = b; + b = a; + documentUri = await findOrCreateActiveBicepFile( context, documentUri, "Choose which Bicep file to build into an ARM template", ); - try { - const buildOutput: string = await this.client.sendRequest("workspace/executeCommand", { - command: "build", - arguments: [documentUri.fsPath], - }); - this.outputChannelManager.appendToOutputChannel(buildOutput); - } catch (err) { - this.client.error("Bicep build failed", parseError(err).message, true); - } + vscode.commands.executeCommand("editor.action.rename", [documentUri.toString(), new vscode.Position(0, 7)]) //works + + // try { + // const buildOutput: string = await this.client.sendRequest("workspace/executeCommand", { + // command: "build", + // arguments: [documentUri.fsPath], + // }); + // this.outputChannelManager.appendToOutputChannel(buildOutput); + // } catch (err) { + // this.client.error("Bicep build failed", parseError(err).message, true); + // } } } diff --git a/src/vscode-bicep/src/extension.ts b/src/vscode-bicep/src/extension.ts index 43b67371ac8..6f142c299ce 100644 --- a/src/vscode-bicep/src/extension.ts +++ b/src/vscode-bicep/src/extension.ts @@ -40,6 +40,7 @@ import { createLogger, getLogger, resetLogger } from "./utils/logger"; import { OutputChannelManager } from "./utils/OutputChannelManager"; import { activateWithTelemetryAndErrorHandling } from "./utils/telemetry"; import { BicepVisualizerViewManager } from "./visualizer"; +import { StartRenameCommand } from "./commands/StartRenameCommand"; let languageClient: lsp.LanguageClient | null = null; @@ -148,6 +149,7 @@ export async function activate(extensionContext: ExtensionContext): Promise { - return await vscode.commands.executeCommand("bicep.build", documentUri); + return await vscode.commands.executeCommand("editor.action.rename", documentUri, new vscode.Position(0, 7)); } export async function executeBuildParamsCommand(documentUri: vscode.Uri): Promise {