diff --git a/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs b/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs index 896625cb8c2..39ba94a605e 100644 --- a/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/StringAssertionsExtensions.cs @@ -85,6 +85,16 @@ public static AndConstraint EqualIgnoringNewlines(this StringA return new AndConstraint(instance); } + public static AndConstraint ContainIgnoringNewlines(this StringAssertions instance, string expected) + { + var normalizedActual = StringUtils.ReplaceNewlines(instance.Subject, "\n"); + var normalizedExpected = StringUtils.ReplaceNewlines(expected, "\n"); + + normalizedActual.Should().Contain(normalizedExpected); + + return new AndConstraint(instance); + } + public static AndConstraint HaveLengthLessThanOrEqualTo(this StringAssertions instance, int maxLength, string because = "", params object[] becauseArgs) { int length = instance.Subject.Length; diff --git a/src/Bicep.LangServer.UnitTests/Handlers/BicepBuildCommandHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Handlers/BicepBuildCommandHandlerTests.cs index 07372a62e83..37780ee8bb5 100644 --- a/src/Bicep.LangServer.UnitTests/Handlers/BicepBuildCommandHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Handlers/BicepBuildCommandHandlerTests.cs @@ -243,5 +243,50 @@ public void TemplateContainsBicepGeneratorMetadata_WithoutBicepGeneratorMetadata Assert.IsFalse(actual); } + + [TestMethod] + public async Task Handle_ShouldPickUp_LoadTextContent_Updates() + { + string testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); + + string sqlFileContents = @"CREATE TABLE regions1 ( + region_id INT IDENTITY(1,1) PRIMARY KEY +);"; + FileHelper.SaveResultFile(TestContext, "test.sql", sqlFileContents, testOutputPath); + + string bicepFileContents = @"var textFromFile = loadTextContent('test.sql')"; + string bicepFilePath = FileHelper.SaveResultFile(TestContext, "input.bicep", bicepFileContents, testOutputPath); + + Uri bicepFileUri = new Uri(bicepFilePath); + DocumentUri documentUri = DocumentUri.From(bicepFileUri); + BicepCompilationManager bicepCompilationManager = BicepCompilationManagerHelper.CreateCompilationManager(documentUri, bicepFileContents, true); + BicepBuildCommandHandler bicepBuildCommandHandler = new BicepBuildCommandHandler(bicepCompilationManager, Repository.Create().Object, BicepTestConstants.FeatureProviderFactory, BicepTestConstants.NamespaceProvider, FileResolver, ModuleDispatcher, BicepTestConstants.ApiVersionProviderFactory, configurationManager, BicepTestConstants.LinterAnalyzer); + + string buildOutputMessage = await bicepBuildCommandHandler.Handle(bicepFilePath, CancellationToken.None); + + string buildOutputFilePath = Path.Combine(testOutputPath, "input.json"); + + VerifyBuildOutputMessageAndContents(buildOutputMessage, File.ReadAllText(buildOutputFilePath), @"""variables"": { + ""textFromFile"": ""CREATE TABLE regions1 (\n region_id INT IDENTITY(1,1) PRIMARY KEY\n);"" + }"); + + // Update test.sql and execute build command + sqlFileContents = @"CREATE TABLE regions2 ( + region_id INT IDENTITY(1,1) PRIMARY KEY +);"; + FileHelper.SaveResultFile(TestContext, "test.sql", sqlFileContents, testOutputPath); + + buildOutputMessage = await bicepBuildCommandHandler.Handle(bicepFilePath, CancellationToken.None); + + VerifyBuildOutputMessageAndContents(buildOutputMessage, File.ReadAllText(buildOutputFilePath), @"""variables"": { + ""textFromFile"": ""CREATE TABLE regions2 (\n region_id INT IDENTITY(1,1) PRIMARY KEY\n);"" + }"); + } + + private void VerifyBuildOutputMessageAndContents(string actualBuildOutputMessage, string buildOutputContents, string expectedText) + { + actualBuildOutputMessage.Should().Be(@"Bicep build succeeded. Created ARM template file: input.json"); + buildOutputContents.Should().ContainIgnoringNewlines(expectedText); + } } } diff --git a/src/Bicep.LangServer.UnitTests/Helpers/CompilationHelperTests.cs b/src/Bicep.LangServer.UnitTests/Helpers/CompilationHelperTests.cs new file mode 100644 index 00000000000..1494f76e929 --- /dev/null +++ b/src/Bicep.LangServer.UnitTests/Helpers/CompilationHelperTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Bicep.Core.Configuration; +using Bicep.Core.FileSystem; +using Bicep.Core.Registry; +using Bicep.Core.UnitTests; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using Bicep.LanguageServer; +using Bicep.LanguageServer.Handlers; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol; +using IOFileSystem = System.IO.Abstractions.FileSystem; +using CompilationHelper = Bicep.LanguageServer.Utils.CompilationHelper; + +namespace Bicep.LangServer.UnitTests.Helpers +{ + [TestClass] + public class CompilationHelperTests + { + [NotNull] + public TestContext? TestContext { get; set; } + + private static readonly FileResolver FileResolver = BicepTestConstants.FileResolver; + private static readonly IConfigurationManager configurationManager = new ConfigurationManager(new IOFileSystem()); + private readonly ModuleDispatcher ModuleDispatcher = new ModuleDispatcher(BicepTestConstants.RegistryProvider, configurationManager); + + [TestMethod] + public void GetCompilation_WithNullCompilationContext_ShouldCreateCompilation() + { + string bicepFileContents = @"resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' = { + name: 'dnsZone' + location: 'global' +}"; + string bicepFilePath = FileHelper.SaveResultFile(TestContext, "input.bicep", bicepFileContents); + DocumentUri documentUri = DocumentUri.FromFileSystemPath(bicepFilePath); + // Do not upsert compilation. This will cause CompilationContext to be null + BicepCompilationManager bicepCompilationManager = BicepCompilationManagerHelper.CreateCompilationManager(documentUri, bicepFileContents, upsertCompilation: false); + var compilationContext = bicepCompilationManager.GetCompilation(documentUri); + + compilationContext.Should().BeNull(); + + var compilation = CompilationHelper.GetCompilation( + documentUri, + documentUri.ToUri(), + BicepTestConstants.ApiVersionProviderFactory, + BicepTestConstants.LinterAnalyzer, + bicepCompilationManager, + configurationManager, + BicepTestConstants.FeatureProviderFactory, + FileResolver, + ModuleDispatcher, + BicepTestConstants.NamespaceProvider); + + compilation.Should().NotBeNull(); + } + + [TestMethod] + public void GetCompilation_WithNonNullCompilationContext_ShouldReuseCompilation() + { + string bicepFileContents = @"resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' = { + name: 'dnsZone' + location: 'global' +}"; + string bicepFilePath = FileHelper.SaveResultFile(TestContext, "input.bicep", bicepFileContents); + DocumentUri documentUri = DocumentUri.FromFileSystemPath(bicepFilePath); + // Upsert compilation. This will cause CompilationContext to be non null + BicepCompilationManager bicepCompilationManager = BicepCompilationManagerHelper.CreateCompilationManager(documentUri, bicepFileContents, upsertCompilation: true); + + var compilation = CompilationHelper.GetCompilation( + documentUri, + documentUri.ToUri(), + BicepTestConstants.ApiVersionProviderFactory, + BicepTestConstants.LinterAnalyzer, + bicepCompilationManager, + configurationManager, + BicepTestConstants.FeatureProviderFactory, + FileResolver, + ModuleDispatcher, + BicepTestConstants.NamespaceProvider); + + compilation.Should().NotBeNull(); + compilation.Should().BeSameAs(bicepCompilationManager.GetCompilation(documentUri)!.Compilation); + } + } +} diff --git a/src/Bicep.LangServer/Handlers/BicepBuildCommandHandler.cs b/src/Bicep.LangServer/Handlers/BicepBuildCommandHandler.cs index bd5eed416da..32d30f763a0 100644 --- a/src/Bicep.LangServer/Handlers/BicepBuildCommandHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepBuildCommandHandler.cs @@ -83,18 +83,17 @@ private string GenerateCompiledFileAndReturnBuildOutputMessage(string bicepFileP var fileUri = documentUri.ToUri(); - CompilationContext? context = compilationManager.GetCompilation(fileUri); - Compilation compilation; - - if (context is null) - { - SourceFileGrouping sourceFileGrouping = SourceFileGroupingBuilder.Build(this.fileResolver, this.moduleDispatcher, new Workspace(), fileUri); - compilation = new Compilation(featureProviderFactory, namespaceProvider, sourceFileGrouping, configurationManager, apiVersionProviderFactory, bicepAnalyzer); - } - else - { - compilation = context.Compilation; - } + var compilation = CompilationHelper.GetCompilation( + documentUri, + fileUri, + apiVersionProviderFactory, + bicepAnalyzer, + compilationManager, + configurationManager, + featureProviderFactory, + fileResolver, + moduleDispatcher, + namespaceProvider); var diagnosticsByFile = compilation.GetAllDiagnosticsByBicepFile() .FirstOrDefault(x => x.Key.FileUri == fileUri); diff --git a/src/Bicep.LangServer/Handlers/BicepDeploymentScopeRequestHandler.cs b/src/Bicep.LangServer/Handlers/BicepDeploymentScopeRequestHandler.cs index aaffb6c7012..68a468d84c3 100644 --- a/src/Bicep.LangServer/Handlers/BicepDeploymentScopeRequestHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDeploymentScopeRequestHandler.cs @@ -84,7 +84,17 @@ public override Task Handle(BicepDeploymentScopePa try { - compilation = GetCompilation(documentUri); + compilation = CompilationHelper.GetCompilation( + documentUri, + documentUri.ToUri(), + apiVersionProviderFactory, + bicepAnalyzer, + compilationManager, + configurationManager, + featureProviderFactory, + fileResolver, + moduleDispatcher, + namespaceProvider); // Cache the compilation so that it can be reused by BicepDeploymentParametersHandler deploymentFileCompilationCache.CacheCompilation(documentUri, compilation); @@ -129,21 +139,5 @@ private string GetCompiledFile(Compilation compilation, DocumentUri documentUri) return stringBuilder.ToString(); } - - private Compilation GetCompilation(DocumentUri documentUri) - { - var fileUri = documentUri.ToUri(); - - CompilationContext? context = compilationManager.GetCompilation(documentUri); - if (context is null) - { - SourceFileGrouping sourceFileGrouping = SourceFileGroupingBuilder.Build(this.fileResolver, this.moduleDispatcher, new Workspace(), fileUri); - return new Compilation(featureProviderFactory, namespaceProvider, sourceFileGrouping, configurationManager, apiVersionProviderFactory, bicepAnalyzer); - } - else - { - return context.Compilation; - } - } } } diff --git a/src/Bicep.LangServer/Utils/CompilationHelper.cs b/src/Bicep.LangServer/Utils/CompilationHelper.cs new file mode 100644 index 00000000000..9f3dc27170a --- /dev/null +++ b/src/Bicep.LangServer/Utils/CompilationHelper.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Bicep.Core.Analyzers.Interfaces; +using Bicep.Core.Analyzers.Linter.ApiVersions; +using Bicep.Core.Configuration; +using Bicep.Core.Features; +using Bicep.Core.FileSystem; +using Bicep.Core.Registry; +using Bicep.Core.Semantics; +using Bicep.Core.Semantics.Namespaces; +using Bicep.Core.Workspaces; +using Bicep.LanguageServer.CompilationManager; +using OmniSharp.Extensions.LanguageServer.Protocol; + +namespace Bicep.LanguageServer.Utils +{ + public static class CompilationHelper + { + public static Compilation GetCompilation( + DocumentUri documentUri, + Uri fileUri, + IApiVersionProviderFactory apiVersionProviderFactory, + IBicepAnalyzer bicepAnalyzer, + ICompilationManager compilationManager, + IConfigurationManager configurationManager, + IFeatureProviderFactory featureProviderFactory, + IFileResolver fileResolver, + IModuleDispatcher moduleDispatcher, + INamespaceProvider namespaceProvider) + { + // Bicep file could contain load functions like loadTextContent(..). We'll refresh compilation to detect changes in files referenced in load functions. + compilationManager.RefreshCompilation(fileUri); + CompilationContext? context = compilationManager.GetCompilation(documentUri); + // CompilationContext will be null if the file is not open in the editor. + // E.g. When user right clicks on a file from the explorer context menu without opening the file and invokes build/deploy + if (context is null) + { + SourceFileGrouping sourceFileGrouping = SourceFileGroupingBuilder.Build(fileResolver, moduleDispatcher, new Workspace(), fileUri); + return new Compilation(featureProviderFactory, namespaceProvider, sourceFileGrouping, configurationManager, apiVersionProviderFactory, bicepAnalyzer); + } + else + { + return context.Compilation; + } + } + } +}