From 37da17fa53e7ba0c33967fad8332dc365effa62a Mon Sep 17 00:00:00 2001 From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:58:49 -0400 Subject: [PATCH] Add support for version in bicepconfig.json --- src/Bicep.Cli/Commands/RootCommand.cs | 7 ++- src/Bicep.Cli/Rpc/CliJsonRpcServer.cs | 3 +- .../BicepConfigTests.cs | 51 +++++++++++++++++++ .../Configuration/RootConfigurationTests.cs | 1 + .../UseRecentApiVersionRuleTests.cs | 1 + .../Utils/BicepVersionTests.cs | 22 ++++++++ .../AnalyzersConfigurationExtensions.cs | 1 + .../BicepConfigurationSection.cs | 21 ++++++++ .../Configuration/ConfigurationManager.cs | 1 + .../ExperimentalFeaturesExtensions.cs | 1 + .../ExtensionsConfigurationExtensions.cs | 2 + .../Configuration/RootConfiguration.cs | 12 ++++- src/Bicep.Core/Configuration/bicepconfig.json | 1 + .../Diagnostics/DiagnosticBuilder.cs | 7 +++ .../Emit/EmitLimitationCalculator.cs | 13 ++++- src/Bicep.Core/Utils/BicepVersion.cs | 19 +++++++ src/Bicep.Core/Utils/Environment.cs | 2 +- .../schemas/bicepconfig.schema.json | 11 ++++ 18 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 src/Bicep.Core.IntegrationTests/BicepConfigTests.cs create mode 100644 src/Bicep.Core.UnitTests/Utils/BicepVersionTests.cs create mode 100644 src/Bicep.Core/Configuration/BicepConfigurationSection.cs create mode 100644 src/Bicep.Core/Utils/BicepVersion.cs diff --git a/src/Bicep.Cli/Commands/RootCommand.cs b/src/Bicep.Cli/Commands/RootCommand.cs index 26303b5ec83..a84bc7e8d9d 100644 --- a/src/Bicep.Cli/Commands/RootCommand.cs +++ b/src/Bicep.Cli/Commands/RootCommand.cs @@ -4,6 +4,8 @@ using System.IO.Compression; using Bicep.Cli.Arguments; using Bicep.Core.Exceptions; +using Bicep.Core.Utils; +using Environment = System.Environment; namespace Bicep.Cli.Commands { @@ -265,10 +267,11 @@ private void PrintThirdPartyNotices() private static string GetVersionString() { - var versionSplit = ThisAssembly.AssemblyInformationalVersion.Split('+'); + var version = BicepVersion.Instance.Value; + var commitHash = BicepVersion.Instance.CommitHash; // .. () - return $"{versionSplit[0]} ({(versionSplit.Length > 1 ? versionSplit[1] : "custom")})"; + return $"{version} ({(commitHash is {} ? commitHash : "custom")})"; } private static void WriteEmbeddedResource(TextWriter writer, string streamName) diff --git a/src/Bicep.Cli/Rpc/CliJsonRpcServer.cs b/src/Bicep.Cli/Rpc/CliJsonRpcServer.cs index c6da811f873..4f4ff65f93b 100644 --- a/src/Bicep.Cli/Rpc/CliJsonRpcServer.cs +++ b/src/Bicep.Cli/Rpc/CliJsonRpcServer.cs @@ -13,6 +13,7 @@ using Bicep.Core.Syntax; using Bicep.Core.Text; using Bicep.Core.TypeSystem; +using Bicep.Core.Utils; using Bicep.Core.Workspaces; using Newtonsoft.Json.Serialization; using StreamJsonRpc; @@ -42,7 +43,7 @@ public async Task Version(VersionRequest request, CancellationT await Task.Yield(); return new( - ThisAssembly.AssemblyInformationalVersion.Split('+')[0]); + BicepVersion.Instance.Value); } /// diff --git a/src/Bicep.Core.IntegrationTests/BicepConfigTests.cs b/src/Bicep.Core.IntegrationTests/BicepConfigTests.cs new file mode 100644 index 00000000000..9395b7a0059 --- /dev/null +++ b/src/Bicep.Core.IntegrationTests/BicepConfigTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using Bicep.Core.Utils; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.IntegrationTests; + +[TestClass] +public class BicepConfigTests +{ + [TestMethod] + public async Task Version_mismatch_should_block_compilation() + { + var result = await CompilationHelper.RestoreAndCompile( + ("main.bicep", new(""" +output foo string = '' +""")), ("../bicepconfig.json", new(""" +{ + "bicep": { + "version": "0.0.1" + } +} +"""))); + + result.ExcludingLinterDiagnostics().Should().HaveDiagnostics([ + ("BCP409", DiagnosticLevel.Error, $"""Bicep version "{BicepVersion.Instance.Value}" was used for compilation, but version "0.0.1" is required in configuration file "/path/bicepconfig.json"."""), + ]); + } + + [TestMethod] + public async Task Correct_version_should_permit_compilation() + { + var result = await CompilationHelper.RestoreAndCompile( + ("main.bicep", new(""" +output foo string = '' +""")), ("../bicepconfig.json", new($$""" +{ + "bicep": { + "version": "{{BicepVersion.Instance.Value}}" + } +} +"""))); + + result.Should().NotHaveAnyDiagnostics(); + } +} diff --git a/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs b/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs index ce3efa0375f..4e545d5e994 100644 --- a/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs @@ -23,6 +23,7 @@ public void RootConfiguration_LeadingTildeInCacheRootDirectory_ExpandPath(string cacheRootDirectory, BicepTestConstants.BuiltInConfiguration.ExperimentalFeaturesEnabled, BicepTestConstants.BuiltInConfiguration.Formatting, + BicepTestConstants.BuiltInConfiguration.Bicep, BicepTestConstants.BuiltInConfiguration.ConfigFileUri, BicepTestConstants.BuiltInConfiguration.DiagnosticBuilders); diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs index 97d76b6c7ba..1d052a713b9 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs @@ -130,6 +130,7 @@ original.ExperimentalFeaturesEnabled with Extensibility = true, }, original.Formatting, + original.Bicep, null, null); } diff --git a/src/Bicep.Core.UnitTests/Utils/BicepVersionTests.cs b/src/Bicep.Core.UnitTests/Utils/BicepVersionTests.cs new file mode 100644 index 00000000000..c06269ece90 --- /dev/null +++ b/src/Bicep.Core.UnitTests/Utils/BicepVersionTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Bicep.Core.Utils; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.UnitTests.Utils; + +[TestClass] +public class BicepVersionTests +{ + [TestMethod] + public void BicepVersion_should_return_assembly_info() + { + var result = BicepVersion.Instance; + result.Value.Should().MatchRegex(@"^[0-9]+\.[0-9]+\.[0-9]+"); + if (result.CommitHash is {}) + { + result.CommitHash.Should().MatchRegex(@"^[0-9a-f]{7,40}$"); + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs index 930ebed8e69..eb279f954eb 100644 --- a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs @@ -46,6 +46,7 @@ public static RootConfiguration WithAnalyzersConfiguration(this RootConfiguratio current.CacheRootDirectory, current.ExperimentalFeaturesEnabled, current.Formatting, + current.Bicep, current.ConfigFileUri, current.DiagnosticBuilders); diff --git a/src/Bicep.Core/Configuration/BicepConfigurationSection.cs b/src/Bicep.Core/Configuration/BicepConfigurationSection.cs new file mode 100644 index 00000000000..106b36780a4 --- /dev/null +++ b/src/Bicep.Core/Configuration/BicepConfigurationSection.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Bicep.Core.Extensions; + +namespace Bicep.Core.Configuration; + +public record BicepConfiguration( + string? Version); + +public class BicepConfigurationSection : ConfigurationSection +{ + public BicepConfigurationSection(BicepConfiguration data) + : base(data) + { + } + + public static BicepConfigurationSection Bind(JsonElement element) + => new(element.ToNonNullObject()); +} diff --git a/src/Bicep.Core/Configuration/ConfigurationManager.cs b/src/Bicep.Core/Configuration/ConfigurationManager.cs index b92bee92f36..4b6ac466e58 100644 --- a/src/Bicep.Core/Configuration/ConfigurationManager.cs +++ b/src/Bicep.Core/Configuration/ConfigurationManager.cs @@ -102,6 +102,7 @@ private static RootConfiguration WithLoadDiagnostics(RootConfiguration configura configuration.CacheRootDirectory, configuration.ExperimentalFeaturesEnabled, configuration.Formatting, + configuration.Bicep, configuration.ConfigFileUri, diagnostics); } diff --git a/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs b/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs index fb0c6a4f369..11117af10bf 100644 --- a/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs +++ b/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs @@ -15,6 +15,7 @@ public static RootConfiguration WithExperimentalFeaturesConfiguration(this RootC current.CacheRootDirectory, featuresEnabled, current.Formatting, + current.Bicep, current.ConfigFileUri, current.DiagnosticBuilders); diff --git a/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs b/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs index b22fbea55bb..9b8f6079f5b 100644 --- a/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs @@ -24,6 +24,7 @@ public static RootConfiguration WithExtensions(this RootConfiguration rootConfig rootConfiguration.CacheRootDirectory, rootConfiguration.ExperimentalFeaturesEnabled, rootConfiguration.Formatting, + rootConfiguration.Bicep, rootConfiguration.ConfigFileUri, rootConfiguration.DiagnosticBuilders); } @@ -39,6 +40,7 @@ public static RootConfiguration WithImplicitExtensions(this RootConfiguration ro rootConfiguration.CacheRootDirectory, rootConfiguration.ExperimentalFeaturesEnabled, rootConfiguration.Formatting, + rootConfiguration.Bicep, rootConfiguration.ConfigFileUri, rootConfiguration.DiagnosticBuilders); } diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index f44bb89f4a9..8413e175231 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -28,6 +28,8 @@ public class RootConfiguration public const string FormattingKey = "formatting"; + public const string BicepKey = "bicep"; + public RootConfiguration( CloudConfiguration cloud, ModuleAliasesConfiguration moduleAliases, @@ -37,6 +39,7 @@ public RootConfiguration( string? cacheRootDirectory, ExperimentalFeaturesEnabled experimentalFeaturesEnabled, FormattingConfiguration formatting, + BicepConfigurationSection bicep, Uri? configFileUri, IEnumerable? diagnosticBuilders) { @@ -48,6 +51,7 @@ public RootConfiguration( this.CacheRootDirectory = ExpandCacheRootDirectory(cacheRootDirectory); this.ExperimentalFeaturesEnabled = experimentalFeaturesEnabled; this.Formatting = formatting; + this.Bicep = bicep; this.ConfigFileUri = configFileUri; this.DiagnosticBuilders = diagnosticBuilders?.ToImmutableArray() ?? []; } @@ -60,11 +64,12 @@ public static RootConfiguration Bind(JsonElement element, Uri? configFileUri = n var cacheRootDirectory = element.TryGetProperty(CacheRootDirectoryKey, out var e) ? e.GetString() : default; var experimentalFeaturesEnabled = ExperimentalFeaturesEnabled.Bind(element.GetProperty(ExperimentalFeaturesEnabledKey)); var formatting = FormattingConfiguration.Bind(element.GetProperty(FormattingKey)); + var bicep = BicepConfigurationSection.Bind(element.GetProperty(BicepKey)); var extensions = ExtensionsConfiguration.Bind(element.GetProperty(ExtensionsKey)); var implicitExtensions = ImplicitExtensionsConfiguration.Bind(element.GetProperty(ImplicitExtensionsKey)); - return new(cloud, moduleAliases, extensions, implicitExtensions, analyzers, cacheRootDirectory, experimentalFeaturesEnabled, formatting, configFileUri, diagnosticBuilders); + return new(cloud, moduleAliases, extensions, implicitExtensions, analyzers, cacheRootDirectory, experimentalFeaturesEnabled, formatting, bicep, configFileUri, diagnosticBuilders); } public CloudConfiguration Cloud { get; } @@ -83,6 +88,8 @@ public static RootConfiguration Bind(JsonElement element, Uri? configFileUri = n public FormattingConfiguration Formatting { get; } + public BicepConfigurationSection Bicep { get; } + public Uri? ConfigFileUri { get; } public ImmutableArray DiagnosticBuilders { get; } @@ -122,6 +129,9 @@ public string ToUtf8Json() writer.WritePropertyName(FormattingKey); this.Formatting.WriteTo(writer); + writer.WritePropertyName(BicepKey); + this.Bicep.WriteTo(writer); + writer.WriteEndObject(); } diff --git a/src/Bicep.Core/Configuration/bicepconfig.json b/src/Bicep.Core/Configuration/bicepconfig.json index eb32c0a108b..77ac475de72 100644 --- a/src/Bicep.Core/Configuration/bicepconfig.json +++ b/src/Bicep.Core/Configuration/bicepconfig.json @@ -2,6 +2,7 @@ // This is the base configuration which provides the defaults for all values (end users don't see this file). // Intellisense for bicepconfig.json is controlled by src/vscode-bicep/schemas/bicepconfig.schema.json + "bicep": {}, "cloud": { "currentProfile": "AzureCloud", "profiles": { diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 740d86bfe91..b21abd8a989 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -1820,6 +1820,13 @@ public Diagnostic MicrosoftGraphBuiltinDeprecatedSoon(ExtensionDeclarationSyntax public Diagnostic NameofInvalidOnUnnamedExpression() => CoreError( "BCP408", $"The \"{LanguageConstants.NameofFunctionName}\" function can only be used with an expression which has a name."); + + public Diagnostic InvalidBicepVersion(string? configFilePath, string expectedVersion, string actualVersion) => CoreError( + "BCP409", + configFilePath switch { + {} => $"""Bicep version "{actualVersion}" was used for compilation, but version "{expectedVersion}" is required in configuration file "{configFilePath}".""", + _ => $"""Bicep version "{actualVersion}" was used for compilation, but version "{expectedVersion}" is required.""", + }); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs index 53e70215730..15fb1af95e0 100644 --- a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs +++ b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs @@ -37,7 +37,8 @@ public static EmitLimitationInfo Calculate(SemanticModel model) ForSyntaxValidatorVisitor.Validate(model, diagnostics); FunctionPlacementValidatorVisitor.Validate(model, diagnostics); IntegerValidatorVisitor.Validate(model, diagnostics); - + + BlockIncorrectBicepVersion(model, diagnostics); DetectDuplicateNames(model, diagnostics, resourceScopeData, moduleScopeData); DetectIncorrectlyFormattedNames(model, diagnostics); DetectUnexpectedResourceLoopInvariantProperties(model, diagnostics); @@ -61,6 +62,16 @@ public static EmitLimitationInfo Calculate(SemanticModel model) return new(diagnostics.GetDiagnostics(), moduleScopeData, resourceScopeData, paramAssignments); } + private static void BlockIncorrectBicepVersion(SemanticModel model, IDiagnosticWriter diagnostics) + { + var actualVersion = ThisAssembly.AssemblyInformationalVersion.Split('+')[0]; + if (model.Configuration.Bicep.Data.Version is {} expectedVersion && + expectedVersion != actualVersion) + { + diagnostics.Write(TextSpan.TextDocumentStart, x => x.InvalidBicepVersion(model.Configuration.ConfigFileUri?.LocalPath, expectedVersion, actualVersion)); + } + } + private static void DetectDuplicateNames(SemanticModel semanticModel, IDiagnosticWriter diagnosticWriter, ImmutableDictionary resourceScopeData, ImmutableDictionary moduleScopeData) { // TODO generalize or move into Az extension diff --git a/src/Bicep.Core/Utils/BicepVersion.cs b/src/Bicep.Core/Utils/BicepVersion.cs new file mode 100644 index 00000000000..a93b56dd63f --- /dev/null +++ b/src/Bicep.Core/Utils/BicepVersion.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +namespace Bicep.Core.Utils; + +public record BicepVersion( + string Value, + string? CommitHash) +{ + public static readonly BicepVersion Instance = GetVersion(); + + private static BicepVersion GetVersion() + { + var split = ThisAssembly.AssemblyInformationalVersion.Split('+'); + + return new( + split[0], + split.Length > 1 ? split[1] : null); + } +} diff --git a/src/Bicep.Core/Utils/Environment.cs b/src/Bicep.Core/Utils/Environment.cs index b82de26fa5e..13fc977ae6a 100644 --- a/src/Bicep.Core/Utils/Environment.cs +++ b/src/Bicep.Core/Utils/Environment.cs @@ -9,4 +9,4 @@ public class Environment : IEnvironment public IEnumerable GetVariableNames() => System.Environment.GetEnvironmentVariables().Keys.OfType(); -} +} \ No newline at end of file diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index e246e3df374..5f8a887e8e3 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -767,6 +767,17 @@ } } }, + "bicep": { + "type": "object", + "description": "Bicep configuration", + "properties": { + "version": { + "type": "string", + "format": "[0-9]+\\.[0-9]+\\.[0-9]+", + "description": "The version of Bicep to require for compilation. If this is specified and not met, compilation will fail." + } + } + }, "cacheRootDirectory": { "type": "string", "description": "The directory in which Bicep may cache templates downloaded from remote registries"