From 383d447b72c07c81ef0615f200d3d777c3b6b905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Wed, 15 Apr 2026 07:38:26 +0200 Subject: [PATCH 01/21] Add support for fileSystem based br/ module aliases --- .../OciArtifactModuleReferenceTests.cs | 4 +- .../OciArtifactEmulatedReferenceTests.cs | 242 ++++++++++++++++++ .../Utils/OciRegistryHelper.cs | 2 +- .../ModuleAliasesConfiguration.cs | 17 +- .../Diagnostics/DiagnosticBuilder.cs | 12 +- .../Modules/ModuleReferenceSchemes.cs | 2 + .../DefaultArtifactRegistryProvider.cs | 4 +- .../Registry/FileSystemModuleRegistry.cs | 47 ++++ .../Oci/OciArtifactEmulatedReference.cs | 125 +++++++++ .../Registry/Oci/OciArtifactReferenceFacts.cs | 3 + .../Registry/OciArtifactRegistry.cs | 35 ++- 11 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs create mode 100644 src/Bicep.Core/Registry/FileSystemModuleRegistry.cs create mode 100644 src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs diff --git a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs index e61f25f32b9..8bc76199d80 100644 --- a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs @@ -277,7 +277,7 @@ private static IEnumerable GetInvalidAliasData() ["moduleAliases.br.myModulePath.modulePath"] = "path", }), "BCP216", - "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. The \"registry\" property cannot be null or undefined.", + "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", }; yield return new object[] @@ -291,7 +291,7 @@ private static IEnumerable GetInvalidAliasData() }, "/bicepconfig.json"), "BCP216", - "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. The \"registry\" property cannot be null or undefined.", + "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", }; } diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs new file mode 100644 index 00000000000..6f4f4a6e2fb --- /dev/null +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Abstractions.TestingHelpers; +using Bicep.Core.Configuration; +using Bicep.Core.Diagnostics; +using Bicep.Core.Registry; +using Bicep.Core.Registry.Oci; +using Bicep.Core.SourceGraph; +using Bicep.Core.UnitTests.Assertions; +using Bicep.IO.Abstraction; +using Bicep.IO.FileSystem; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.UnitTests.Registry.Oci +{ + [TestClass] + public class OciArtifactEmulatedReferenceTests + { + [TestMethod] + [DataRow("keyvault:1.0.0", "keyvault")] + [DataRow("storage/queue:v2.0", "storage/queue")] + [DataRow("keyvault@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4", "keyvault")] + [DataRow("keyvault", "keyvault")] + [DataRow("a/b/c:latest", "a/b/c")] + [DataRow("mymodule:v1", "mymodule")] + [DataRow("", "")] + [DataRow(":", "")] + [DataRow("@sha256:abc", "")] + public void ExtractModulePath_ShouldExtractCorrectPath(string input, string expected) + { + OciArtifactEmulatedReference.ExtractModulePath(input).Should().Be(expected); + } + + [TestMethod] + public void TryParse_EmptyModulePath_ShouldFail() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + ":1.0.0", + fileExplorer); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP090"); + } + + [TestMethod] + public void TryParse_ValidModulePath_ShouldSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "../bicepModules", + configFileUri, + "keyvault:1.0.0", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("keyvault"); + reference!.FullyQualifiedReference.Should().Be("br:keyvault"); + reference!.IsExternal.Should().BeFalse(); + reference.Scheme.Should().Be("br-fs"); + } + + [TestMethod] + public void TryParse_MultiSegmentModulePath_ShouldSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + "storage/queue:v2.0", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("storage/queue"); + reference!.FullyQualifiedReference.Should().Be("br:storage/queue"); + } + + [TestMethod] + public void TryParse_DigestReference_ShouldIgnoreDigestAndSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + "keyvault@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("keyvault"); + reference!.FullyQualifiedReference.Should().Be("br:keyvault"); + } + + [TestMethod] + public void TryGetEntryPointFileHandle_ShouldReturnFileHandle() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var parseResult = OciArtifactEmulatedReference.TryParse( + referencingFile, + "../bicepModules", + configFileUri, + "keyvault:1.0.0", + fileExplorer); + + parseResult.IsSuccess(out var reference, out _).Should().BeTrue(); + + var entryPointResult = reference!.TryGetEntryPointFileHandle(); + + entryPointResult.IsSuccess(out var fileHandle, out _).Should().BeTrue(); + fileHandle.Should().NotBeNull(); + fileHandle!.Uri.Path.Should().Contain("keyvault.bicep"); + } + + [TestMethod] + public void Equals_SameReferences_ShouldBeEqual() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result1 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:2.0.0", fileExplorer); + + result1.IsSuccess(out var ref1, out _).Should().BeTrue(); + result2.IsSuccess(out var ref2, out _).Should().BeTrue(); + + ref1!.Equals(ref2).Should().BeTrue(); + ref1.GetHashCode().Should().Be(ref2!.GetHashCode()); + } + + [TestMethod] + public void Equals_DifferentModulePaths_ShouldNotBeEqual() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result1 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "storage:1.0.0", fileExplorer); + + result1.IsSuccess(out var ref1, out _).Should().BeTrue(); + result2.IsSuccess(out var ref2, out _).Should().BeTrue(); + + ref1!.Equals(ref2).Should().BeFalse(); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_BothRegistryAndFileSystemSet_ShouldFail() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", + ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP446"); + diagnostic.Message.Should().Contain("mutually exclusive"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_NeitherRegistryNorFileSystemSet_ShouldFail() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.modulePath"] = "path", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP216"); + diagnostic.Message.Should().Contain("fileSystem"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_OnlyFileSystemSet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.FileSystem.Should().Be("../bicepModules"); + alias.Registry.Should().BeNull(); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_OnlyRegistrySet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.Registry.Should().Be("example.azurecr.io"); + alias.FileSystem.Should().BeNull(); + } + } +} diff --git a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs index 82c024b84dd..4460e30fd00 100644 --- a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs @@ -77,7 +77,7 @@ public static (OciArtifactRegistry, FakeRegistryBlobClient) CreateModuleRegistry .Setup(m => m.CreateAuthenticatedBlobClient(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(blobClient); - var registry = new OciArtifactRegistry(clientFactory.Object, StrictMock.Of().Object); + var registry = new OciArtifactRegistry(clientFactory.Object, StrictMock.Of().Object, new FileSystemFileExplorer(new MockFileSystem())); return (registry, blobClient); } diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index 1e3f30ba77a..53c35c7c37b 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -37,9 +37,13 @@ public record OciArtifactModuleAlias public string? ModulePath { get; init; } - public override string ToString() => this.ModulePath is not null - ? $"{Registry}/{ModulePath}" - : $"{Registry}"; + public string? FileSystem { get; init; } + + public override string ToString() => this.FileSystem is not null + ? $"{FileSystem}" + : this.ModulePath is not null + ? $"{Registry}/{ModulePath}" + : $"{Registry}"; } public partial class ModuleAliasesConfiguration : ConfigurationSection @@ -101,7 +105,12 @@ public ResultWithDiagnosticBuilder TryGetOciArtifactModu return new(x => x.OciArtifactModuleAliasNameDoesNotExistInConfiguration(aliasName, configFileUri)); } - if (alias.Registry is null) + if (alias.Registry is not null && alias.FileSystem is not null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(aliasName, configFileUri)); + } + + if (alias.Registry is null && alias.FileSystem is null) { return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, configFileUri)); } diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 999aba6eb7b..b64adee4be6 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -991,7 +991,7 @@ public Diagnostic ReferencedArmTemplateHasErrors() => CoreError( public Diagnostic UnknownModuleReferenceScheme(string badScheme, ImmutableArray allowedSchemes) { - string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local))); + string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local) && scheme != ArtifactReferenceSchemes.OciEmulated)); return CoreError( "BCP189", @@ -1111,7 +1111,7 @@ public Diagnostic InvalidTemplateSpecAliasResourceGroupNullOrUndefined(string al public Diagnostic InvalidOciArtifactModuleAliasRegistryNullOrUndefined(string aliasName, IOUri? configFileUri) => CoreError( "BCP216", - $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" property cannot be null or undefined."); + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. Either the \"registry\" or \"fileSystem\" property must be specified."); public Diagnostic InvalidTemplateSpecReferenceInvalidSubscriptionId(string? aliasName, string subscriptionId, string referenceValue) => CoreError( "BCP217", @@ -2026,6 +2026,14 @@ public Diagnostic RuntimeValueNotAllowedInExtensionDeclarationWithClause(string? public Diagnostic NullIfNotFoundOnlyValidOnExistingResources() => CoreError( "BCP445", $@"The ""@{LanguageConstants.NullIfNotFoundDecoratorName}()"" decorator can only be used on existing resources."); + + public Diagnostic InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(string aliasName, IOUri? configFileUri) => CoreError( + "BCP446", + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" and \"fileSystem\" properties are mutually exclusive."); + + public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string aliasName) => CoreError( + "BCP447", + $"The OCI artifact module alias \"{aliasName}\" has a \"fileSystem\" property which is only supported for modules, not extensions."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs index 75d3f813f1a..b399a229042 100644 --- a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs +++ b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs @@ -11,6 +11,8 @@ public static class ArtifactReferenceSchemes public const string Oci = OciArtifactReferenceFacts.Scheme; + public const string OciEmulated = OciArtifactReferenceFacts.EmulatedScheme; + public const string TemplateSpecs = "ts"; } } diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index a1bcf5268a6..70d0f76d274 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Bicep.Core.Registry.Catalog; +using Bicep.IO.Abstraction; using Microsoft.Extensions.DependencyInjection; namespace Bicep.Core.Registry @@ -12,7 +13,8 @@ public DefaultArtifactRegistryProvider(IServiceProvider serviceProvider, IContai : base(new IArtifactRegistry[] { new LocalModuleRegistry(), - new OciArtifactRegistry(clientFactory, serviceProvider.GetRequiredService()), + new OciArtifactRegistry(clientFactory, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()), + new FileSystemModuleRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs b/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs new file mode 100644 index 00000000000..b456468b745 --- /dev/null +++ b/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.Modules; +using Bicep.Core.Registry.Oci; +using Bicep.Core.Semantics; +using Bicep.Core.SourceGraph; +using Bicep.Core.SourceLink; + +namespace Bicep.Core.Registry +{ + public class FileSystemModuleRegistry : ArtifactRegistry + { + public override string Scheme => ArtifactReferenceSchemes.OciEmulated; + + public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, OciArtifactEmulatedReference reference) + => RegistryCapabilities.Default; + + public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) + => throw new NotSupportedException("Parsing is handled by OciArtifactRegistry."); + + public override bool IsArtifactRestoreRequired(OciArtifactEmulatedReference reference) => false; + + public override Task CheckArtifactExists(ArtifactType artifactType, OciArtifactEmulatedReference reference) + => Task.FromResult(reference.TryGetEntryPointFileHandle().IsSuccess(out var fileHandle, out _) && fileHandle.Exists()); + + public override Task> RestoreArtifacts(IEnumerable references) + => Task.FromResult>( + new Dictionary()); + + public override Task> InvalidateArtifactsCache(IEnumerable references) + => Task.FromResult>( + new Dictionary()); + + public override Task PublishModule(OciArtifactEmulatedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) + => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + + public override Task PublishExtension(OciArtifactEmulatedReference reference, ExtensionPackage package) + => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + + public override string? TryGetDocumentationUri(OciArtifactEmulatedReference reference) => null; + + public override Task TryGetModuleDescription(ModuleSymbol module, OciArtifactEmulatedReference reference) + => Task.FromResult(null); + } +} diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs new file mode 100644 index 00000000000..3b95a813ad8 --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.Modules; +using Bicep.Core.SourceGraph; +using Bicep.IO.Abstraction; + +namespace Bicep.Core.Registry.Oci +{ + /// + /// Represents an OCI module reference that is emulated via a local filesystem path. + /// When a module alias specifies a "fileSystem" property instead of "registry", + /// module references are resolved to local .bicep files instead of pulling from a container registry. + /// + public class OciArtifactEmulatedReference : ArtifactReference + { + private readonly IFileHandle fileHandle; + + public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle) : + base(referencingFile, OciArtifactReferenceFacts.EmulatedScheme) + { + this.modulePath = modulePath; + this.fileHandle = fileHandle; + } + + private readonly string modulePath; + + // Override FullyQualifiedReference so user-facing diagnostics shows "br:..." + public override string FullyQualifiedReference => $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; + + public override string UnqualifiedReference => modulePath; + + public override bool IsExternal => false; + + public override ResultWithDiagnosticBuilder TryGetEntryPointFileHandle() + { + return new(fileHandle); + } + + // Extracts the module path from an unqualified reference string by removing any tag or digest suffix + public static string ExtractModulePath(string unqualifiedReference) + { + // Check for digest separator (@) + var digestIndex = unqualifiedReference.IndexOf('@'); + if (digestIndex >= 0) + { + return unqualifiedReference[..digestIndex]; + } + + // Check for tag separator (:) + var tagIndex = unqualifiedReference.LastIndexOf(':'); + if (tagIndex >= 0) + { + return unqualifiedReference[..tagIndex]; + } + + // No tag or digest — use the whole reference as the module path + return unqualifiedReference; + } + + // referencingFile is the Bicep source file containing the module reference + // fileSystemPath is the filesystem path from the alias configuration + // configFileUri is the URI of the bicepconfig.json file, used to resolve relative paths + // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") + // fileExplorer is the file explorer used to create file handles + public static ResultWithDiagnosticBuilder TryParse( + BicepSourceFile referencingFile, + string fileSystemPath, + IOUri? configFileUri, + string unqualifiedReference, + IFileExplorer fileExplorer) + { + var modulePath = ExtractModulePath(unqualifiedReference); + + if (string.IsNullOrEmpty(modulePath)) + { + return new(x => x.ModulePathHasNotBeenSpecified()); + } + + // Resolve the filesystem base directory relative to bicepconfig.json + IOUri baseUri; + if (configFileUri is not null) + { + // Ensure the fileSystem path ends with '/' so it's treated as a directory + var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') + ? fileSystemPath + : fileSystemPath + "/"; + baseUri = configFileUri.Resolve(directoryPath); + } + else + { + baseUri = IOUri.FromFilePath(fileSystemPath); + } + + // Construct the file URI by appending the module path with a .bicep extension. + var moduleFileName = modulePath + ".bicep"; + var moduleFileUri = baseUri.Resolve(moduleFileName); + + var fileHandle = fileExplorer.GetFile(moduleFileUri); + + return new(new OciArtifactEmulatedReference(referencingFile, modulePath, fileHandle)); + } + + public override bool Equals(object? obj) + { + if (obj is not OciArtifactEmulatedReference other) + { + return false; + } + + return StringComparer.Ordinal.Equals(modulePath, other.modulePath) && + fileHandle.Equals(other.fileHandle); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(modulePath, StringComparer.Ordinal); + hash.Add(fileHandle); + + return hash.ToHashCode(); + } + } +} diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs index 7e9faecd9a7..8a52a0ee355 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs @@ -8,6 +8,9 @@ namespace Bicep.Core.Registry.Oci public static partial class OciArtifactReferenceFacts { public const string Scheme = "br"; + + public const string EmulatedScheme = "br-fs"; + public const string SchemeWithColon = Scheme + ":"; public const int MaxRegistryLength = 255; diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 984fb8d727e..8de8475b3a0 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -30,12 +30,16 @@ public sealed class OciArtifactRegistry : ExternalArtifactRegistry ArtifactReferenceSchemes.Oci; @@ -48,6 +52,35 @@ public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) { + // Check if the alias resolves to a filesystem-based alias. + if (aliasName is not null) + { + if (!referencingFile.Configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var alias, out var aliasFailureBuilder)) + { + return new(aliasFailureBuilder); + } + + if (alias.FileSystem is not null) + { + if (artifactType != ArtifactType.Module) + { + return new(x => x.OciArtifactModuleAliasFileSystemOnlySupportsModules(aliasName)); + } + + if (!OciArtifactEmulatedReference.TryParse( + referencingFile, + alias.FileSystem, + referencingFile.Configuration.ConfigFileUri, + reference, + this.fileExplorer).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) + { + return new(emulatedFailureBuilder); + } + + return new(emulatedRef); + } + } + if (!OciArtifactReference.TryParse(referencingFile, artifactType, aliasName, reference).IsSuccess(out var @ref, out var failureBuilder)) { return new(failureBuilder); From 07a8148a5462fcb73d722feab9854d7fa8927f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Thu, 16 Apr 2026 21:32:50 +0200 Subject: [PATCH 02/21] Add baseline for emulated br alias --- .../baselines/Registry_LF/bicepconfig.json | 3 + .../Files/baselines/Registry_LF/main.bicep | 13 +- .../Registry_LF/main.diagnostics.bicep | 12 ++ .../Registry_LF/main.formatted.bicep | 13 ++ .../Files/baselines/Registry_LF/main.ir.bicep | 50 +++++++- .../Files/baselines/Registry_LF/main.json | 103 ++++++++++++++- .../Registry_LF/main.sourcemap.bicep | 113 +++++++++++++++++ .../Registry_LF/main.symbolicnames.json | 104 ++++++++++++++- .../baselines/Registry_LF/main.symbols.bicep | 14 ++ .../baselines/Registry_LF/main.syntax.bicep | 120 +++++++++++++++++- .../baselines/Registry_LF/main.tokens.bicep | 72 ++++++++++- 11 files changed, 610 insertions(+), 7 deletions(-) diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json index 047ab63222d..161bd860fd4 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json @@ -13,6 +13,9 @@ "demo-two": { "registry": "mock-registry-two.invalid", "modulePath": "demo" + }, + "mock-registry-emulated": { + "fileSystem": "Publish" } } } diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep index 6480bc5dcba..8a35621edd3 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep @@ -54,6 +54,17 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg @@ -130,4 +141,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { params: { ipv6port: 'test' } -} \ No newline at end of file +} diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep index b104fe240fe..13575506321 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep @@ -54,6 +54,17 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg @@ -132,3 +143,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { ipv6port: 'test' } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep index 5f20b35edaf..95d8588d6c1 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep @@ -58,6 +58,19 @@ module siteDeploy2 'br/demo-two:site:v3' = [ } ] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [ + for site in websites: { + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } + } +] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index 8d0b57400a4..0e0568069e1 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2463) ProgramExpression +//@[000:2747) ProgramExpression //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] @@ -12,6 +12,10 @@ targetScope = 'subscription' //@[000:0000) | | └─ModuleReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] +//@[000:0000) | ├─ResourceDependencyExpression [UNPARENTED] +//@[000:0000) | | └─ModuleReferenceExpression [UNPARENTED] +//@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] +//@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] @@ -187,6 +191,49 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[000:0281) ├─DeclaredModuleExpression +//@[057:0281) | ├─ForLoopExpression +//@[070:0078) | | ├─VariableReferenceExpression { Variable = websites } +//@[080:0280) | | └─ObjectExpression +//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } +//@[070:0078) | | | └─VariableReferenceExpression { Variable = websites } +//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } + name: '${site.name}siteDeploy3' +//@[002:0033) | | └─ObjectPropertyExpression +//@[002:0006) | | ├─StringLiteralExpression { Value = name } +//@[008:0033) | | └─InterpolatedStringExpression +//@[011:0020) | | └─PropertyAccessExpression { PropertyName = name } +//@[011:0015) | | └─ArrayAccessExpression +//@[011:0015) | | ├─CopyIndexExpression + scope: rg + params: { +//@[010:0150) | ├─ObjectExpression + appPlanId: appPlanDeploy.outputs.planId +//@[004:0043) | | ├─ObjectPropertyExpression +//@[004:0013) | | | ├─StringLiteralExpression { Value = appPlanId } +//@[015:0043) | | | └─ModuleOutputPropertyAccessExpression { PropertyName = planId } +//@[015:0036) | | | └─PropertyAccessExpression { PropertyName = outputs } +//@[015:0028) | | | └─ModuleReferenceExpression + namePrefix: site.name +//@[004:0025) | | ├─ObjectPropertyExpression +//@[004:0014) | | | ├─StringLiteralExpression { Value = namePrefix } +//@[016:0025) | | | └─PropertyAccessExpression { PropertyName = name } +//@[016:0020) | | | └─ArrayAccessExpression +//@[016:0020) | | | ├─CopyIndexExpression + dockerImage: 'nginxdemos/hello' +//@[004:0035) | | ├─ObjectPropertyExpression +//@[004:0015) | | | ├─StringLiteralExpression { Value = dockerImage } +//@[017:0035) | | | └─StringLiteralExpression { Value = nginxdemos/hello } + dockerImageTag: site.tag +//@[004:0028) | | └─ObjectPropertyExpression +//@[004:0018) | | ├─StringLiteralExpression { Value = dockerImageTag } +//@[020:0028) | | └─PropertyAccessExpression { PropertyName = tag } +//@[020:0024) | | └─ArrayAccessExpression +//@[020:0024) | | ├─CopyIndexExpression + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─DeclaredModuleExpression //@[090:0168) | ├─ObjectExpression @@ -374,3 +421,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[014:0020) | | └─StringLiteralExpression { Value = test } } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json index 703ac380df8..1c474d2bc7e 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "16097344762357511977" + "templateHash": "16849026672229767237" } }, "variables": { @@ -361,6 +361,107 @@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" ] }, + { + "copy": { + "name": "siteDeploy3", + "count": "[length(variables('websites'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "resourceGroup": "adotfrank-rg", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appPlanId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" + }, + "namePrefix": { + "value": "[variables('websites')[copyIndex()].name]" + }, + "dockerImage": { + "value": "nginxdemos/hello" + }, + "dockerImageTag": { + "value": "[variables('websites')[copyIndex()].tag]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "15188988612540889945" + } + }, + "parameters": { + "namePrefix": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "dockerImage": { + "type": "string" + }, + "dockerImageTag": { + "type": "string" + }, + "appPlanId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Web/sites", + "apiVersion": "2020-06-01", + "name": "[format('{0}site', parameters('namePrefix'))]", + "location": "[parameters('location')]", + "properties": { + "siteConfig": { + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "https://index.docker.io" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "" + }, + { + "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", + "value": "false" + } + ], + "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" + }, + "serverFarmId": "[parameters('appPlanId')]" + } + } + ], + "outputs": { + "siteUrl": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep index f286a23b89d..b9d0030cc38 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep @@ -394,6 +394,118 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@ { +//@ "copy": { +//@ "name": "siteDeploy3", +//@ "count": "[length(variables('websites'))]" +//@ }, +//@ "type": "Microsoft.Resources/deployments", +//@ "apiVersion": "2025-04-01", +//@ "resourceGroup": "adotfrank-rg", +//@ "properties": { +//@ "expressionEvaluationOptions": { +//@ "scope": "inner" +//@ }, +//@ "mode": "Incremental", +//@ "template": { +//@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", +//@ "contentVersion": "1.0.0.0", +//@ "metadata": { +//@ "_generator": { +//@ "name": "bicep", +//@ "version": "dev", +//@ "templateHash": "15188988612540889945" +//@ } +//@ }, +//@ "parameters": { +//@ "namePrefix": { +//@ "type": "string" +//@ }, +//@ "location": { +//@ "type": "string", +//@ "defaultValue": "[resourceGroup().location]" +//@ }, +//@ "dockerImage": { +//@ "type": "string" +//@ }, +//@ "dockerImageTag": { +//@ "type": "string" +//@ }, +//@ "appPlanId": { +//@ "type": "string" +//@ } +//@ }, +//@ "resources": [ +//@ { +//@ "type": "Microsoft.Web/sites", +//@ "apiVersion": "2020-06-01", +//@ "name": "[format('{0}site', parameters('namePrefix'))]", +//@ "location": "[parameters('location')]", +//@ "properties": { +//@ "siteConfig": { +//@ "appSettings": [ +//@ { +//@ "name": "DOCKER_REGISTRY_SERVER_URL", +//@ "value": "https://index.docker.io" +//@ }, +//@ { +//@ "name": "DOCKER_REGISTRY_SERVER_USERNAME", +//@ "value": "" +//@ }, +//@ { +//@ "name": "DOCKER_REGISTRY_SERVER_PASSWORD", +//@ "value": "" +//@ }, +//@ { +//@ "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", +//@ "value": "false" +//@ } +//@ ], +//@ "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" +//@ }, +//@ "serverFarmId": "[parameters('appPlanId')]" +//@ } +//@ } +//@ ], +//@ "outputs": { +//@ "siteUrl": { +//@ "type": "string", +//@ "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" +//@ } +//@ } +//@ } +//@ }, +//@ "dependsOn": [ +//@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", +//@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" +//@ ] +//@ }, + name: '${site.name}siteDeploy3' +//@ "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + scope: rg + params: { +//@ "parameters": { +//@ }, + appPlanId: appPlanDeploy.outputs.planId +//@ "appPlanId": { +//@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" +//@ }, + namePrefix: site.name +//@ "namePrefix": { +//@ "value": "[variables('websites')[copyIndex()].name]" +//@ }, + dockerImage: 'nginxdemos/hello' +//@ "dockerImage": { +//@ "value": "nginxdemos/hello" +//@ }, + dockerImageTag: site.tag +//@ "dockerImageTag": { +//@ "value": "[variables('websites')[copyIndex()].tag]" +//@ } + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@ { //@ "type": "Microsoft.Resources/deployments", @@ -780,3 +892,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@ } } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json index f90b2d774a0..e9b482322da 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "3717084932040700305" + "templateHash": "9031371693506242739" } }, "variables": { @@ -366,6 +366,108 @@ "rg" ] }, + "siteDeploy3": { + "copy": { + "name": "siteDeploy3", + "count": "[length(variables('websites'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "resourceGroup": "adotfrank-rg", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appPlanId": { + "value": "[reference('appPlanDeploy').outputs.planId.value]" + }, + "namePrefix": { + "value": "[variables('websites')[copyIndex()].name]" + }, + "dockerImage": { + "value": "nginxdemos/hello" + }, + "dockerImageTag": { + "value": "[variables('websites')[copyIndex()].tag]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "1727609956407115618" + } + }, + "parameters": { + "namePrefix": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "dockerImage": { + "type": "string" + }, + "dockerImageTag": { + "type": "string" + }, + "appPlanId": { + "type": "string" + } + }, + "resources": { + "namePrefix_site": { + "type": "Microsoft.Web/sites", + "apiVersion": "2020-06-01", + "name": "[format('{0}site', parameters('namePrefix'))]", + "location": "[parameters('location')]", + "properties": { + "siteConfig": { + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "https://index.docker.io" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "" + }, + { + "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", + "value": "false" + } + ], + "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" + }, + "serverFarmId": "[parameters('appPlanId')]" + } + } + }, + "outputs": { + "siteUrl": { + "type": "string", + "value": "[reference('namePrefix_site').hostNames[0]]" + } + } + } + }, + "dependsOn": [ + "appPlanDeploy", + "rg" + ] + }, "storageDeploy": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index 1bfca75ea18..d6d89234270 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -62,6 +62,19 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[62:66) Local site. Type: object | object. Declaration start char: 62, length: 4 +//@[07:18) Module siteDeploy3. Type: module[]. Declaration start char: 0, length: 281 + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[07:20) Module storageDeploy. Type: module. Declaration start char: 0, length: 168 name: 'storageDeploy' @@ -152,3 +165,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { ipv6port: 'test' } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index 1f5a364ea01..136c3e2f4d5 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2463) ProgramSyntax +//@[000:2747) ProgramSyntax //@[000:0028) ├─TargetScopeSyntax //@[000:0011) | ├─Token(Identifier) |targetScope| //@[012:0013) | ├─Token(Assignment) |=| @@ -436,6 +436,120 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:0002) | └─Token(RightSquare) |]| //@[002:0004) ├─Token(NewLine) |\n\n| +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[000:0281) ├─ModuleDeclarationSyntax +//@[000:0006) | ├─Token(Identifier) |module| +//@[007:0018) | ├─IdentifierSyntax +//@[007:0018) | | └─Token(Identifier) |siteDeploy3| +//@[019:0054) | ├─StringSyntax +//@[019:0054) | | └─Token(StringComplete) |'br/mock-registry-emulated:site:v3'| +//@[055:0056) | ├─Token(Assignment) |=| +//@[057:0281) | └─ForSyntax +//@[057:0058) | ├─Token(LeftSquare) |[| +//@[058:0061) | ├─Token(Identifier) |for| +//@[062:0066) | ├─LocalVariableSyntax +//@[062:0066) | | └─IdentifierSyntax +//@[062:0066) | | └─Token(Identifier) |site| +//@[067:0069) | ├─Token(Identifier) |in| +//@[070:0078) | ├─VariableAccessSyntax +//@[070:0078) | | └─IdentifierSyntax +//@[070:0078) | | └─Token(Identifier) |websites| +//@[078:0079) | ├─Token(Colon) |:| +//@[080:0280) | ├─ObjectSyntax +//@[080:0081) | | ├─Token(LeftBrace) |{| +//@[081:0082) | | ├─Token(NewLine) |\n| + name: '${site.name}siteDeploy3' +//@[002:0033) | | ├─ObjectPropertySyntax +//@[002:0006) | | | ├─IdentifierSyntax +//@[002:0006) | | | | └─Token(Identifier) |name| +//@[006:0007) | | | ├─Token(Colon) |:| +//@[008:0033) | | | └─StringSyntax +//@[008:0011) | | | ├─Token(StringLeftPiece) |'${| +//@[011:0020) | | | ├─PropertyAccessSyntax +//@[011:0015) | | | | ├─VariableAccessSyntax +//@[011:0015) | | | | | └─IdentifierSyntax +//@[011:0015) | | | | | └─Token(Identifier) |site| +//@[015:0016) | | | | ├─Token(Dot) |.| +//@[016:0020) | | | | └─IdentifierSyntax +//@[016:0020) | | | | └─Token(Identifier) |name| +//@[020:0033) | | | └─Token(StringRightPiece) |}siteDeploy3'| +//@[033:0034) | | ├─Token(NewLine) |\n| + scope: rg +//@[002:0011) | | ├─ObjectPropertySyntax +//@[002:0007) | | | ├─IdentifierSyntax +//@[002:0007) | | | | └─Token(Identifier) |scope| +//@[007:0008) | | | ├─Token(Colon) |:| +//@[009:0011) | | | └─VariableAccessSyntax +//@[009:0011) | | | └─IdentifierSyntax +//@[009:0011) | | | └─Token(Identifier) |rg| +//@[011:0012) | | ├─Token(NewLine) |\n| + params: { +//@[002:0150) | | ├─ObjectPropertySyntax +//@[002:0008) | | | ├─IdentifierSyntax +//@[002:0008) | | | | └─Token(Identifier) |params| +//@[008:0009) | | | ├─Token(Colon) |:| +//@[010:0150) | | | └─ObjectSyntax +//@[010:0011) | | | ├─Token(LeftBrace) |{| +//@[011:0012) | | | ├─Token(NewLine) |\n| + appPlanId: appPlanDeploy.outputs.planId +//@[004:0043) | | | ├─ObjectPropertySyntax +//@[004:0013) | | | | ├─IdentifierSyntax +//@[004:0013) | | | | | └─Token(Identifier) |appPlanId| +//@[013:0014) | | | | ├─Token(Colon) |:| +//@[015:0043) | | | | └─PropertyAccessSyntax +//@[015:0036) | | | | ├─PropertyAccessSyntax +//@[015:0028) | | | | | ├─VariableAccessSyntax +//@[015:0028) | | | | | | └─IdentifierSyntax +//@[015:0028) | | | | | | └─Token(Identifier) |appPlanDeploy| +//@[028:0029) | | | | | ├─Token(Dot) |.| +//@[029:0036) | | | | | └─IdentifierSyntax +//@[029:0036) | | | | | └─Token(Identifier) |outputs| +//@[036:0037) | | | | ├─Token(Dot) |.| +//@[037:0043) | | | | └─IdentifierSyntax +//@[037:0043) | | | | └─Token(Identifier) |planId| +//@[043:0044) | | | ├─Token(NewLine) |\n| + namePrefix: site.name +//@[004:0025) | | | ├─ObjectPropertySyntax +//@[004:0014) | | | | ├─IdentifierSyntax +//@[004:0014) | | | | | └─Token(Identifier) |namePrefix| +//@[014:0015) | | | | ├─Token(Colon) |:| +//@[016:0025) | | | | └─PropertyAccessSyntax +//@[016:0020) | | | | ├─VariableAccessSyntax +//@[016:0020) | | | | | └─IdentifierSyntax +//@[016:0020) | | | | | └─Token(Identifier) |site| +//@[020:0021) | | | | ├─Token(Dot) |.| +//@[021:0025) | | | | └─IdentifierSyntax +//@[021:0025) | | | | └─Token(Identifier) |name| +//@[025:0026) | | | ├─Token(NewLine) |\n| + dockerImage: 'nginxdemos/hello' +//@[004:0035) | | | ├─ObjectPropertySyntax +//@[004:0015) | | | | ├─IdentifierSyntax +//@[004:0015) | | | | | └─Token(Identifier) |dockerImage| +//@[015:0016) | | | | ├─Token(Colon) |:| +//@[017:0035) | | | | └─StringSyntax +//@[017:0035) | | | | └─Token(StringComplete) |'nginxdemos/hello'| +//@[035:0036) | | | ├─Token(NewLine) |\n| + dockerImageTag: site.tag +//@[004:0028) | | | ├─ObjectPropertySyntax +//@[004:0018) | | | | ├─IdentifierSyntax +//@[004:0018) | | | | | └─Token(Identifier) |dockerImageTag| +//@[018:0019) | | | | ├─Token(Colon) |:| +//@[020:0028) | | | | └─PropertyAccessSyntax +//@[020:0024) | | | | ├─VariableAccessSyntax +//@[020:0024) | | | | | └─IdentifierSyntax +//@[020:0024) | | | | | └─Token(Identifier) |site| +//@[024:0025) | | | | ├─Token(Dot) |.| +//@[025:0028) | | | | └─IdentifierSyntax +//@[025:0028) | | | | └─Token(Identifier) |tag| +//@[028:0029) | | | ├─Token(NewLine) |\n| + } +//@[002:0003) | | | └─Token(RightBrace) |}| +//@[003:0004) | | ├─Token(NewLine) |\n| +}] +//@[000:0001) | | └─Token(RightBrace) |}| +//@[001:0002) | └─Token(RightSquare) |]| +//@[002:0004) ├─Token(NewLine) |\n\n| + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─ModuleDeclarationSyntax //@[000:0006) | ├─Token(Identifier) |module| @@ -988,4 +1102,6 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[003:0004) | ├─Token(NewLine) |\n| } //@[000:0001) | └─Token(RightBrace) |}| -//@[001:0001) └─Token(EndOfFile) || +//@[001:0002) ├─Token(NewLine) |\n| + +//@[000:0000) └─Token(EndOfFile) || diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index 43d953768e3..ebd99f647de 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -275,6 +275,74 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:002) RightSquare |]| //@[002:004) NewLine |\n\n| +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[000:006) Identifier |module| +//@[007:018) Identifier |siteDeploy3| +//@[019:054) StringComplete |'br/mock-registry-emulated:site:v3'| +//@[055:056) Assignment |=| +//@[057:058) LeftSquare |[| +//@[058:061) Identifier |for| +//@[062:066) Identifier |site| +//@[067:069) Identifier |in| +//@[070:078) Identifier |websites| +//@[078:079) Colon |:| +//@[080:081) LeftBrace |{| +//@[081:082) NewLine |\n| + name: '${site.name}siteDeploy3' +//@[002:006) Identifier |name| +//@[006:007) Colon |:| +//@[008:011) StringLeftPiece |'${| +//@[011:015) Identifier |site| +//@[015:016) Dot |.| +//@[016:020) Identifier |name| +//@[020:033) StringRightPiece |}siteDeploy3'| +//@[033:034) NewLine |\n| + scope: rg +//@[002:007) Identifier |scope| +//@[007:008) Colon |:| +//@[009:011) Identifier |rg| +//@[011:012) NewLine |\n| + params: { +//@[002:008) Identifier |params| +//@[008:009) Colon |:| +//@[010:011) LeftBrace |{| +//@[011:012) NewLine |\n| + appPlanId: appPlanDeploy.outputs.planId +//@[004:013) Identifier |appPlanId| +//@[013:014) Colon |:| +//@[015:028) Identifier |appPlanDeploy| +//@[028:029) Dot |.| +//@[029:036) Identifier |outputs| +//@[036:037) Dot |.| +//@[037:043) Identifier |planId| +//@[043:044) NewLine |\n| + namePrefix: site.name +//@[004:014) Identifier |namePrefix| +//@[014:015) Colon |:| +//@[016:020) Identifier |site| +//@[020:021) Dot |.| +//@[021:025) Identifier |name| +//@[025:026) NewLine |\n| + dockerImage: 'nginxdemos/hello' +//@[004:015) Identifier |dockerImage| +//@[015:016) Colon |:| +//@[017:035) StringComplete |'nginxdemos/hello'| +//@[035:036) NewLine |\n| + dockerImageTag: site.tag +//@[004:018) Identifier |dockerImageTag| +//@[018:019) Colon |:| +//@[020:024) Identifier |site| +//@[024:025) Dot |.| +//@[025:028) Identifier |tag| +//@[028:029) NewLine |\n| + } +//@[002:003) RightBrace |}| +//@[003:004) NewLine |\n| +}] +//@[000:001) RightBrace |}| +//@[001:002) RightSquare |]| +//@[002:004) NewLine |\n\n| + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:006) Identifier |module| //@[007:020) Identifier |storageDeploy| @@ -633,4 +701,6 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[003:004) NewLine |\n| } //@[000:001) RightBrace |}| -//@[001:001) EndOfFile || +//@[001:002) NewLine |\n| + +//@[000:000) EndOfFile || From 9838910a186dcc3f7303ec44b3682593275e845d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 19 Apr 2026 22:15:58 +0200 Subject: [PATCH 03/21] Update integration tests --- .../RegistryTests.cs | 4 +- .../Files/baselines/Registry_LF/main.bicep | 19 +- .../Registry_LF/main.diagnostics.bicep | 19 +- .../Registry_LF/main.formatted.bicep | 21 +-- .../Files/baselines/Registry_LF/main.ir.bicep | 64 ++----- .../Files/baselines/Registry_LF/main.json | 80 +++------ .../Registry_LF/main.sourcemap.bicep | 139 +++++---------- .../Registry_LF/main.symbolicnames.json | 88 +++------ .../baselines/Registry_LF/main.symbols.bicep | 22 +-- .../baselines/Registry_LF/main.syntax.bicep | 167 ++++++------------ .../baselines/Registry_LF/main.tokens.bicep | 102 ++++------- 11 files changed, 231 insertions(+), 494 deletions(-) diff --git a/src/Bicep.Core.IntegrationTests/RegistryTests.cs b/src/Bicep.Core.IntegrationTests/RegistryTests.cs index 466897dec6c..3bc5a7719a5 100644 --- a/src/Bicep.Core.IntegrationTests/RegistryTests.cs +++ b/src/Bicep.Core.IntegrationTests/RegistryTests.cs @@ -64,9 +64,9 @@ public async Task InvalidRootCachePathShouldProduceReasonableErrors() var compilation = await compiler.CreateCompilation(fileUri.ToIOUri()); var diagnostics = compilation.GetAllDiagnosticsByBicepFile(); - diagnostics.Should().HaveCount(1); + diagnostics.Should().HaveCount(2); var expectedErrorMessage = "Unable to restore the artifact with reference \"{0}\": Unable to create the local artifact directory \""; - diagnostics.Single().Value.ExcludingLinterDiagnostics().Should().SatisfyRespectively( + diagnostics[compilation.SourceFileGrouping.EntryPoint].ExcludingLinterDiagnostics().Should().SatisfyRespectively( x => { x.Level.Should().Be(DiagnosticLevel.Error); diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep index 8a35621edd3..db4d3503e0f 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -54,17 +62,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep index 13575506321..7d4f17d9542 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -54,17 +62,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep index 95d8588d6c1..c7a28bb80ad 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -58,19 +66,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [ } ] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [ - for site in websites: { - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } - } -] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index 0e0568069e1..fcd302c7f41 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -1,11 +1,9 @@ targetScope = 'subscription' -//@[000:2747) ProgramExpression +//@[000:2603) ProgramExpression //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] -//@[000:0000) | ├─ResourceDependencyExpression [UNPARENTED] -//@[000:0000) | | └─ModuleReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | ├─ResourceDependencyExpression [UNPARENTED] @@ -78,6 +76,23 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:0137) ├─DeclaredModuleExpression +//@[060:0137) | ├─ObjectExpression + name: 'planDeploy3' +//@[002:0021) | | └─ObjectPropertyExpression +//@[002:0006) | | ├─StringLiteralExpression { Value = name } +//@[008:0021) | | └─StringLiteralExpression { Value = planDeploy3 } + scope: rg + params: { +//@[010:0039) | ├─ObjectExpression + namePrefix: 'hello' +//@[004:0023) | | └─ObjectPropertyExpression +//@[004:0014) | | ├─StringLiteralExpression { Value = namePrefix } +//@[016:0023) | | └─StringLiteralExpression { Value = hello } + } +} + var websites = [ //@[000:0110) ├─DeclaredVariableExpression { Name = websites } //@[015:0110) | └─ArrayExpression @@ -191,49 +206,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[000:0281) ├─DeclaredModuleExpression -//@[057:0281) | ├─ForLoopExpression -//@[070:0078) | | ├─VariableReferenceExpression { Variable = websites } -//@[080:0280) | | └─ObjectExpression -//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } -//@[070:0078) | | | └─VariableReferenceExpression { Variable = websites } -//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } - name: '${site.name}siteDeploy3' -//@[002:0033) | | └─ObjectPropertyExpression -//@[002:0006) | | ├─StringLiteralExpression { Value = name } -//@[008:0033) | | └─InterpolatedStringExpression -//@[011:0020) | | └─PropertyAccessExpression { PropertyName = name } -//@[011:0015) | | └─ArrayAccessExpression -//@[011:0015) | | ├─CopyIndexExpression - scope: rg - params: { -//@[010:0150) | ├─ObjectExpression - appPlanId: appPlanDeploy.outputs.planId -//@[004:0043) | | ├─ObjectPropertyExpression -//@[004:0013) | | | ├─StringLiteralExpression { Value = appPlanId } -//@[015:0043) | | | └─ModuleOutputPropertyAccessExpression { PropertyName = planId } -//@[015:0036) | | | └─PropertyAccessExpression { PropertyName = outputs } -//@[015:0028) | | | └─ModuleReferenceExpression - namePrefix: site.name -//@[004:0025) | | ├─ObjectPropertyExpression -//@[004:0014) | | | ├─StringLiteralExpression { Value = namePrefix } -//@[016:0025) | | | └─PropertyAccessExpression { PropertyName = name } -//@[016:0020) | | | └─ArrayAccessExpression -//@[016:0020) | | | ├─CopyIndexExpression - dockerImage: 'nginxdemos/hello' -//@[004:0035) | | ├─ObjectPropertyExpression -//@[004:0015) | | | ├─StringLiteralExpression { Value = dockerImage } -//@[017:0035) | | | └─StringLiteralExpression { Value = nginxdemos/hello } - dockerImageTag: site.tag -//@[004:0028) | | └─ObjectPropertyExpression -//@[004:0018) | | ├─StringLiteralExpression { Value = dockerImageTag } -//@[020:0028) | | └─PropertyAccessExpression { PropertyName = tag } -//@[020:0024) | | └─ArrayAccessExpression -//@[020:0024) | | ├─CopyIndexExpression - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─DeclaredModuleExpression //@[090:0168) | ├─ObjectExpression diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json index 1c474d2bc7e..710dcd5ed2b 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "16849026672229767237" + "templateHash": "11340138247578714789" } }, "variables": { @@ -160,13 +160,9 @@ ] }, { - "copy": { - "name": "siteDeploy", - "count": "[length(variables('websites'))]" - }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", + "name": "planDeploy3", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -174,17 +170,8 @@ }, "mode": "Incremental", "parameters": { - "appPlanId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" - }, "namePrefix": { - "value": "[variables('websites')[copyIndex()].name]" - }, - "dockerImage": { - "value": "nginxdemos/hello" - }, - "dockerImageTag": { - "value": "[variables('websites')[copyIndex()].tag]" + "value": "hello" } }, "template": { @@ -194,80 +181,53 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "15188988612540889945" + "templateHash": "15019246960605065046" } }, "parameters": { "namePrefix": { "type": "string" }, - "location": { + "sku": { "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "dockerImage": { - "type": "string" - }, - "dockerImageTag": { - "type": "string" - }, - "appPlanId": { - "type": "string" + "defaultValue": "B1" } }, "resources": [ { - "type": "Microsoft.Web/sites", + "type": "Microsoft.Web/serverfarms", "apiVersion": "2020-06-01", - "name": "[format('{0}site', parameters('namePrefix'))]", - "location": "[parameters('location')]", + "name": "[format('{0}appPlan', parameters('namePrefix'))]", + "location": "[resourceGroup().location]", + "kind": "linux", + "sku": { + "name": "[parameters('sku')]" + }, "properties": { - "siteConfig": { - "appSettings": [ - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "https://index.docker.io" - }, - { - "name": "DOCKER_REGISTRY_SERVER_USERNAME", - "value": "" - }, - { - "name": "DOCKER_REGISTRY_SERVER_PASSWORD", - "value": "" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - } - ], - "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" - }, - "serverFarmId": "[parameters('appPlanId')]" + "reserved": true } } ], "outputs": { - "siteUrl": { + "planId": { "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" + "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" } } } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" ] }, { "copy": { - "name": "siteDeploy2", + "name": "siteDeploy", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -363,12 +323,12 @@ }, { "copy": { - "name": "siteDeploy3", + "name": "siteDeploy2", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep index b9d0030cc38..529e8a6ea67 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep @@ -149,33 +149,8 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -var websites = [ -//@ "websites": [ -//@ ], - { -//@ { -//@ }, - name: 'fancy' -//@ "name": "fancy", - tag: 'latest' -//@ "tag": "latest" - } - { -//@ { -//@ } - name: 'plain' -//@ "name": "plain", - tag: 'plain-text' -//@ "tag": "plain-text" - } -] - -module siteDeploy 'br:mock-registry-two.invalid/demo/site:v3' = [for site in websites: { +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { //@ { -//@ "copy": { -//@ "name": "siteDeploy", -//@ "count": "[length(variables('websites'))]" -//@ }, //@ "type": "Microsoft.Resources/deployments", //@ "apiVersion": "2025-04-01", //@ "resourceGroup": "adotfrank-rg", @@ -191,101 +166,83 @@ module siteDeploy 'br:mock-registry-two.invalid/demo/site:v3' = [for site in web //@ "_generator": { //@ "name": "bicep", //@ "version": "dev", -//@ "templateHash": "15188988612540889945" +//@ "templateHash": "15019246960605065046" //@ } //@ }, //@ "parameters": { //@ "namePrefix": { //@ "type": "string" //@ }, -//@ "location": { +//@ "sku": { //@ "type": "string", -//@ "defaultValue": "[resourceGroup().location]" -//@ }, -//@ "dockerImage": { -//@ "type": "string" -//@ }, -//@ "dockerImageTag": { -//@ "type": "string" -//@ }, -//@ "appPlanId": { -//@ "type": "string" +//@ "defaultValue": "B1" //@ } //@ }, //@ "resources": [ //@ { -//@ "type": "Microsoft.Web/sites", +//@ "type": "Microsoft.Web/serverfarms", //@ "apiVersion": "2020-06-01", -//@ "name": "[format('{0}site', parameters('namePrefix'))]", -//@ "location": "[parameters('location')]", +//@ "name": "[format('{0}appPlan', parameters('namePrefix'))]", +//@ "location": "[resourceGroup().location]", +//@ "kind": "linux", +//@ "sku": { +//@ "name": "[parameters('sku')]" +//@ }, //@ "properties": { -//@ "siteConfig": { -//@ "appSettings": [ -//@ { -//@ "name": "DOCKER_REGISTRY_SERVER_URL", -//@ "value": "https://index.docker.io" -//@ }, -//@ { -//@ "name": "DOCKER_REGISTRY_SERVER_USERNAME", -//@ "value": "" -//@ }, -//@ { -//@ "name": "DOCKER_REGISTRY_SERVER_PASSWORD", -//@ "value": "" -//@ }, -//@ { -//@ "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", -//@ "value": "false" -//@ } -//@ ], -//@ "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" -//@ }, -//@ "serverFarmId": "[parameters('appPlanId')]" +//@ "reserved": true //@ } //@ } //@ ], //@ "outputs": { -//@ "siteUrl": { +//@ "planId": { //@ "type": "string", -//@ "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" +//@ "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" //@ } //@ } //@ } //@ }, //@ "dependsOn": [ -//@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", //@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" //@ ] //@ }, - name: '${site.name}siteDeploy' -//@ "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", + name: 'planDeploy3' +//@ "name": "planDeploy3", scope: rg params: { //@ "parameters": { //@ }, - appPlanId: appPlanDeploy.outputs.planId -//@ "appPlanId": { -//@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" -//@ }, - namePrefix: site.name + namePrefix: 'hello' //@ "namePrefix": { -//@ "value": "[variables('websites')[copyIndex()].name]" -//@ }, - dockerImage: 'nginxdemos/hello' -//@ "dockerImage": { -//@ "value": "nginxdemos/hello" -//@ }, - dockerImageTag: site.tag -//@ "dockerImageTag": { -//@ "value": "[variables('websites')[copyIndex()].tag]" +//@ "value": "hello" //@ } } -}] +} -module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { +var websites = [ +//@ "websites": [ +//@ ], + { +//@ { +//@ }, + name: 'fancy' +//@ "name": "fancy", + tag: 'latest' +//@ "tag": "latest" + } + { +//@ { +//@ } + name: 'plain' +//@ "name": "plain", + tag: 'plain-text' +//@ "tag": "plain-text" + } +] + +module siteDeploy 'br:mock-registry-two.invalid/demo/site:v3' = [for site in websites: { //@ { //@ "copy": { -//@ "name": "siteDeploy2", +//@ "name": "siteDeploy", //@ "count": "[length(variables('websites'))]" //@ }, //@ "type": "Microsoft.Resources/deployments", @@ -369,8 +326,8 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" //@ ] //@ }, - name: '${site.name}siteDeploy2' -//@ "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", + name: '${site.name}siteDeploy' +//@ "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", scope: rg params: { //@ "parameters": { @@ -394,10 +351,10 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@ { //@ "copy": { -//@ "name": "siteDeploy3", +//@ "name": "siteDeploy2", //@ "count": "[length(variables('websites'))]" //@ }, //@ "type": "Microsoft.Resources/deployments", @@ -481,8 +438,8 @@ module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: //@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" //@ ] //@ }, - name: '${site.name}siteDeploy3' -//@ "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + name: '${site.name}siteDeploy2' +//@ "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", scope: rg params: { //@ "parameters": { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json index e9b482322da..57e16934904 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "9031371693506242739" + "templateHash": "17302964314594708830" } }, "variables": { @@ -162,14 +162,10 @@ "rg" ] }, - "siteDeploy": { - "copy": { - "name": "siteDeploy", - "count": "[length(variables('websites'))]" - }, + "appPlanDeploy3": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", + "name": "planDeploy3", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -177,17 +173,8 @@ }, "mode": "Incremental", "parameters": { - "appPlanId": { - "value": "[reference('appPlanDeploy').outputs.planId.value]" - }, "namePrefix": { - "value": "[variables('websites')[copyIndex()].name]" - }, - "dockerImage": { - "value": "nginxdemos/hello" - }, - "dockerImageTag": { - "value": "[variables('websites')[copyIndex()].tag]" + "value": "hello" } }, "template": { @@ -198,80 +185,53 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "1727609956407115618" + "templateHash": "13508561622047952911" } }, "parameters": { "namePrefix": { "type": "string" }, - "location": { + "sku": { "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "dockerImage": { - "type": "string" - }, - "dockerImageTag": { - "type": "string" - }, - "appPlanId": { - "type": "string" + "defaultValue": "B1" } }, "resources": { - "namePrefix_site": { - "type": "Microsoft.Web/sites", + "appPlan": { + "type": "Microsoft.Web/serverfarms", "apiVersion": "2020-06-01", - "name": "[format('{0}site', parameters('namePrefix'))]", - "location": "[parameters('location')]", + "name": "[format('{0}appPlan', parameters('namePrefix'))]", + "location": "[resourceGroup().location]", + "kind": "linux", + "sku": { + "name": "[parameters('sku')]" + }, "properties": { - "siteConfig": { - "appSettings": [ - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "https://index.docker.io" - }, - { - "name": "DOCKER_REGISTRY_SERVER_USERNAME", - "value": "" - }, - { - "name": "DOCKER_REGISTRY_SERVER_PASSWORD", - "value": "" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - } - ], - "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" - }, - "serverFarmId": "[parameters('appPlanId')]" + "reserved": true } } }, "outputs": { - "siteUrl": { + "planId": { "type": "string", - "value": "[reference('namePrefix_site').hostNames[0]]" + "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" } } } }, "dependsOn": [ - "appPlanDeploy", "rg" ] }, - "siteDeploy2": { + "siteDeploy": { "copy": { - "name": "siteDeploy2", + "name": "siteDeploy", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -366,14 +326,14 @@ "rg" ] }, - "siteDeploy3": { + "siteDeploy2": { "copy": { - "name": "siteDeploy3", + "name": "siteDeploy2", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index d6d89234270..fea26bf29d8 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -24,6 +24,15 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 137 + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ //@[04:12) Variable websites. Type: [object, object]. Declaration start char: 0, length: 110 { @@ -62,19 +71,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[62:66) Local site. Type: object | object. Declaration start char: 62, length: 4 -//@[07:18) Module siteDeploy3. Type: module[]. Declaration start char: 0, length: 281 - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[07:20) Module storageDeploy. Type: module. Declaration start char: 0, length: 168 name: 'storageDeploy' diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index 136c3e2f4d5..55f64164bc5 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2747) ProgramSyntax +//@[000:2603) ProgramSyntax //@[000:0028) ├─TargetScopeSyntax //@[000:0011) | ├─Token(Identifier) |targetScope| //@[012:0013) | ├─Token(Assignment) |=| @@ -147,6 +147,57 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:0001) | └─Token(RightBrace) |}| //@[001:0003) ├─Token(NewLine) |\n\n| +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:0137) ├─ModuleDeclarationSyntax +//@[000:0006) | ├─Token(Identifier) |module| +//@[007:0021) | ├─IdentifierSyntax +//@[007:0021) | | └─Token(Identifier) |appPlanDeploy3| +//@[022:0057) | ├─StringSyntax +//@[022:0057) | | └─Token(StringComplete) |'br/mock-registry-emulated:plan:v2'| +//@[058:0059) | ├─Token(Assignment) |=| +//@[060:0137) | └─ObjectSyntax +//@[060:0061) | ├─Token(LeftBrace) |{| +//@[061:0062) | ├─Token(NewLine) |\n| + name: 'planDeploy3' +//@[002:0021) | ├─ObjectPropertySyntax +//@[002:0006) | | ├─IdentifierSyntax +//@[002:0006) | | | └─Token(Identifier) |name| +//@[006:0007) | | ├─Token(Colon) |:| +//@[008:0021) | | └─StringSyntax +//@[008:0021) | | └─Token(StringComplete) |'planDeploy3'| +//@[021:0022) | ├─Token(NewLine) |\n| + scope: rg +//@[002:0011) | ├─ObjectPropertySyntax +//@[002:0007) | | ├─IdentifierSyntax +//@[002:0007) | | | └─Token(Identifier) |scope| +//@[007:0008) | | ├─Token(Colon) |:| +//@[009:0011) | | └─VariableAccessSyntax +//@[009:0011) | | └─IdentifierSyntax +//@[009:0011) | | └─Token(Identifier) |rg| +//@[011:0012) | ├─Token(NewLine) |\n| + params: { +//@[002:0039) | ├─ObjectPropertySyntax +//@[002:0008) | | ├─IdentifierSyntax +//@[002:0008) | | | └─Token(Identifier) |params| +//@[008:0009) | | ├─Token(Colon) |:| +//@[010:0039) | | └─ObjectSyntax +//@[010:0011) | | ├─Token(LeftBrace) |{| +//@[011:0012) | | ├─Token(NewLine) |\n| + namePrefix: 'hello' +//@[004:0023) | | ├─ObjectPropertySyntax +//@[004:0014) | | | ├─IdentifierSyntax +//@[004:0014) | | | | └─Token(Identifier) |namePrefix| +//@[014:0015) | | | ├─Token(Colon) |:| +//@[016:0023) | | | └─StringSyntax +//@[016:0023) | | | └─Token(StringComplete) |'hello'| +//@[023:0024) | | ├─Token(NewLine) |\n| + } +//@[002:0003) | | └─Token(RightBrace) |}| +//@[003:0004) | ├─Token(NewLine) |\n| +} +//@[000:0001) | └─Token(RightBrace) |}| +//@[001:0003) ├─Token(NewLine) |\n\n| + var websites = [ //@[000:0110) ├─VariableDeclarationSyntax //@[000:0003) | ├─Token(Identifier) |var| @@ -436,120 +487,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:0002) | └─Token(RightSquare) |]| //@[002:0004) ├─Token(NewLine) |\n\n| -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[000:0281) ├─ModuleDeclarationSyntax -//@[000:0006) | ├─Token(Identifier) |module| -//@[007:0018) | ├─IdentifierSyntax -//@[007:0018) | | └─Token(Identifier) |siteDeploy3| -//@[019:0054) | ├─StringSyntax -//@[019:0054) | | └─Token(StringComplete) |'br/mock-registry-emulated:site:v3'| -//@[055:0056) | ├─Token(Assignment) |=| -//@[057:0281) | └─ForSyntax -//@[057:0058) | ├─Token(LeftSquare) |[| -//@[058:0061) | ├─Token(Identifier) |for| -//@[062:0066) | ├─LocalVariableSyntax -//@[062:0066) | | └─IdentifierSyntax -//@[062:0066) | | └─Token(Identifier) |site| -//@[067:0069) | ├─Token(Identifier) |in| -//@[070:0078) | ├─VariableAccessSyntax -//@[070:0078) | | └─IdentifierSyntax -//@[070:0078) | | └─Token(Identifier) |websites| -//@[078:0079) | ├─Token(Colon) |:| -//@[080:0280) | ├─ObjectSyntax -//@[080:0081) | | ├─Token(LeftBrace) |{| -//@[081:0082) | | ├─Token(NewLine) |\n| - name: '${site.name}siteDeploy3' -//@[002:0033) | | ├─ObjectPropertySyntax -//@[002:0006) | | | ├─IdentifierSyntax -//@[002:0006) | | | | └─Token(Identifier) |name| -//@[006:0007) | | | ├─Token(Colon) |:| -//@[008:0033) | | | └─StringSyntax -//@[008:0011) | | | ├─Token(StringLeftPiece) |'${| -//@[011:0020) | | | ├─PropertyAccessSyntax -//@[011:0015) | | | | ├─VariableAccessSyntax -//@[011:0015) | | | | | └─IdentifierSyntax -//@[011:0015) | | | | | └─Token(Identifier) |site| -//@[015:0016) | | | | ├─Token(Dot) |.| -//@[016:0020) | | | | └─IdentifierSyntax -//@[016:0020) | | | | └─Token(Identifier) |name| -//@[020:0033) | | | └─Token(StringRightPiece) |}siteDeploy3'| -//@[033:0034) | | ├─Token(NewLine) |\n| - scope: rg -//@[002:0011) | | ├─ObjectPropertySyntax -//@[002:0007) | | | ├─IdentifierSyntax -//@[002:0007) | | | | └─Token(Identifier) |scope| -//@[007:0008) | | | ├─Token(Colon) |:| -//@[009:0011) | | | └─VariableAccessSyntax -//@[009:0011) | | | └─IdentifierSyntax -//@[009:0011) | | | └─Token(Identifier) |rg| -//@[011:0012) | | ├─Token(NewLine) |\n| - params: { -//@[002:0150) | | ├─ObjectPropertySyntax -//@[002:0008) | | | ├─IdentifierSyntax -//@[002:0008) | | | | └─Token(Identifier) |params| -//@[008:0009) | | | ├─Token(Colon) |:| -//@[010:0150) | | | └─ObjectSyntax -//@[010:0011) | | | ├─Token(LeftBrace) |{| -//@[011:0012) | | | ├─Token(NewLine) |\n| - appPlanId: appPlanDeploy.outputs.planId -//@[004:0043) | | | ├─ObjectPropertySyntax -//@[004:0013) | | | | ├─IdentifierSyntax -//@[004:0013) | | | | | └─Token(Identifier) |appPlanId| -//@[013:0014) | | | | ├─Token(Colon) |:| -//@[015:0043) | | | | └─PropertyAccessSyntax -//@[015:0036) | | | | ├─PropertyAccessSyntax -//@[015:0028) | | | | | ├─VariableAccessSyntax -//@[015:0028) | | | | | | └─IdentifierSyntax -//@[015:0028) | | | | | | └─Token(Identifier) |appPlanDeploy| -//@[028:0029) | | | | | ├─Token(Dot) |.| -//@[029:0036) | | | | | └─IdentifierSyntax -//@[029:0036) | | | | | └─Token(Identifier) |outputs| -//@[036:0037) | | | | ├─Token(Dot) |.| -//@[037:0043) | | | | └─IdentifierSyntax -//@[037:0043) | | | | └─Token(Identifier) |planId| -//@[043:0044) | | | ├─Token(NewLine) |\n| - namePrefix: site.name -//@[004:0025) | | | ├─ObjectPropertySyntax -//@[004:0014) | | | | ├─IdentifierSyntax -//@[004:0014) | | | | | └─Token(Identifier) |namePrefix| -//@[014:0015) | | | | ├─Token(Colon) |:| -//@[016:0025) | | | | └─PropertyAccessSyntax -//@[016:0020) | | | | ├─VariableAccessSyntax -//@[016:0020) | | | | | └─IdentifierSyntax -//@[016:0020) | | | | | └─Token(Identifier) |site| -//@[020:0021) | | | | ├─Token(Dot) |.| -//@[021:0025) | | | | └─IdentifierSyntax -//@[021:0025) | | | | └─Token(Identifier) |name| -//@[025:0026) | | | ├─Token(NewLine) |\n| - dockerImage: 'nginxdemos/hello' -//@[004:0035) | | | ├─ObjectPropertySyntax -//@[004:0015) | | | | ├─IdentifierSyntax -//@[004:0015) | | | | | └─Token(Identifier) |dockerImage| -//@[015:0016) | | | | ├─Token(Colon) |:| -//@[017:0035) | | | | └─StringSyntax -//@[017:0035) | | | | └─Token(StringComplete) |'nginxdemos/hello'| -//@[035:0036) | | | ├─Token(NewLine) |\n| - dockerImageTag: site.tag -//@[004:0028) | | | ├─ObjectPropertySyntax -//@[004:0018) | | | | ├─IdentifierSyntax -//@[004:0018) | | | | | └─Token(Identifier) |dockerImageTag| -//@[018:0019) | | | | ├─Token(Colon) |:| -//@[020:0028) | | | | └─PropertyAccessSyntax -//@[020:0024) | | | | ├─VariableAccessSyntax -//@[020:0024) | | | | | └─IdentifierSyntax -//@[020:0024) | | | | | └─Token(Identifier) |site| -//@[024:0025) | | | | ├─Token(Dot) |.| -//@[025:0028) | | | | └─IdentifierSyntax -//@[025:0028) | | | | └─Token(Identifier) |tag| -//@[028:0029) | | | ├─Token(NewLine) |\n| - } -//@[002:0003) | | | └─Token(RightBrace) |}| -//@[003:0004) | | ├─Token(NewLine) |\n| -}] -//@[000:0001) | | └─Token(RightBrace) |}| -//@[001:0002) | └─Token(RightSquare) |]| -//@[002:0004) ├─Token(NewLine) |\n\n| - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─ModuleDeclarationSyntax //@[000:0006) | ├─Token(Identifier) |module| diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index ebd99f647de..ff81a47d607 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -97,6 +97,40 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:001) RightBrace |}| //@[001:003) NewLine |\n\n| +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:006) Identifier |module| +//@[007:021) Identifier |appPlanDeploy3| +//@[022:057) StringComplete |'br/mock-registry-emulated:plan:v2'| +//@[058:059) Assignment |=| +//@[060:061) LeftBrace |{| +//@[061:062) NewLine |\n| + name: 'planDeploy3' +//@[002:006) Identifier |name| +//@[006:007) Colon |:| +//@[008:021) StringComplete |'planDeploy3'| +//@[021:022) NewLine |\n| + scope: rg +//@[002:007) Identifier |scope| +//@[007:008) Colon |:| +//@[009:011) Identifier |rg| +//@[011:012) NewLine |\n| + params: { +//@[002:008) Identifier |params| +//@[008:009) Colon |:| +//@[010:011) LeftBrace |{| +//@[011:012) NewLine |\n| + namePrefix: 'hello' +//@[004:014) Identifier |namePrefix| +//@[014:015) Colon |:| +//@[016:023) StringComplete |'hello'| +//@[023:024) NewLine |\n| + } +//@[002:003) RightBrace |}| +//@[003:004) NewLine |\n| +} +//@[000:001) RightBrace |}| +//@[001:003) NewLine |\n\n| + var websites = [ //@[000:003) Identifier |var| //@[004:012) Identifier |websites| @@ -275,74 +309,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:002) RightSquare |]| //@[002:004) NewLine |\n\n| -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[000:006) Identifier |module| -//@[007:018) Identifier |siteDeploy3| -//@[019:054) StringComplete |'br/mock-registry-emulated:site:v3'| -//@[055:056) Assignment |=| -//@[057:058) LeftSquare |[| -//@[058:061) Identifier |for| -//@[062:066) Identifier |site| -//@[067:069) Identifier |in| -//@[070:078) Identifier |websites| -//@[078:079) Colon |:| -//@[080:081) LeftBrace |{| -//@[081:082) NewLine |\n| - name: '${site.name}siteDeploy3' -//@[002:006) Identifier |name| -//@[006:007) Colon |:| -//@[008:011) StringLeftPiece |'${| -//@[011:015) Identifier |site| -//@[015:016) Dot |.| -//@[016:020) Identifier |name| -//@[020:033) StringRightPiece |}siteDeploy3'| -//@[033:034) NewLine |\n| - scope: rg -//@[002:007) Identifier |scope| -//@[007:008) Colon |:| -//@[009:011) Identifier |rg| -//@[011:012) NewLine |\n| - params: { -//@[002:008) Identifier |params| -//@[008:009) Colon |:| -//@[010:011) LeftBrace |{| -//@[011:012) NewLine |\n| - appPlanId: appPlanDeploy.outputs.planId -//@[004:013) Identifier |appPlanId| -//@[013:014) Colon |:| -//@[015:028) Identifier |appPlanDeploy| -//@[028:029) Dot |.| -//@[029:036) Identifier |outputs| -//@[036:037) Dot |.| -//@[037:043) Identifier |planId| -//@[043:044) NewLine |\n| - namePrefix: site.name -//@[004:014) Identifier |namePrefix| -//@[014:015) Colon |:| -//@[016:020) Identifier |site| -//@[020:021) Dot |.| -//@[021:025) Identifier |name| -//@[025:026) NewLine |\n| - dockerImage: 'nginxdemos/hello' -//@[004:015) Identifier |dockerImage| -//@[015:016) Colon |:| -//@[017:035) StringComplete |'nginxdemos/hello'| -//@[035:036) NewLine |\n| - dockerImageTag: site.tag -//@[004:018) Identifier |dockerImageTag| -//@[018:019) Colon |:| -//@[020:024) Identifier |site| -//@[024:025) Dot |.| -//@[025:028) Identifier |tag| -//@[028:029) NewLine |\n| - } -//@[002:003) RightBrace |}| -//@[003:004) NewLine |\n| -}] -//@[000:001) RightBrace |}| -//@[001:002) RightSquare |]| -//@[002:004) NewLine |\n\n| - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:006) Identifier |module| //@[007:020) Identifier |storageDeploy| From ebdae41481b88ae3f53961f498f130c6b7ae02ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 19 Apr 2026 22:35:00 +0200 Subject: [PATCH 04/21] Add check for OCI-compliant modulePath --- .../OciArtifactEmulatedReferenceTests.cs | 27 +++++++++++++++++++ .../Oci/OciArtifactEmulatedReference.cs | 13 ++++++++- .../Registry/OciArtifactRegistry.cs | 3 ++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs index 6f4f4a6e2fb..8a9ce33bd3f 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -238,5 +238,32 @@ public void TryGetOciArtifactModuleAlias_OnlyRegistrySet_ShouldSucceed() alias!.Registry.Should().Be("example.azurecr.io"); alias.FileSystem.Should().BeNull(); } + + [TestMethod] + [DataRow("../escape:1.0.0", "..")] + [DataRow("valid/../escape:1.0.0", "..")] + [DataRow(".:1.0.0", ".")] + [DataRow("UPPERCASE:1.0.0", "UPPERCASE")] + [DataRow("has spaces/module:1.0.0", "has spaces")] + [DataRow("valid/bad!segment:1.0.0", "bad!segment")] + public void TryParse_InvalidPathSegment_ShouldFail(string unqualifiedReference, string expectedBadSegment) + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + unqualifiedReference, + fileExplorer, + "myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP195"); + diagnostic.Message.Should().Contain(expectedBadSegment); + } } } diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index 3b95a813ad8..a3482100895 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -64,12 +64,14 @@ public static string ExtractModulePath(string unqualifiedReference) // configFileUri is the URI of the bicepconfig.json file, used to resolve relative paths // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") // fileExplorer is the file explorer used to create file handles + // aliasName is the name of the module alias, used in diagnostics public static ResultWithDiagnosticBuilder TryParse( BicepSourceFile referencingFile, string fileSystemPath, IOUri? configFileUri, string unqualifiedReference, - IFileExplorer fileExplorer) + IFileExplorer fileExplorer, + string? aliasName = null) { var modulePath = ExtractModulePath(unqualifiedReference); @@ -78,6 +80,15 @@ public static ResultWithDiagnosticBuilder TryParse return new(x => x.ModulePathHasNotBeenSpecified()); } + var segments = modulePath.Split('/'); + foreach (var segment in segments) + { + if (!OciArtifactReferenceFacts.IsOciNamespaceSegment(segment)) + { + return new(x => x.InvalidOciArtifactReferenceInvalidPathSegment(aliasName, unqualifiedReference, segment)); + } + } + // Resolve the filesystem base directory relative to bicepconfig.json IOUri baseUri; if (configFileUri is not null) diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 8de8475b3a0..1c2ef5d8be4 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -72,7 +72,8 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR alias.FileSystem, referencingFile.Configuration.ConfigFileUri, reference, - this.fileExplorer).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) + this.fileExplorer, + aliasName).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) { return new(emulatedFailureBuilder); } From 6eab145b0ff5f0d2ee6731c18bf884b93917cd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 19 Apr 2026 22:46:33 +0200 Subject: [PATCH 05/21] Rename FileSystemModuleRegistry to OciArtifactEmulatedRegistry and add diagnostics reserving br-fs scheme for internal use only --- src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs | 4 ++++ src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs | 2 +- ...SystemModuleRegistry.cs => OciArtifactEmulatedRegistry.cs} | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) rename src/Bicep.Core/Registry/{FileSystemModuleRegistry.cs => OciArtifactEmulatedRegistry.cs} (93%) diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index b64adee4be6..016ab2a7537 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2034,6 +2034,10 @@ public Diagnostic InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether( public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string aliasName) => CoreError( "BCP447", $"The OCI artifact module alias \"{aliasName}\" has a \"fileSystem\" property which is only supported for modules, not extensions."); + + public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( + "BCP448", + "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index 70d0f76d274..0fae64844cc 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -14,7 +14,7 @@ public DefaultArtifactRegistryProvider(IServiceProvider serviceProvider, IContai { new LocalModuleRegistry(), new OciArtifactRegistry(clientFactory, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()), - new FileSystemModuleRegistry(), + new OciArtifactEmulatedRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs b/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs similarity index 93% rename from src/Bicep.Core/Registry/FileSystemModuleRegistry.cs rename to src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs index b456468b745..8f0edf86763 100644 --- a/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs @@ -10,7 +10,7 @@ namespace Bicep.Core.Registry { - public class FileSystemModuleRegistry : ArtifactRegistry + public class OciArtifactEmulatedRegistry : ArtifactRegistry { public override string Scheme => ArtifactReferenceSchemes.OciEmulated; @@ -18,7 +18,7 @@ public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, => RegistryCapabilities.Default; public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) - => throw new NotSupportedException("Parsing is handled by OciArtifactRegistry."); + => new(x => x.ModuleReferenceSchemeBrFsNotSupported()); public override bool IsArtifactRestoreRequired(OciArtifactEmulatedReference reference) => false; From 3b0268b3945ba0f79a1d499afdb925418399d3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Mon, 20 Apr 2026 20:59:21 +0200 Subject: [PATCH 06/21] Require bicepconfig for feature OciEmulatedModuleAlias and only accept alias paths relative to bicepconfig --- .../Diagnostics/DiagnosticBuilder.cs | 4 ++++ .../Oci/OciArtifactEmulatedReference.cs | 21 +++++++------------ .../Registry/OciArtifactRegistry.cs | 5 +++++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 016ab2a7537..1e074f44abd 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2038,6 +2038,10 @@ public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string ali public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( "BCP448", "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); + + public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( + "BCP449", + $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index a3482100895..5a08f166b70 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -68,7 +68,7 @@ public static string ExtractModulePath(string unqualifiedReference) public static ResultWithDiagnosticBuilder TryParse( BicepSourceFile referencingFile, string fileSystemPath, - IOUri? configFileUri, + IOUri configFileUri, string unqualifiedReference, IFileExplorer fileExplorer, string? aliasName = null) @@ -89,20 +89,13 @@ public static ResultWithDiagnosticBuilder TryParse } } - // Resolve the filesystem base directory relative to bicepconfig.json IOUri baseUri; - if (configFileUri is not null) - { - // Ensure the fileSystem path ends with '/' so it's treated as a directory - var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') - ? fileSystemPath - : fileSystemPath + "/"; - baseUri = configFileUri.Resolve(directoryPath); - } - else - { - baseUri = IOUri.FromFilePath(fileSystemPath); - } + // Resolve the filesystem base directory relative to bicepconfig.json + // Ensure the fileSystem path ends with '/' so it's treated as a directory + var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') + ? fileSystemPath + : fileSystemPath + "/"; + baseUri = configFileUri.Resolve(directoryPath); // Construct the file URI by appending the module path with a .bicep extension. var moduleFileName = modulePath + ".bicep"; diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 1c2ef5d8be4..def160ce28e 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -67,6 +67,11 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR return new(x => x.OciArtifactModuleAliasFileSystemOnlySupportsModules(aliasName)); } + if (referencingFile.Configuration.ConfigFileUri is null) + { + return new(x => x.ConfigurationFileNotFound("OciEmulatedModuleAliases")); + } + if (!OciArtifactEmulatedReference.TryParse( referencingFile, alias.FileSystem, From 7ec58bb03ff82a6a3dd4a03a384b99db18870235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Mon, 20 Apr 2026 21:17:07 +0200 Subject: [PATCH 07/21] Update bicepconfig schema with fileSystem property for ModuleAlias --- .../schemas/bicepconfig.schema.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 6c28ca07ba1..7b66acf5c8b 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -136,8 +136,17 @@ "bicepRegistryModuleAlias": { "type": "object", "additionalProperties": false, - "required": [ - "registry" + "oneOf": [ + { + "required": [ + "registry" + ] + }, + { + "required": [ + "fileSystem" + ] + } ], "properties": { "registry": { @@ -147,6 +156,12 @@ "minLength": 1, "maxLength": 255 }, + "fileSystem": { + "title": "File System", + "description": "The path relative to bicepconfig.json used to emulate a registry alias", + "type": "string", + "minLength": 1 + }, "modulePath": { "title": "Module Path", "description": "The module path of the alias", From 2a840e49fac5b0c2819a3aead3f33394b1690ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 3 May 2026 20:57:10 +0200 Subject: [PATCH 08/21] Improve rewrite of original module alias --- .../Registry/Oci/OciArtifactEmulatedReference.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index 5a08f166b70..ca96a816954 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -16,18 +16,19 @@ namespace Bicep.Core.Registry.Oci public class OciArtifactEmulatedReference : ArtifactReference { private readonly IFileHandle fileHandle; + private readonly string modulePath; + private readonly string? fullyQualifiedReference; - public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle) : - base(referencingFile, OciArtifactReferenceFacts.EmulatedScheme) + public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle, string? fullyQualifiedReference = null) + : base(referencingFile, OciArtifactReferenceFacts.EmulatedScheme) { this.modulePath = modulePath; this.fileHandle = fileHandle; + this.fullyQualifiedReference = fullyQualifiedReference; } - private readonly string modulePath; - // Override FullyQualifiedReference so user-facing diagnostics shows "br:..." - public override string FullyQualifiedReference => $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; + public override string FullyQualifiedReference => fullyQualifiedReference ?? $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; public override string UnqualifiedReference => modulePath; From d0cd2964687bd56e83b9ff0202ad6acb4654b64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 3 May 2026 21:05:33 +0200 Subject: [PATCH 09/21] Remove unused using --- src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index ca96a816954..701ce82d7a5 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Bicep.Core.Diagnostics; -using Bicep.Core.Modules; using Bicep.Core.SourceGraph; using Bicep.IO.Abstraction; From 89de8eeca777949f7aa49e7d603de7f37777309a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Mon, 4 May 2026 06:53:09 +0200 Subject: [PATCH 10/21] Add support for absolute paths for module aliases Co-authored-by: Copilot --- .../Diagnostics/DiagnosticBuilder.cs | 6 +++++- .../Oci/OciArtifactEmulatedReference.cs | 20 ++++++++++++++++--- src/Bicep.IO/Abstraction/IOUri.cs | 3 +++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 1e074f44abd..bac6660a221 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2038,10 +2038,14 @@ public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string ali public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( "BCP448", "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); - public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( "BCP449", $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); + + public Diagnostic InvalidOciArtifactModuleAliasFileSystemPath(string? aliasName, string path, string reason) => CoreError( + "BCP450", + $"The OCI artifact module alias{(aliasName is not null ? $" \"{aliasName}\"" : "")} has an invalid \"fileSystem\" path \"{path}\": {reason}"); + } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index 701ce82d7a5..d1f3ad5776d 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -90,12 +90,26 @@ public static ResultWithDiagnosticBuilder TryParse } IOUri baseUri; - // Resolve the filesystem base directory relative to bicepconfig.json - // Ensure the fileSystem path ends with '/' so it's treated as a directory + // Ensure the fileSystem path ends with '/' so it's treated as a directory. var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') ? fileSystemPath : fileSystemPath + "/"; - baseUri = configFileUri.Resolve(directoryPath); + + if (IOUri.IsAbsoluteFilePath(fileSystemPath)) + { + try + { + baseUri = IOUri.FromFilePath(directoryPath); + } + catch (IOException ex) + { + return new(x => x.InvalidOciArtifactModuleAliasFileSystemPath(aliasName, fileSystemPath, ex.Message)); + } + } + else + { + baseUri = configFileUri.Resolve(directoryPath); + } // Construct the file URI by appending the module path with a .bicep extension. var moduleFileName = modulePath + ".bicep"; diff --git a/src/Bicep.IO/Abstraction/IOUri.cs b/src/Bicep.IO/Abstraction/IOUri.cs index 49e33f7f67b..e056da52e1b 100644 --- a/src/Bicep.IO/Abstraction/IOUri.cs +++ b/src/Bicep.IO/Abstraction/IOUri.cs @@ -71,6 +71,9 @@ public IOUri(IOUriScheme scheme, string? authority, string path, string query = public static implicit operator string(IOUri uri) => uri.ToString(); + public static bool IsAbsoluteFilePath(string path) => + FilePath.IsPathFullyQualified(path) || path.StartsWith('/') || path.StartsWith('\\'); + public static IOUri FromFilePath(string filePath) { if (!FilePath.IsPathFullyQualified(filePath) && !(filePath.StartsWith('/') || filePath.StartsWith('\\'))) From ab422c1935a40d32bdf503ecd1b63080ffe94172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Wed, 3 Jun 2026 10:37:00 +0200 Subject: [PATCH 11/21] Move configuration to ModuleAliasMock --- .../baselines/Registry_LF/bicepconfig.json | 7 + .../ConfigurationManagerTests.cs | 81 +++++++++++ .../Configuration/RootConfigurationTests.cs | 1 + .../UseRecentApiVersionRuleTests.cs | 1 + .../AnalyzersConfigurationExtensions.cs | 2 + .../ExperimentalFeaturesExtensions.cs | 1 + .../ExtensionsConfigurationExtensions.cs | 2 + .../Configuration/RootConfiguration.cs | 76 +++++++++- src/Bicep.Core/Configuration/bicepconfig.json | 134 +++++++++--------- .../Modules/TemplateSpecModuleReference.cs | 2 +- .../Registry/Oci/OciArtifactReference.cs | 2 +- .../Registry/OciArtifactRegistry.cs | 2 +- .../ModuleReferenceCompletionProvider.cs | 7 +- .../schemas/bicepconfig.schema.json | 57 ++++++-- 14 files changed, 292 insertions(+), 83 deletions(-) diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json index 161bd860fd4..af9d06d82a8 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json @@ -14,6 +14,13 @@ "registry": "mock-registry-two.invalid", "modulePath": "demo" }, + "mock-registry-emulated": { + "registry": "mock-registry-three.invalid" + } + } + }, + "moduleAliasesMock": { + "br": { "mock-registry-emulated": { "fileSystem": "Publish" } diff --git a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs index 85677b3c181..04e19f3d5fd 100644 --- a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -64,6 +64,10 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA } } }, + "moduleAliasesMock": { + "ts": {}, + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -173,6 +177,10 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat } } }, + "moduleAliasesMock": { + "ts": {}, + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -249,6 +257,10 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration } } }, + "moduleAliasesMock": { + "ts": {}, + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -424,6 +436,10 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf } } }, + "moduleAliasesMock": { + "ts": {}, + "br": {} + }, "extensions": { "kubernetes": "builtin:", "az": "builtin:" @@ -781,6 +797,10 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat } } }, + "moduleAliasesMock": { + "ts": {}, + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -873,5 +893,66 @@ public void Bicepconfig_resolution_is_a_merge_between_closest_bicepconfig_file_a configuration.ModuleAliases.TryGetOciArtifactModuleAlias("public").IsSuccess(out var moduleAlias).Should().BeTrue(); moduleAlias!.Registry.Should().Be("mcr.microsoft.com"); } + + [TestMethod] + public void GetConfiguration_ModuleAliasesMock_SupersedesModuleAliasesForSameAlias() + { + // Arrange. + var fileSet = InMemoryTestFileSet.Create(("bicepconfig.json", """ + { + "moduleAliases": { + "br": { + "myAlias": { "registry": "real.azurecr.io", "modulePath": "real/path" } + } + }, + "moduleAliasesMock": { + "br": { + "myAlias": { "fileSystem": "mock/path" } + } + } + } + """)); + var sut = new ConfigurationManager(fileSet.FileExplorer); + + // Act. + var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); + + // Assert. + configuration.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue(); + alias!.FileSystem.Should().Be("mock/path"); + + // The merged view should expose the mock definition without throwing on duplicate keys. + var merged = configuration.GetOciArtifactModuleAliases(); + merged.Should().ContainKey("myAlias"); + merged["myAlias"].FileSystem.Should().Be("mock/path"); + } + + [TestMethod] + public void GetConfiguration_ModuleAliasesMock_FallsBackToModuleAliasesWhenAliasNotInMock() + { + // Arrange. + var fileSet = InMemoryTestFileSet.Create(("bicepconfig.json", """ + { + "moduleAliases": { + "br": { + "myAlias": { "registry": "real.azurecr.io" } + } + }, + "moduleAliasesMock": { + "br": { + "otherAlias": { "fileSystem": "mock/path" } + } + } + } + """)); + var sut = new ConfigurationManager(fileSet.FileExplorer); + + // Act. + var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); + + // Assert. + configuration.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue(); + alias!.Registry.Should().Be("real.azurecr.io"); + } } } diff --git a/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs b/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs index 7a1e3301831..394087fceb7 100644 --- a/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs @@ -17,6 +17,7 @@ public void RootConfiguration_LeadingTildeInCacheRootDirectory_ExpandPath(string var configuration = new RootConfiguration( BicepTestConstants.BuiltInConfiguration.Cloud, BicepTestConstants.BuiltInConfiguration.ModuleAliases, + BicepTestConstants.BuiltInConfiguration.ModuleAliasesMock, BicepTestConstants.BuiltInConfiguration.Extensions, BicepTestConstants.BuiltInConfiguration.ImplicitExtensions, BicepTestConstants.BuiltInConfiguration.Analyzers, diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs index 09e66dc48aa..e816359b147 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs @@ -105,6 +105,7 @@ private static RootConfiguration CreateConfigurationWithFakeToday(RootConfigurat return new RootConfiguration( original.Cloud, original.ModuleAliases, + original.ModuleAliasesMock, original.Extensions, original.ImplicitExtensions, new AnalyzersConfiguration( diff --git a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs index ada8e48fa5e..2170f6cc41e 100644 --- a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs @@ -40,6 +40,7 @@ public static RootConfiguration WithAnalyzersConfiguration(this RootConfiguratio new( current.Cloud, current.ModuleAliases, + current.ModuleAliasesMock, current.Extensions, current.ImplicitExtensions, analyzersConfiguration, @@ -63,6 +64,7 @@ public static RootConfiguration WithCloudConfiguration(this RootConfiguration cu new( cloudConfiguration, current.ModuleAliases, + current.ModuleAliasesMock, current.Extensions, current.ImplicitExtensions, current.Analyzers, diff --git a/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs b/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs index f30c7baab06..8e34ce80e59 100644 --- a/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs +++ b/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs @@ -9,6 +9,7 @@ public static RootConfiguration WithExperimentalFeaturesConfiguration(this RootC new( current.Cloud, current.ModuleAliases, + current.ModuleAliasesMock, current.Extensions, current.ImplicitExtensions, current.Analyzers, diff --git a/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs b/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs index 3320d14f5b7..7380759c4c5 100644 --- a/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs @@ -18,6 +18,7 @@ public static RootConfiguration WithExtensions(this RootConfiguration rootConfig return new RootConfiguration( rootConfiguration.Cloud, rootConfiguration.ModuleAliases, + rootConfiguration.ModuleAliasesMock, rootConfiguration.Extensions.WithExtensions(payload), rootConfiguration.ImplicitExtensions, rootConfiguration.Analyzers, @@ -34,6 +35,7 @@ public static RootConfiguration WithImplicitExtensions(this RootConfiguration ro return new RootConfiguration( rootConfiguration.Cloud, rootConfiguration.ModuleAliases, + rootConfiguration.ModuleAliasesMock, rootConfiguration.Extensions, rootConfiguration.ImplicitExtensions.WithImplicitExtensions(payload), rootConfiguration.Analyzers, diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index f8727bf206e..2f341462adb 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -7,6 +7,7 @@ using System.Text.Json; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Json; using Bicep.IO.Abstraction; namespace Bicep.Core.Configuration @@ -17,6 +18,8 @@ public class RootConfiguration public const string ModuleAliasesKey = "moduleAliases"; + public const string ModuleAliasesMockKey = "moduleAliasesMock"; + public const string ExtensionsKey = "extensions"; public const string ImplicitExtensionsKey = "implicitExtensions"; @@ -34,6 +37,7 @@ public class RootConfiguration public RootConfiguration( CloudConfiguration cloud, ModuleAliasesConfiguration moduleAliases, + ModuleAliasesConfiguration moduleAliasesMock, ExtensionsConfiguration extensions, ImplicitExtensionsConfiguration implicitExtensions, AnalyzersConfiguration analyzers, @@ -46,6 +50,7 @@ public RootConfiguration( { this.Cloud = cloud; this.ModuleAliases = moduleAliases; + this.ModuleAliasesMock = moduleAliasesMock; this.Extensions = extensions; this.ImplicitExtensions = implicitExtensions; this.Analyzers = analyzers; @@ -61,6 +66,10 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = { var cloud = CloudConfiguration.Bind(element.GetProperty(CloudKey)); var moduleAliases = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesKey), configFileUri); + var moduleAliasesMock = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesMockKey), configFileUri); + // var moduleAliasesMock = element.TryGetProperty(ModuleAliasesMockKey, out var mockElement) + // ? ModuleAliasesConfiguration.Bind(mockElement, configFileUri) + // : ModuleAliasesConfiguration.Bind(JsonElementFactory.CreateElement(new ModuleAliases()), configFileUri); var analyzers = new AnalyzersConfiguration(element.GetProperty(AnalyzersKey)); var cacheRootDirectory = element.TryGetProperty(CacheRootDirectoryKey, out var e) ? e.GetString() : default; var experimentalFeaturesWarning = element.TryGetProperty(ExperimentalFeaturesWarningKey, out var value) && value.GetBoolean(); @@ -70,13 +79,15 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = var extensions = ExtensionsConfiguration.Bind(element.GetProperty(ExtensionsKey)); var implicitExtensions = ImplicitExtensionsConfiguration.Bind(element.GetProperty(ImplicitExtensionsKey)); - return new(cloud, moduleAliases, extensions, implicitExtensions, analyzers, cacheRootDirectory, experimentalFeaturesWarning, experimentalFeaturesEnabled, formatting, configFileUri, null); + return new(cloud, moduleAliases, moduleAliasesMock, extensions, implicitExtensions, analyzers, cacheRootDirectory, experimentalFeaturesWarning, experimentalFeaturesEnabled, formatting, configFileUri, null); } public CloudConfiguration Cloud { get; } public ModuleAliasesConfiguration ModuleAliases { get; } + public ModuleAliasesConfiguration ModuleAliasesMock { get; } + public ExtensionsConfiguration Extensions { get; } public ImplicitExtensionsConfiguration ImplicitExtensions { get; } @@ -97,9 +108,68 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = public bool IsBuiltIn => ConfigFileUri is null; + /// + /// Gets an OCI artifact module alias. If the alias is defined in moduleAliasesMock, that definition supersedes + /// the one in moduleAliases. Otherwise, the alias is resolved from moduleAliases. + /// + public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAlias(string aliasName) + { + if (this.ModuleAliasesMock.GetOciArtifactModuleAliases().ContainsKey(aliasName)) + { + return this.ModuleAliasesMock.TryGetOciArtifactModuleAlias(aliasName); + } + + return this.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName); + } + + /// + /// Gets a template spec module alias. If the alias is defined in moduleAliasesMock, that definition supersedes + /// the one in moduleAliases. Otherwise, the alias is resolved from moduleAliases. + /// + public ResultWithDiagnosticBuilder TryGetTemplateSpecModuleAlias(string aliasName) + { + if (this.ModuleAliasesMock.GetTemplateSpecModuleAliases().ContainsKey(aliasName)) + { + return this.ModuleAliasesMock.TryGetTemplateSpecModuleAlias(aliasName); + } + + return this.ModuleAliases.TryGetTemplateSpecModuleAlias(aliasName); + } + + /// + /// Gets all OCI artifact module aliases, merging moduleAliases with moduleAliasesMock. Aliases defined in + /// moduleAliasesMock supersede those with the same name in moduleAliases. + /// + public ImmutableSortedDictionary GetOciArtifactModuleAliases() + { + var merged = this.ModuleAliases.GetOciArtifactModuleAliases(); + foreach (var (aliasName, alias) in this.ModuleAliasesMock.GetOciArtifactModuleAliases()) + { + merged = merged.SetItem(aliasName, alias); + } + + return merged; + } + + /// + /// Gets all template spec module aliases, merging moduleAliases with moduleAliasesMock. Aliases defined in + /// moduleAliasesMock supersede those with the same name in moduleAliases. + /// + public ImmutableSortedDictionary GetTemplateSpecModuleAliases() + { + var merged = this.ModuleAliases.GetTemplateSpecModuleAliases(); + foreach (var (aliasName, alias) in this.ModuleAliasesMock.GetTemplateSpecModuleAliases()) + { + merged = merged.SetItem(aliasName, alias); + } + + return merged; + } + public RootConfiguration With( CloudConfiguration? cloud = null, ModuleAliasesConfiguration? moduleAliases = null, + ModuleAliasesConfiguration? moduleAliasesMock = null, ExtensionsConfiguration? extensions = null, ImplicitExtensionsConfiguration? implicitExtensions = null, AnalyzersConfiguration? analyzers = null, @@ -113,6 +183,7 @@ public RootConfiguration With( return new RootConfiguration( cloud ?? this.Cloud, moduleAliases ?? this.ModuleAliases, + moduleAliasesMock ?? this.ModuleAliasesMock, extensions ?? this.Extensions, implicitExtensions ?? this.ImplicitExtensions, analyzers ?? this.Analyzers, @@ -137,6 +208,9 @@ public string ToUtf8Json() writer.WritePropertyName(ModuleAliasesKey); this.ModuleAliases.WriteTo(writer); + writer.WritePropertyName(ModuleAliasesMockKey); + this.ModuleAliasesMock.WriteTo(writer); + writer.WritePropertyName(ExtensionsKey); this.Extensions.WriteTo(writer); diff --git a/src/Bicep.Core/Configuration/bicepconfig.json b/src/Bicep.Core/Configuration/bicepconfig.json index 5ece485979c..f3ccb8545fb 100644 --- a/src/Bicep.Core/Configuration/bicepconfig.json +++ b/src/Bicep.Core/Configuration/bicepconfig.json @@ -1,66 +1,70 @@ -{ - // 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 - - "cloud": { - "currentProfile": "AzureCloud", - "profiles": { - "AzureCloud": { - "resourceManagerEndpoint": "https://management.azure.com", - "activeDirectoryAuthority": "https://login.microsoftonline.com" - }, - "AzureChinaCloud": { - "resourceManagerEndpoint": "https://management.chinacloudapi.cn", - "activeDirectoryAuthority": "https://login.chinacloudapi.cn" - }, - "AzureUSGovernment": { - "resourceManagerEndpoint": "https://management.usgovcloudapi.net", - "activeDirectoryAuthority": "https://login.microsoftonline.us" - } - }, - "credentialPrecedence": ["AzureCLI", "AzurePowerShell"] - }, - "moduleAliases": { - "ts": {}, - "br": { - "public": { - "registry": "mcr.microsoft.com", - "modulePath": "bicep" - } - } - }, - "extensions": { - "az": "builtin:", - "kubernetes": "builtin:" - }, - "implicitExtensions": ["az"], - "analyzers": { - "core": { - "verbose": false, - "enabled": true, - "rules": { - "no-hardcoded-env-urls": { - "level": "warning", - "disallowedhosts": [ - "azuredatalakeanalytics.net", - "azuredatalakestore.net", - "batch.core.windows.net", - "core.windows.net", - "database.windows.net", - "datalake.azure.net", - "gallery.azure.com", - "graph.windows.net", - "login.microsoftonline.com", - "management.azure.com", - "management.core.windows.net", - "vault.azure.net" - ], - "excludedhosts": ["schema.management.azure.com"] - } - } - } - }, +{ + // 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 + + "cloud": { + "currentProfile": "AzureCloud", + "profiles": { + "AzureCloud": { + "resourceManagerEndpoint": "https://management.azure.com", + "activeDirectoryAuthority": "https://login.microsoftonline.com" + }, + "AzureChinaCloud": { + "resourceManagerEndpoint": "https://management.chinacloudapi.cn", + "activeDirectoryAuthority": "https://login.chinacloudapi.cn" + }, + "AzureUSGovernment": { + "resourceManagerEndpoint": "https://management.usgovcloudapi.net", + "activeDirectoryAuthority": "https://login.microsoftonline.us" + } + }, + "credentialPrecedence": ["AzureCLI", "AzurePowerShell"] + }, + "moduleAliases": { + "ts": {}, + "br": { + "public": { + "registry": "mcr.microsoft.com", + "modulePath": "bicep" + } + } + }, + "moduleAliasesMock": { + "ts": {}, + "br": {} + }, + "extensions": { + "az": "builtin:", + "kubernetes": "builtin:" + }, + "implicitExtensions": ["az"], + "analyzers": { + "core": { + "verbose": false, + "enabled": true, + "rules": { + "no-hardcoded-env-urls": { + "level": "warning", + "disallowedhosts": [ + "azuredatalakeanalytics.net", + "azuredatalakestore.net", + "batch.core.windows.net", + "core.windows.net", + "database.windows.net", + "datalake.azure.net", + "gallery.azure.com", + "graph.windows.net", + "login.microsoftonline.com", + "management.azure.com", + "management.core.windows.net", + "vault.azure.net" + ], + "excludedhosts": ["schema.management.azure.com"] + } + } + } + }, "experimentalFeaturesWarning": true, - "experimentalFeaturesEnabled": {}, - "formatting": {} -} + "experimentalFeaturesEnabled": {}, + "formatting": {} +} diff --git a/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs b/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs index 30fe759531b..339b8894924 100644 --- a/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs +++ b/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs @@ -74,7 +74,7 @@ public static ResultWithDiagnosticBuilder TryParse( { if (aliasName is not null) { - if (!configuration.ModuleAliases.TryGetTemplateSpecModuleAlias(aliasName).IsSuccess(out var alias, out var errorBuilder)) + if (!configuration.TryGetTemplateSpecModuleAlias(aliasName).IsSuccess(out var alias, out var errorBuilder)) { return new(errorBuilder); } diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs index 993b4347032..3d0b0ee1fa0 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs @@ -123,7 +123,7 @@ private static ResultWithDiagnosticBuilder TryPars switch (type) { case ArtifactType.Module: - if (!configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var moduleAlias, out var moduleFailureBuilder)) + if (!configuration.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var moduleAlias, out var moduleFailureBuilder)) { return new(moduleFailureBuilder); } diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index ceb06e6d8d0..ce9b41fbdd5 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -57,7 +57,7 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR // Check if the alias resolves to a filesystem-based alias. if (aliasName is not null) { - if (!referencingFile.Configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var alias, out var aliasFailureBuilder)) + if (!referencingFile.Configuration.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var alias, out var aliasFailureBuilder)) { return new(aliasFailureBuilder); } diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 525fa4258f7..9e4c80356a9 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -251,7 +251,7 @@ private IEnumerable GetTopLevelCompletions(BicepCompletionContex List completionItems = new(); - var templateSpecModuleAliases = rootConfiguration.ModuleAliases.GetTemplateSpecModuleAliases(); + var templateSpecModuleAliases = rootConfiguration.GetTemplateSpecModuleAliases(); var bicepModuleAliases = GetModuleAliases(rootConfiguration); // Top-level TemplateSpec completions @@ -375,7 +375,7 @@ private async Task> GetVersionCompletions(BicepCompl private static ImmutableSortedDictionary GetModuleAliases(RootConfiguration configuration) { - return configuration.ModuleAliases.GetOciArtifactModuleAliases(); + return configuration.GetOciArtifactModuleAliases(); } private static bool TryGetValidModuleAlias( @@ -387,7 +387,8 @@ private static bool TryGetValidModuleAlias( registry = null; modulePath = null; - if (configuration.ModuleAliases.GetOciArtifactModuleAliases().TryGetValue(aliasName, out var aliasConfig) + // Mock aliases supersede real aliases with the same name. + if (configuration.GetOciArtifactModuleAliases().TryGetValue(aliasName, out var aliasConfig) && !string.IsNullOrWhiteSpace(aliasConfig.Registry)) { registry = aliasConfig.Registry; diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 4c29b8b9a2d..2b181a208d4 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -136,17 +136,8 @@ "bicepRegistryModuleAlias": { "type": "object", "additionalProperties": false, - "oneOf": [ - { - "required": [ - "registry" - ] - }, - { - "required": [ - "fileSystem" - ] - } + "required": [ + "registry" ], "properties": { "registry": { @@ -156,6 +147,21 @@ "minLength": 1, "maxLength": 255 }, + "modulePath": { + "title": "Module Path", + "description": "The module path of the alias", + "type": "string", + "minLength": 1 + } + } + }, + "bicepRegistryModuleAliasMock": { + "type": "object", + "additionalProperties": false, + "required": [ + "fileSystem" + ], + "properties": { "fileSystem": { "title": "File System", "description": "The path relative to bicepconfig.json used to emulate a registry alias", @@ -303,6 +309,35 @@ } } }, + "moduleAliasesMock": { + "title": "Module Aliases Mock", + "description": "Mock module aliases that supersede moduleAliases when present", + "type": "object", + "additionalProperties": false, + "default": { + "ts": {}, + "br": {} + }, + "properties": { + "ts": { + "title": "Template Spec Module Aliases Mock", + "description": "Mock Template Spec module alias definitions", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/templateSpecModuleAlias", + "additionalProperties": false + } + }, + "br": { + "title": "Bicep Registry Module Aliases Mock", + "description": "Mock Bicep Registry module alias definitions", + "additionalProperties": { + "$ref": "#/definitions/bicepRegistryModuleAliasMock", + "additionalProperties": false + } + } + } + }, "extensions": { "title": "Bicep Extensions", "description": "Bicep extension references", From b5a6494109e678ff9f3db98d7b96cdec0b781f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Wed, 3 Jun 2026 13:50:18 +0200 Subject: [PATCH 12/21] Rename anything Emulated to Mocked --- .../baselines/Registry_LF/bicepconfig.json | 4 ++-- .../Files/baselines/Registry_LF/main.bicep | 2 +- .../Registry_LF/main.diagnostics.bicep | 2 +- .../Registry_LF/main.formatted.bicep | 2 +- .../Files/baselines/Registry_LF/main.ir.bicep | 2 +- .../Registry_LF/main.sourcemap.bicep | 2 +- .../baselines/Registry_LF/main.symbols.bicep | 2 +- .../baselines/Registry_LF/main.syntax.bicep | 4 ++-- .../baselines/Registry_LF/main.tokens.bicep | 4 ++-- .../OciArtifactEmulatedReferenceTests.cs | 24 +++++++++---------- .../Diagnostics/DiagnosticBuilder.cs | 2 +- .../Modules/ModuleReferenceSchemes.cs | 2 +- .../DefaultArtifactRegistryProvider.cs | 2 +- ...rence.cs => OciArtifactMockedReference.cs} | 14 +++++------ .../Registry/Oci/OciArtifactReferenceFacts.cs | 2 +- ...Registry.cs => OciArtifactMockRegistry.cs} | 22 ++++++++--------- .../Registry/OciArtifactRegistry.cs | 10 ++++---- 17 files changed, 51 insertions(+), 51 deletions(-) rename src/Bicep.Core/Registry/Oci/{OciArtifactEmulatedReference.cs => OciArtifactMockedReference.cs} (89%) rename src/Bicep.Core/Registry/{OciArtifactEmulatedRegistry.cs => OciArtifactMockRegistry.cs} (73%) diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json index af9d06d82a8..a0ccd1c03c6 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json @@ -14,14 +14,14 @@ "registry": "mock-registry-two.invalid", "modulePath": "demo" }, - "mock-registry-emulated": { + "mock-registry-mocked": { "registry": "mock-registry-three.invalid" } } }, "moduleAliasesMock": { "br": { - "mock-registry-emulated": { + "mock-registry-mocked": { "fileSystem": "Publish" } } diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep index db4d3503e0f..f7f10f261e0 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep @@ -21,7 +21,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { name: 'planDeploy3' scope: rg params: { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep index 7d4f17d9542..d246828887f 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep @@ -21,7 +21,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { name: 'planDeploy3' scope: rg params: { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep index c7a28bb80ad..8348128e843 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep @@ -21,7 +21,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { name: 'planDeploy3' scope: rg params: { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index fcd302c7f41..fd1ff47bd19 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -76,7 +76,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { //@[000:0137) ├─DeclaredModuleExpression //@[060:0137) | ├─ObjectExpression name: 'planDeploy3' diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep index 529e8a6ea67..64c762a0aeb 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep @@ -149,7 +149,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { //@ { //@ "type": "Microsoft.Resources/deployments", //@ "apiVersion": "2025-04-01", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index fea26bf29d8..71709130e80 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -24,7 +24,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { //@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 137 name: 'planDeploy3' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index 55f64164bc5..fb9f9957978 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -147,13 +147,13 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:0001) | └─Token(RightBrace) |}| //@[001:0003) ├─Token(NewLine) |\n\n| -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { //@[000:0137) ├─ModuleDeclarationSyntax //@[000:0006) | ├─Token(Identifier) |module| //@[007:0021) | ├─IdentifierSyntax //@[007:0021) | | └─Token(Identifier) |appPlanDeploy3| //@[022:0057) | ├─StringSyntax -//@[022:0057) | | └─Token(StringComplete) |'br/mock-registry-emulated:plan:v2'| +//@[022:0057) | | └─Token(StringComplete) |'br/mock-registry-mocked:plan:v2'| //@[058:0059) | ├─Token(Assignment) |=| //@[060:0137) | └─ObjectSyntax //@[060:0061) | ├─Token(LeftBrace) |{| diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index ff81a47d607..051eee22739 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -97,10 +97,10 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:001) RightBrace |}| //@[001:003) NewLine |\n\n| -module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { //@[000:006) Identifier |module| //@[007:021) Identifier |appPlanDeploy3| -//@[022:057) StringComplete |'br/mock-registry-emulated:plan:v2'| +//@[022:057) StringComplete |'br/mock-registry-mocked:plan:v2'| //@[058:059) Assignment |=| //@[060:061) LeftBrace |{| //@[061:062) NewLine |\n| diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs index ae942c39e5e..1fa7de51bfe 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -17,7 +17,7 @@ namespace Bicep.Core.UnitTests.Registry.Oci { [TestClass] - public class OciArtifactEmulatedReferenceTests + public class OciArtifactMockedReferenceTests { [TestMethod] [DataRow("keyvault:1.0.0", "keyvault")] @@ -31,7 +31,7 @@ public class OciArtifactEmulatedReferenceTests [DataRow("@sha256:abc", "")] public void ExtractModulePath_ShouldExtractCorrectPath(string input, string expected) { - OciArtifactEmulatedReference.ExtractModulePath(input).Should().Be(expected); + OciArtifactMockedReference.ExtractModulePath(input).Should().Be(expected); } [TestMethod] @@ -41,7 +41,7 @@ public void TryParse_EmptyModulePath_ShouldFail() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result = OciArtifactEmulatedReference.TryParse( + var result = OciArtifactMockedReference.TryParse( referencingFile, "./modules", configFileUri, @@ -60,7 +60,7 @@ public void TryParse_ValidModulePath_ShouldSucceed() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result = OciArtifactEmulatedReference.TryParse( + var result = OciArtifactMockedReference.TryParse( referencingFile, "../bicepModules", configFileUri, @@ -81,7 +81,7 @@ public void TryParse_MultiSegmentModulePath_ShouldSucceed() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result = OciArtifactEmulatedReference.TryParse( + var result = OciArtifactMockedReference.TryParse( referencingFile, "./modules", configFileUri, @@ -100,7 +100,7 @@ public void TryParse_DigestReference_ShouldIgnoreDigestAndSucceed() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result = OciArtifactEmulatedReference.TryParse( + var result = OciArtifactMockedReference.TryParse( referencingFile, "./modules", configFileUri, @@ -119,7 +119,7 @@ public void TryGetEntryPointFileHandle_ShouldReturnFileHandle() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var parseResult = OciArtifactEmulatedReference.TryParse( + var parseResult = OciArtifactMockedReference.TryParse( referencingFile, "../bicepModules", configFileUri, @@ -142,9 +142,9 @@ public void Equals_SameReferences_ShouldBeEqual() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result1 = OciArtifactEmulatedReference.TryParse( + var result1 = OciArtifactMockedReference.TryParse( referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); - var result2 = OciArtifactEmulatedReference.TryParse( + var result2 = OciArtifactMockedReference.TryParse( referencingFile, "../bicepModules", configFileUri, "keyvault:2.0.0", fileExplorer); result1.IsSuccess(out var ref1, out _).Should().BeTrue(); @@ -161,9 +161,9 @@ public void Equals_DifferentModulePaths_ShouldNotBeEqual() var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result1 = OciArtifactEmulatedReference.TryParse( + var result1 = OciArtifactMockedReference.TryParse( referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); - var result2 = OciArtifactEmulatedReference.TryParse( + var result2 = OciArtifactMockedReference.TryParse( referencingFile, "../bicepModules", configFileUri, "storage:1.0.0", fileExplorer); result1.IsSuccess(out var ref1, out _).Should().BeTrue(); @@ -252,7 +252,7 @@ public void TryParse_InvalidPathSegment_ShouldFail(string unqualifiedReference, var referencingFile = BicepTestConstants.CreateDummyBicepFile(); var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); - var result = OciArtifactEmulatedReference.TryParse( + var result = OciArtifactMockedReference.TryParse( referencingFile, "./modules", configFileUri, diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 61eee9c1e26..150bd0ac93e 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -991,7 +991,7 @@ public Diagnostic ReferencedArmTemplateHasErrors() => CoreError( public Diagnostic UnknownModuleReferenceScheme(string badScheme, ImmutableArray allowedSchemes) { - string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local) && scheme != ArtifactReferenceSchemes.OciEmulated)); + string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local) && scheme != ArtifactReferenceSchemes.OciMocked)); return CoreError( "BCP189", diff --git a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs index b399a229042..d52caf49697 100644 --- a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs +++ b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs @@ -11,7 +11,7 @@ public static class ArtifactReferenceSchemes public const string Oci = OciArtifactReferenceFacts.Scheme; - public const string OciEmulated = OciArtifactReferenceFacts.EmulatedScheme; + public const string OciMocked = OciArtifactReferenceFacts.MockedScheme; public const string TemplateSpecs = "ts"; } diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index 5fc90b74551..a90ef3c7a0b 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -14,7 +14,7 @@ public DefaultArtifactRegistryProvider(RegistryConfiguration registryConfigurati { new LocalModuleRegistry(), new OciArtifactRegistry(registryConfiguration, clientFactory, publicModuleMetadataProvider, fileExplorer), - new OciArtifactEmulatedRegistry(), + new OciArtifactMockedRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs similarity index 89% rename from src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs rename to src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs index fa1b6dcd008..52d124aff43 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs @@ -8,18 +8,18 @@ namespace Bicep.Core.Registry.Oci { /// - /// Represents an OCI module reference that is emulated via a local filesystem path. + /// Represents an OCI module reference that is mocked via a local filesystem path. /// When a module alias specifies a "fileSystem" property instead of "registry", /// module references are resolved to local .bicep files instead of pulling from a container registry. /// - public class OciArtifactEmulatedReference : ArtifactReference + public class OciArtifactMockedReference : ArtifactReference { private readonly IFileHandle fileHandle; private readonly string modulePath; private readonly string? fullyQualifiedReference; - public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle, string? fullyQualifiedReference = null) - : base(referencingFile.Features, referencingFile.Configuration, OciArtifactReferenceFacts.EmulatedScheme) + public OciArtifactMockedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle, string? fullyQualifiedReference = null) + : base(referencingFile.Features, referencingFile.Configuration, OciArtifactReferenceFacts.MockedScheme) { this.modulePath = modulePath; this.fileHandle = fileHandle; @@ -65,7 +65,7 @@ public static string ExtractModulePath(string unqualifiedReference) // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") // fileExplorer is the file explorer used to create file handles // aliasName is the name of the module alias, used in diagnostics - public static ResultWithDiagnosticBuilder TryParse( + public static ResultWithDiagnosticBuilder TryParse( BicepSourceFile referencingFile, string fileSystemPath, IOUri configFileUri, @@ -117,12 +117,12 @@ public static ResultWithDiagnosticBuilder TryParse var fileHandle = fileExplorer.GetFile(moduleFileUri); - return new(new OciArtifactEmulatedReference(referencingFile, modulePath, fileHandle)); + return new(new OciArtifactMockedReference(referencingFile, modulePath, fileHandle)); } public override bool Equals(object? obj) { - if (obj is not OciArtifactEmulatedReference other) + if (obj is not OciArtifactMockedReference other) { return false; } diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs index 8a52a0ee355..44dd5c7fff2 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs @@ -9,7 +9,7 @@ public static partial class OciArtifactReferenceFacts { public const string Scheme = "br"; - public const string EmulatedScheme = "br-fs"; + public const string MockedScheme = "br-mock"; public const string SchemeWithColon = Scheme + ":"; diff --git a/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs b/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs similarity index 73% rename from src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs rename to src/Bicep.Core/Registry/OciArtifactMockRegistry.cs index 8f0edf86763..083c9334c14 100644 --- a/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs @@ -10,38 +10,38 @@ namespace Bicep.Core.Registry { - public class OciArtifactEmulatedRegistry : ArtifactRegistry + public class OciArtifactMockedRegistry : ArtifactRegistry { - public override string Scheme => ArtifactReferenceSchemes.OciEmulated; + public override string Scheme => ArtifactReferenceSchemes.OciMocked; - public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, OciArtifactEmulatedReference reference) + public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, OciArtifactMockedReference reference) => RegistryCapabilities.Default; public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) => new(x => x.ModuleReferenceSchemeBrFsNotSupported()); - public override bool IsArtifactRestoreRequired(OciArtifactEmulatedReference reference) => false; + public override bool IsArtifactRestoreRequired(OciArtifactMockedReference reference) => false; - public override Task CheckArtifactExists(ArtifactType artifactType, OciArtifactEmulatedReference reference) + public override Task CheckArtifactExists(ArtifactType artifactType, OciArtifactMockedReference reference) => Task.FromResult(reference.TryGetEntryPointFileHandle().IsSuccess(out var fileHandle, out _) && fileHandle.Exists()); - public override Task> RestoreArtifacts(IEnumerable references) + public override Task> RestoreArtifacts(IEnumerable references) => Task.FromResult>( new Dictionary()); - public override Task> InvalidateArtifactsCache(IEnumerable references) + public override Task> InvalidateArtifactsCache(IEnumerable references) => Task.FromResult>( new Dictionary()); - public override Task PublishModule(OciArtifactEmulatedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) + public override Task PublishModule(OciArtifactMockedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); - public override Task PublishExtension(OciArtifactEmulatedReference reference, ExtensionPackage package) + public override Task PublishExtension(OciArtifactMockedReference reference, ExtensionPackage package) => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); - public override string? TryGetDocumentationUri(OciArtifactEmulatedReference reference) => null; + public override string? TryGetDocumentationUri(OciArtifactMockedReference reference) => null; - public override Task TryGetModuleDescription(ModuleSymbol module, OciArtifactEmulatedReference reference) + public override Task TryGetModuleDescription(ModuleSymbol module, OciArtifactMockedReference reference) => Task.FromResult(null); } } diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index ce9b41fbdd5..80a17765daa 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -71,21 +71,21 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR if (referencingFile.Configuration.ConfigFileUri is null) { - return new(x => x.ConfigurationFileNotFound("OciEmulatedModuleAliases")); + return new(x => x.ConfigurationFileNotFound("OciModuleAliasesMock")); } - if (!OciArtifactEmulatedReference.TryParse( + if (!OciArtifactMockedReference.TryParse( referencingFile, alias.FileSystem, referencingFile.Configuration.ConfigFileUri, reference, this.fileExplorer, - aliasName).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) + aliasName).IsSuccess(out var mockedRef, out var mockedFailureBuilder)) { - return new(emulatedFailureBuilder); + return new(mockedFailureBuilder); } - return new(emulatedRef); + return new(mockedRef); } } From 3102064dd6766fdd9253d4e13d7727ad14446082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Fri, 5 Jun 2026 13:00:38 +0200 Subject: [PATCH 13/21] Cleanup ts: references since template specs are not supported for mocks --- .../Configuration/ConfigurationManagerTests.cs | 5 ----- src/Bicep.Core/Configuration/bicepconfig.json | 1 - src/vscode-bicep/schemas/bicepconfig.schema.json | 10 ---------- 3 files changed, 16 deletions(-) diff --git a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs index 04e19f3d5fd..715848ef3ff 100644 --- a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -65,7 +65,6 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA } }, "moduleAliasesMock": { - "ts": {}, "br": {} }, "extensions": { @@ -178,7 +177,6 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat } }, "moduleAliasesMock": { - "ts": {}, "br": {} }, "extensions": { @@ -258,7 +256,6 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration } }, "moduleAliasesMock": { - "ts": {}, "br": {} }, "extensions": { @@ -437,7 +434,6 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf } }, "moduleAliasesMock": { - "ts": {}, "br": {} }, "extensions": { @@ -798,7 +794,6 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat } }, "moduleAliasesMock": { - "ts": {}, "br": {} }, "extensions": { diff --git a/src/Bicep.Core/Configuration/bicepconfig.json b/src/Bicep.Core/Configuration/bicepconfig.json index f3ccb8545fb..0612830eb3f 100644 --- a/src/Bicep.Core/Configuration/bicepconfig.json +++ b/src/Bicep.Core/Configuration/bicepconfig.json @@ -30,7 +30,6 @@ } }, "moduleAliasesMock": { - "ts": {}, "br": {} }, "extensions": { diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 2b181a208d4..71ccb632eb7 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -315,19 +315,9 @@ "type": "object", "additionalProperties": false, "default": { - "ts": {}, "br": {} }, "properties": { - "ts": { - "title": "Template Spec Module Aliases Mock", - "description": "Mock Template Spec module alias definitions", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/templateSpecModuleAlias", - "additionalProperties": false - } - }, "br": { "title": "Bicep Registry Module Aliases Mock", "description": "Mock Bicep Registry module alias definitions", From f2a5dc1bb5b6b22b780258f127c45b11aa417744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Fri, 5 Jun 2026 13:29:05 +0200 Subject: [PATCH 14/21] Use reference to constant instead of hard-coded value. --- .../Registry/OciArtifactEmulatedReferenceTests.cs | 2 +- src/Bicep.Core/Configuration/RootConfiguration.cs | 3 --- src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs index 1fa7de51bfe..ab16b69d209 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -71,7 +71,7 @@ public void TryParse_ValidModulePath_ShouldSucceed() reference!.UnqualifiedReference.Should().Be("keyvault"); reference!.FullyQualifiedReference.Should().Be("br:keyvault"); reference!.IsExternal.Should().BeFalse(); - reference.Scheme.Should().Be("br-fs"); + reference.Scheme.Should().Be(OciArtifactReferenceFacts.MockedScheme); } [TestMethod] diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index 2f341462adb..c05576a0def 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -67,9 +67,6 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = var cloud = CloudConfiguration.Bind(element.GetProperty(CloudKey)); var moduleAliases = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesKey), configFileUri); var moduleAliasesMock = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesMockKey), configFileUri); - // var moduleAliasesMock = element.TryGetProperty(ModuleAliasesMockKey, out var mockElement) - // ? ModuleAliasesConfiguration.Bind(mockElement, configFileUri) - // : ModuleAliasesConfiguration.Bind(JsonElementFactory.CreateElement(new ModuleAliases()), configFileUri); var analyzers = new AnalyzersConfiguration(element.GetProperty(AnalyzersKey)); var cacheRootDirectory = element.TryGetProperty(CacheRootDirectoryKey, out var e) ? e.GetString() : default; var experimentalFeaturesWarning = element.TryGetProperty(ExperimentalFeaturesWarningKey, out var value) && value.GetBoolean(); diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 150bd0ac93e..4275ea6722f 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2042,7 +2042,7 @@ public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string ali public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( "BCP449", - "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); + $"The '{Registry.Oci.OciArtifactReferenceFacts.MockedScheme}' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( "BCP450", From 2a5465bce7f19632d1291b7c8a35befb7c41da51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sat, 6 Jun 2026 08:29:12 +0200 Subject: [PATCH 15/21] Fix serialization of moduleAliasMock outputting ts property --- .../ModuleAliasesConfiguration.cs | 36 +++++++++++++++++-- .../Configuration/RootConfiguration.cs | 4 ++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index 53c35c7c37b..c4e6ab32f67 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Json; using Bicep.IO.Abstraction; using static Bicep.Core.Diagnostics.DiagnosticBuilder; @@ -50,13 +51,44 @@ public partial class ModuleAliasesConfiguration : ConfigurationSection new(element.ToNonNullObject(), configFileUri, serializeOciArtifactAliasesOnly: false); + + /// + /// Binds a mock module aliases configuration. Mock aliases only support Bicep Registry (br) aliases, so any + /// template spec (ts) section is discarded on bind and omitted when serializing. + /// + public static ModuleAliasesConfiguration BindMock(JsonElement element, IOUri? configFileUri) + { + var data = element.ToNonNullObject() with + { + TemplateSpecModuleAliases = ImmutableSortedDictionary.Empty, + }; + + return new(data, configFileUri, serializeOciArtifactAliasesOnly: true); } - public static ModuleAliasesConfiguration Bind(JsonElement element, IOUri? configFileUri) => new(element.ToNonNullObject(), configFileUri); + public override void WriteTo(Utf8JsonWriter writer) + { + if (!this.serializeOciArtifactAliasesOnly) + { + base.WriteTo(writer); + return; + } + + writer.WriteStartObject(); + writer.WritePropertyName("br"); + JsonElementFactory.CreateElement(this.Data.OciArtifactModuleAliases).WriteTo(writer); + writer.WriteEndObject(); + } public ImmutableSortedDictionary GetOciArtifactModuleAliases() { diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index c05576a0def..0d39cb58a87 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -66,7 +66,9 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = { var cloud = CloudConfiguration.Bind(element.GetProperty(CloudKey)); var moduleAliases = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesKey), configFileUri); - var moduleAliasesMock = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesMockKey), configFileUri); + var moduleAliasesMock = element.TryGetProperty(ModuleAliasesMockKey, out var mockElement) + ? ModuleAliasesConfiguration.BindMock(mockElement, configFileUri) + : ModuleAliasesConfiguration.BindMock(JsonElementFactory.CreateElement(new ModuleAliases()), configFileUri); var analyzers = new AnalyzersConfiguration(element.GetProperty(AnalyzersKey)); var cacheRootDirectory = element.TryGetProperty(CacheRootDirectoryKey, out var e) ? e.GetString() : default; var experimentalFeaturesWarning = element.TryGetProperty(ExperimentalFeaturesWarningKey, out var value) && value.GetBoolean(); From 52604c6fa107bc02a8dc5d0381ca6905c5600cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sat, 6 Jun 2026 08:34:26 +0200 Subject: [PATCH 16/21] moduleAliasMock does not use modulePath, remove it from schema --- src/vscode-bicep/schemas/bicepconfig.schema.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 71ccb632eb7..e2ba631ef46 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -167,12 +167,6 @@ "description": "The path relative to bicepconfig.json used to emulate a registry alias", "type": "string", "minLength": 1 - }, - "modulePath": { - "title": "Module Path", - "description": "The module path of the alias", - "type": "string", - "minLength": 1 } } }, From 5473b5a044d2cca785973c1ce75f71e2bc4b611c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sat, 6 Jun 2026 08:35:08 +0200 Subject: [PATCH 17/21] Add ModuleAliasMock to test configuration --- src/Bicep.Core.UnitTests/BicepTestConstants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bicep.Core.UnitTests/BicepTestConstants.cs b/src/Bicep.Core.UnitTests/BicepTestConstants.cs index 2d1e439f46c..7c5d8bcd65c 100644 --- a/src/Bicep.Core.UnitTests/BicepTestConstants.cs +++ b/src/Bicep.Core.UnitTests/BicepTestConstants.cs @@ -112,6 +112,7 @@ public static RootConfiguration CreateMockConfiguration(Dictionary(), + ["moduleAliasesMock"] = new Dictionary(), ["extensions"] = new Dictionary(), ["implicitExtensions"] = new[] { "az" }, ["analyzers"] = new Dictionary(), From 690c326ffdbaf04baa344dc3612181cb4a5627f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sat, 6 Jun 2026 08:35:14 +0200 Subject: [PATCH 18/21] Update baselines --- .../Files/baselines/Registry_LF/main.ir.bicep | 6 +++--- .../baselines/Registry_LF/main.symbols.bicep | 2 +- .../baselines/Registry_LF/main.syntax.bicep | 16 ++++++++-------- .../baselines/Registry_LF/main.tokens.bicep | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index fd1ff47bd19..8cd83b71f5c 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2603) ProgramExpression +//@[000:2601) ProgramExpression //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] @@ -77,8 +77,8 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { -//@[000:0137) ├─DeclaredModuleExpression -//@[060:0137) | ├─ObjectExpression +//@[000:0135) ├─DeclaredModuleExpression +//@[058:0135) | ├─ObjectExpression name: 'planDeploy3' //@[002:0021) | | └─ObjectPropertyExpression //@[002:0006) | | ├─StringLiteralExpression { Value = name } diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index 71709130e80..7dcb8ce9ea7 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -25,7 +25,7 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { -//@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 137 +//@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 135 name: 'planDeploy3' scope: rg params: { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index fb9f9957978..07c5cf46cf1 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2603) ProgramSyntax +//@[000:2601) ProgramSyntax //@[000:0028) ├─TargetScopeSyntax //@[000:0011) | ├─Token(Identifier) |targetScope| //@[012:0013) | ├─Token(Assignment) |=| @@ -148,16 +148,16 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[001:0003) ├─Token(NewLine) |\n\n| module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { -//@[000:0137) ├─ModuleDeclarationSyntax +//@[000:0135) ├─ModuleDeclarationSyntax //@[000:0006) | ├─Token(Identifier) |module| //@[007:0021) | ├─IdentifierSyntax //@[007:0021) | | └─Token(Identifier) |appPlanDeploy3| -//@[022:0057) | ├─StringSyntax -//@[022:0057) | | └─Token(StringComplete) |'br/mock-registry-mocked:plan:v2'| -//@[058:0059) | ├─Token(Assignment) |=| -//@[060:0137) | └─ObjectSyntax -//@[060:0061) | ├─Token(LeftBrace) |{| -//@[061:0062) | ├─Token(NewLine) |\n| +//@[022:0055) | ├─StringSyntax +//@[022:0055) | | └─Token(StringComplete) |'br/mock-registry-mocked:plan:v2'| +//@[056:0057) | ├─Token(Assignment) |=| +//@[058:0135) | └─ObjectSyntax +//@[058:0059) | ├─Token(LeftBrace) |{| +//@[059:0060) | ├─Token(NewLine) |\n| name: 'planDeploy3' //@[002:0021) | ├─ObjectPropertySyntax //@[002:0006) | | ├─IdentifierSyntax diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index 051eee22739..69e9b140343 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -100,10 +100,10 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { //@[000:006) Identifier |module| //@[007:021) Identifier |appPlanDeploy3| -//@[022:057) StringComplete |'br/mock-registry-mocked:plan:v2'| -//@[058:059) Assignment |=| -//@[060:061) LeftBrace |{| -//@[061:062) NewLine |\n| +//@[022:055) StringComplete |'br/mock-registry-mocked:plan:v2'| +//@[056:057) Assignment |=| +//@[058:059) LeftBrace |{| +//@[059:060) NewLine |\n| name: 'planDeploy3' //@[002:006) Identifier |name| //@[006:007) Colon |:| From 9a1b981e547b12e279184a4c9cfe31dbd83abc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Fri, 12 Jun 2026 08:23:56 +0200 Subject: [PATCH 19/21] Split ModuleAliasesMockConfiguration from ModuleAliasesConfiguration and rename Filesystem property to MapToFilePath --- .../baselines/Registry_LF/bicepconfig.json | 2 +- .../ConfigurationManagerTests.cs | 16 ++-- .../OciArtifactModuleReferenceTests.cs | 4 +- ....cs => OciArtifactMockedReferenceTests.cs} | 32 ++------ .../ModuleAliasesConfiguration.cs | 43 +--------- .../ModuleAliasesMockConfiguration.cs | 80 +++++++++++++++++++ .../Configuration/RootConfiguration.cs | 63 ++++++--------- .../Diagnostics/DiagnosticBuilder.cs | 16 ++-- .../Oci/OciArtifactMockedReference.cs | 18 ++--- .../Registry/OciArtifactMockRegistry.cs | 4 +- .../Registry/OciArtifactRegistry.cs | 25 +++--- .../schemas/bicepconfig.schema.json | 6 +- 12 files changed, 159 insertions(+), 150 deletions(-) rename src/Bicep.Core.UnitTests/Registry/{OciArtifactEmulatedReferenceTests.cs => OciArtifactMockedReferenceTests.cs} (87%) create mode 100644 src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json index a0ccd1c03c6..91a04d13158 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json @@ -22,7 +22,7 @@ "moduleAliasesMock": { "br": { "mock-registry-mocked": { - "fileSystem": "Publish" + "mapToFilePath": "Publish" } } } diff --git a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs index 715848ef3ff..ef06935d4ac 100644 --- a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -902,7 +902,7 @@ public void GetConfiguration_ModuleAliasesMock_SupersedesModuleAliasesForSameAli }, "moduleAliasesMock": { "br": { - "myAlias": { "fileSystem": "mock/path" } + "myAlias": { "mapToFilePath": "mock/path" } } } } @@ -913,13 +913,13 @@ public void GetConfiguration_ModuleAliasesMock_SupersedesModuleAliasesForSameAli var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); // Assert. - configuration.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue(); - alias!.FileSystem.Should().Be("mock/path"); + configuration.TryGetOciArtifactModuleAliasMock("myAlias").IsSuccess(out var mockAlias).Should().BeTrue(); + mockAlias!.MapToFilePath.Should().Be("mock/path"); - // The merged view should expose the mock definition without throwing on duplicate keys. - var merged = configuration.GetOciArtifactModuleAliases(); - merged.Should().ContainKey("myAlias"); - merged["myAlias"].FileSystem.Should().Be("mock/path"); + // The merged view should expose the mock definition without throwing on duplicate keys. + var mocks = configuration.GetOciArtifactModuleAliasMocks(); + mocks.Should().ContainKey("myAlias"); + mocks["myAlias"].MapToFilePath.Should().Be("mock/path"); } [TestMethod] @@ -935,7 +935,7 @@ public void GetConfiguration_ModuleAliasesMock_FallsBackToModuleAliasesWhenAlias }, "moduleAliasesMock": { "br": { - "otherAlias": { "fileSystem": "mock/path" } + "otherAlias": { "mapToFilePath": "mock/path" } } } } diff --git a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs index a915de36860..68e4e8c345c 100644 --- a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs @@ -277,7 +277,7 @@ private static IEnumerable GetInvalidAliasData() ["moduleAliases.br.myModulePath.modulePath"] = "path", }), "BCP216", - "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", + "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. The \"registry\" property must be specified.", }; yield return new object[] @@ -291,7 +291,7 @@ private static IEnumerable GetInvalidAliasData() }, "/bicepconfig.json"), "BCP216", - "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", + "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. The \"registry\" property must be specified.", }; } diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactMockedReferenceTests.cs similarity index 87% rename from src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs rename to src/Bicep.Core.UnitTests/Registry/OciArtifactMockedReferenceTests.cs index ab16b69d209..a2238dd5c5b 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactMockedReferenceTests.cs @@ -173,25 +173,7 @@ public void Equals_DifferentModulePaths_ShouldNotBeEqual() } [TestMethod] - public void TryGetOciArtifactModuleAlias_BothRegistryAndFileSystemSet_ShouldFail() - { - var configuration = BicepTestConstants.CreateMockConfiguration( - new() - { - ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", - ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", - }); - - var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); - - result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); - var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); - diagnostic.Code.Should().Be("BCP447"); - diagnostic.Message.Should().Contain("mutually exclusive"); - } - - [TestMethod] - public void TryGetOciArtifactModuleAlias_NeitherRegistryNorFileSystemSet_ShouldFail() + public void TryGetOciArtifactModuleAlias_RegistryNotSet_ShouldFail() { var configuration = BicepTestConstants.CreateMockConfiguration( new() @@ -204,23 +186,22 @@ public void TryGetOciArtifactModuleAlias_NeitherRegistryNorFileSystemSet_ShouldF result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); diagnostic.Code.Should().Be("BCP216"); - diagnostic.Message.Should().Contain("fileSystem"); + diagnostic.Message.Should().Contain("registry"); } [TestMethod] - public void TryGetOciArtifactModuleAlias_OnlyFileSystemSet_ShouldSucceed() + public void TryGetOciArtifactModuleAliasMock_OnlyMapToFilePathSet_ShouldSucceed() { var configuration = BicepTestConstants.CreateMockConfiguration( new() { - ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + ["moduleAliasesMock.br.myAlias.mapToFilePath"] = "../bicepModules", }); - var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + var result = configuration.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock("myAlias"); result.IsSuccess(out var alias, out _).Should().BeTrue(); - alias!.FileSystem.Should().Be("../bicepModules"); - alias.Registry.Should().BeNull(); + alias!.MapToFilePath.Should().Be("../bicepModules"); } [TestMethod] @@ -236,7 +217,6 @@ public void TryGetOciArtifactModuleAlias_OnlyRegistrySet_ShouldSucceed() result.IsSuccess(out var alias, out _).Should().BeTrue(); alias!.Registry.Should().Be("example.azurecr.io"); - alias.FileSystem.Should().BeNull(); } [TestMethod] diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index c4e6ab32f67..4003d9f2bb9 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -38,11 +38,7 @@ public record OciArtifactModuleAlias public string? ModulePath { get; init; } - public string? FileSystem { get; init; } - - public override string ToString() => this.FileSystem is not null - ? $"{FileSystem}" - : this.ModulePath is not null + public override string ToString() => this.ModulePath is not null ? $"{Registry}/{ModulePath}" : $"{Registry}"; } @@ -62,34 +58,6 @@ private ModuleAliasesConfiguration(ModuleAliases data, IOUri? configFileUri, boo public static ModuleAliasesConfiguration Bind(JsonElement element, IOUri? configFileUri) => new(element.ToNonNullObject(), configFileUri, serializeOciArtifactAliasesOnly: false); - /// - /// Binds a mock module aliases configuration. Mock aliases only support Bicep Registry (br) aliases, so any - /// template spec (ts) section is discarded on bind and omitted when serializing. - /// - public static ModuleAliasesConfiguration BindMock(JsonElement element, IOUri? configFileUri) - { - var data = element.ToNonNullObject() with - { - TemplateSpecModuleAliases = ImmutableSortedDictionary.Empty, - }; - - return new(data, configFileUri, serializeOciArtifactAliasesOnly: true); - } - - public override void WriteTo(Utf8JsonWriter writer) - { - if (!this.serializeOciArtifactAliasesOnly) - { - base.WriteTo(writer); - return; - } - - writer.WriteStartObject(); - writer.WritePropertyName("br"); - JsonElementFactory.CreateElement(this.Data.OciArtifactModuleAliases).WriteTo(writer); - writer.WriteEndObject(); - } - public ImmutableSortedDictionary GetOciArtifactModuleAliases() { return this.Data.OciArtifactModuleAliases; @@ -137,12 +105,7 @@ public ResultWithDiagnosticBuilder TryGetOciArtifactModu return new(x => x.OciArtifactModuleAliasNameDoesNotExistInConfiguration(aliasName, configFileUri)); } - if (alias.Registry is not null && alias.FileSystem is not null) - { - return new(x => x.InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(aliasName, configFileUri)); - } - - if (alias.Registry is null && alias.FileSystem is null) + if (alias.Registry is null) { return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, configFileUri)); } @@ -163,6 +126,6 @@ private static bool ValidateAliasName(string aliasName, [NotNullWhen(false)] out } [GeneratedRegex("^[a-zA-Z0-9-_]+$", RegexOptions.CultureInvariant)] - private static partial Regex ModuleAliasNameRegex(); + internal static partial Regex ModuleAliasNameRegex(); } } diff --git a/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs new file mode 100644 index 00000000000..c82b74c596d --- /dev/null +++ b/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Bicep.Core.Diagnostics; +using Bicep.Core.Extensions; +using Bicep.Core.Json; +using Bicep.IO.Abstraction; +using static Bicep.Core.Diagnostics.DiagnosticBuilder; + +namespace Bicep.Core.Configuration +{ + public record ModuleAliasesMock + { + [JsonPropertyName("br")] + public ImmutableSortedDictionary OciArtifactModuleAliasesMock { get; init; } = ImmutableSortedDictionary.Empty; + } + + public record OciArtifactModuleAliasMock + { + public string? MapToFilePath { get; init; } + + public override string ToString() => $"{MapToFilePath}"; + } + + public partial class ModuleAliasesMockConfiguration : ConfigurationSection + { + private readonly IOUri? configFileUri; + + private ModuleAliasesMockConfiguration(ModuleAliasesMock data, IOUri? configFileUri) + : base(data) + { + this.configFileUri = configFileUri; + } + + public static ModuleAliasesMockConfiguration Bind(JsonElement element, IOUri? configFileUri) => new(element.ToNonNullObject(), configFileUri); + + public ImmutableSortedDictionary GetOciArtifactModuleAliasesMock() + { + return this.Data.OciArtifactModuleAliasesMock; + } + + public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAliasMock(string aliasName) + { + if (!ValidateAliasName(aliasName, out var errorBuilder)) + { + return new(errorBuilder); + } + + if (!this.Data.OciArtifactModuleAliasesMock.TryGetValue(aliasName, out var alias)) + { + return new(x => x.OciArtifactModuleAliasNameDoesNotExistInConfiguration(aliasName, configFileUri)); + } + + if (alias.MapToFilePath is null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, configFileUri)); + } + + return new(alias); + } + + private static bool ValidateAliasName(string aliasName, [NotNullWhen(false)] out DiagnosticBuilderDelegate? errorBuilder) + { + // To ensure consistency with module alias, we're referring to the same regex for validating alias names + if (!ModuleAliasesConfiguration.ModuleAliasNameRegex().IsMatch(aliasName)) + { + errorBuilder = x => x.InvalidModuleAliasName(aliasName); + return false; + } + + errorBuilder = null; + return true; + } + } +} diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index 0d39cb58a87..b048228df7f 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -37,7 +37,7 @@ public class RootConfiguration public RootConfiguration( CloudConfiguration cloud, ModuleAliasesConfiguration moduleAliases, - ModuleAliasesConfiguration moduleAliasesMock, + ModuleAliasesMockConfiguration moduleAliasesMock, ExtensionsConfiguration extensions, ImplicitExtensionsConfiguration implicitExtensions, AnalyzersConfiguration analyzers, @@ -67,8 +67,8 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = var cloud = CloudConfiguration.Bind(element.GetProperty(CloudKey)); var moduleAliases = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesKey), configFileUri); var moduleAliasesMock = element.TryGetProperty(ModuleAliasesMockKey, out var mockElement) - ? ModuleAliasesConfiguration.BindMock(mockElement, configFileUri) - : ModuleAliasesConfiguration.BindMock(JsonElementFactory.CreateElement(new ModuleAliases()), configFileUri); + ? ModuleAliasesMockConfiguration.Bind(mockElement, configFileUri) + : ModuleAliasesMockConfiguration.Bind(JsonElementFactory.CreateElement(new ModuleAliasesMock()), configFileUri); var analyzers = new AnalyzersConfiguration(element.GetProperty(AnalyzersKey)); var cacheRootDirectory = element.TryGetProperty(CacheRootDirectoryKey, out var e) ? e.GetString() : default; var experimentalFeaturesWarning = element.TryGetProperty(ExperimentalFeaturesWarningKey, out var value) && value.GetBoolean(); @@ -85,7 +85,7 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = public ModuleAliasesConfiguration ModuleAliases { get; } - public ModuleAliasesConfiguration ModuleAliasesMock { get; } + public ModuleAliasesMockConfiguration ModuleAliasesMock { get; } public ExtensionsConfiguration Extensions { get; } @@ -108,67 +108,56 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = public bool IsBuiltIn => ConfigFileUri is null; /// - /// Gets an OCI artifact module alias. If the alias is defined in moduleAliasesMock, that definition supersedes - /// the one in moduleAliases. Otherwise, the alias is resolved from moduleAliases. + /// Gets an OCI artifact module alias. /// public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAlias(string aliasName) { - if (this.ModuleAliasesMock.GetOciArtifactModuleAliases().ContainsKey(aliasName)) - { - return this.ModuleAliasesMock.TryGetOciArtifactModuleAlias(aliasName); - } - return this.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName); } /// - /// Gets a template spec module alias. If the alias is defined in moduleAliasesMock, that definition supersedes - /// the one in moduleAliases. Otherwise, the alias is resolved from moduleAliases. + /// Gets a template spec module alias /// public ResultWithDiagnosticBuilder TryGetTemplateSpecModuleAlias(string aliasName) { - if (this.ModuleAliasesMock.GetTemplateSpecModuleAliases().ContainsKey(aliasName)) - { - return this.ModuleAliasesMock.TryGetTemplateSpecModuleAlias(aliasName); - } - return this.ModuleAliases.TryGetTemplateSpecModuleAlias(aliasName); } - /// - /// Gets all OCI artifact module aliases, merging moduleAliases with moduleAliasesMock. Aliases defined in - /// moduleAliasesMock supersede those with the same name in moduleAliases. + /// Gets all OCI artifact module aliases from the configuration. /// public ImmutableSortedDictionary GetOciArtifactModuleAliases() { - var merged = this.ModuleAliases.GetOciArtifactModuleAliases(); - foreach (var (aliasName, alias) in this.ModuleAliasesMock.GetOciArtifactModuleAliases()) - { - merged = merged.SetItem(aliasName, alias); - } - - return merged; + return this.ModuleAliases.GetOciArtifactModuleAliases(); } /// - /// Gets all template spec module aliases, merging moduleAliases with moduleAliasesMock. Aliases defined in - /// moduleAliasesMock supersede those with the same name in moduleAliases. + /// Gets all template spec module aliases from the configuration. /// public ImmutableSortedDictionary GetTemplateSpecModuleAliases() { - var merged = this.ModuleAliases.GetTemplateSpecModuleAliases(); - foreach (var (aliasName, alias) in this.ModuleAliasesMock.GetTemplateSpecModuleAliases()) - { - merged = merged.SetItem(aliasName, alias); - } + return this.ModuleAliases.GetTemplateSpecModuleAliases(); + } - return merged; + /// + /// Gets all OCI artifact module alias mocks from the moduleAliasesMock configuration. + /// + public ImmutableSortedDictionary GetOciArtifactModuleAliasMocks() + { + return this.ModuleAliasesMock.GetOciArtifactModuleAliasesMock(); + } + + /// + /// Gets an OCI artifact module alias mock. Returns null if the alias is not defined in moduleAliasesMock. + /// + public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAliasMock(string aliasName) + { + return this.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock(aliasName); } public RootConfiguration With( CloudConfiguration? cloud = null, ModuleAliasesConfiguration? moduleAliases = null, - ModuleAliasesConfiguration? moduleAliasesMock = null, + ModuleAliasesMockConfiguration? moduleAliasesMock = null, ExtensionsConfiguration? extensions = null, ImplicitExtensionsConfiguration? implicitExtensions = null, AnalyzersConfiguration? analyzers = null, diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 4275ea6722f..7a86b4c832b 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -1111,7 +1111,7 @@ public Diagnostic InvalidTemplateSpecAliasResourceGroupNullOrUndefined(string al public Diagnostic InvalidOciArtifactModuleAliasRegistryNullOrUndefined(string aliasName, IOUri? configFileUri) => CoreError( "BCP216", - $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. Either the \"registry\" or \"fileSystem\" property must be specified."); + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" property must be specified."); public Diagnostic InvalidTemplateSpecReferenceInvalidSubscriptionId(string? aliasName, string subscriptionId, string referenceValue) => CoreError( "BCP217", @@ -2032,25 +2032,21 @@ public Diagnostic ArtifactRestoreBlockedByRegistry(string registryHostname) => C $"Restore from registry \"{registryHostname}\" is blocked because it is not in the trusted registries list. " + $"See https://aka.ms/bicep/registry-trust for details."); - public Diagnostic InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(string aliasName, IOUri? configFileUri) => CoreError( - "BCP447", - $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" and \"fileSystem\" properties are mutually exclusive."); - - public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string aliasName) => CoreError( + public Diagnostic OciArtifactModuleAliasMapToFilePathOnlySupportsModules(string aliasName) => CoreError( "BCP448", - $"The OCI artifact module alias \"{aliasName}\" has a \"fileSystem\" property which is only supported for modules, not extensions."); + $"The OCI artifact module alias \"{aliasName}\" has a \"mapToFilePath\" property which is only supported for modules, not extensions."); public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( "BCP449", - $"The '{Registry.Oci.OciArtifactReferenceFacts.MockedScheme}' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); + $"The '{Registry.Oci.OciArtifactReferenceFacts.MockedScheme}' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'mapToFilePath' alias instead."); public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( "BCP450", $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); - public Diagnostic InvalidOciArtifactModuleAliasFileSystemPath(string? aliasName, string path, string reason) => CoreError( + public Diagnostic InvalidOciArtifactModuleAliasMapToFilePath(string? aliasName, string path, string reason) => CoreError( "BCP451", - $"The OCI artifact module alias{(aliasName is not null ? $" \"{aliasName}\"" : "")} has an invalid \"fileSystem\" path \"{path}\": {reason}"); + $"The OCI artifact module alias{(aliasName is not null ? $" \"{aliasName}\"" : "")} has an invalid \"mapToFilePath\" path \"{path}\": {reason}"); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs index 52d124aff43..f1b13506222 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs @@ -9,7 +9,7 @@ namespace Bicep.Core.Registry.Oci { /// /// Represents an OCI module reference that is mocked via a local filesystem path. - /// When a module alias specifies a "fileSystem" property instead of "registry", + /// When a module alias is added to moduleAliasesMock configuration, /// module references are resolved to local .bicep files instead of pulling from a container registry. /// public class OciArtifactMockedReference : ArtifactReference @@ -60,14 +60,14 @@ public static string ExtractModulePath(string unqualifiedReference) } // referencingFile is the Bicep source file containing the module reference - // fileSystemPath is the filesystem path from the alias configuration + // mapToFilePath is the path from the alias configuration // configFileUri is the URI of the bicepconfig.json file, used to resolve relative paths // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") // fileExplorer is the file explorer used to create file handles // aliasName is the name of the module alias, used in diagnostics public static ResultWithDiagnosticBuilder TryParse( BicepSourceFile referencingFile, - string fileSystemPath, + string mapToFilePath, IOUri configFileUri, string unqualifiedReference, IFileExplorer fileExplorer, @@ -90,12 +90,12 @@ public static ResultWithDiagnosticBuilder TryParse( } IOUri baseUri; - // Ensure the fileSystem path ends with '/' so it's treated as a directory. - var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') - ? fileSystemPath - : fileSystemPath + "/"; + // Ensure the mapToFilePath path ends with '/' so it's treated as a directory. + var directoryPath = mapToFilePath.EndsWith('/') || mapToFilePath.EndsWith('\\') + ? mapToFilePath + : mapToFilePath + "/"; - if (IOUri.IsAbsoluteFilePath(fileSystemPath)) + if (IOUri.IsAbsoluteFilePath(mapToFilePath)) { try { @@ -103,7 +103,7 @@ public static ResultWithDiagnosticBuilder TryParse( } catch (IOException ex) { - return new(x => x.InvalidOciArtifactModuleAliasFileSystemPath(aliasName, fileSystemPath, ex.Message)); + return new(x => x.InvalidOciArtifactModuleAliasMapToFilePath(aliasName, mapToFilePath, ex.Message)); } } else diff --git a/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs b/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs index 083c9334c14..9b06e6e8a90 100644 --- a/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs @@ -34,10 +34,10 @@ public override Task CheckArtifactExists(ArtifactType artifactType, OciArt new Dictionary()); public override Task PublishModule(OciArtifactMockedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) - => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + => throw new NotSupportedException("Publishing is not supported for mocked module aliases."); public override Task PublishExtension(OciArtifactMockedReference reference, ExtensionPackage package) - => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + => throw new NotSupportedException("Publishing is not supported for mocked module aliases."); public override string? TryGetDocumentationUri(OciArtifactMockedReference reference) => null; diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 80a17765daa..172ed0eceff 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -54,19 +54,15 @@ public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) { - // Check if the alias resolves to a filesystem-based alias. + // Check if the alias resolves to a mocked alias if (aliasName is not null) { - if (!referencingFile.Configuration.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var alias, out var aliasFailureBuilder)) - { - return new(aliasFailureBuilder); - } - - if (alias.FileSystem is not null) + if (referencingFile.Configuration.TryGetOciArtifactModuleAliasMock(aliasName).IsSuccess(out var mockAlias, out var _)) { + // Mock aliases only support modules, not extensions. if (artifactType != ArtifactType.Module) { - return new(x => x.OciArtifactModuleAliasFileSystemOnlySupportsModules(aliasName)); + return new(x => x.OciArtifactModuleAliasMapToFilePathOnlySupportsModules(aliasName)); } if (referencingFile.Configuration.ConfigFileUri is null) @@ -74,18 +70,23 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR return new(x => x.ConfigurationFileNotFound("OciModuleAliasesMock")); } + if (mockAlias.MapToFilePath is null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, referencingFile.Configuration.ConfigFileUri)); + } + if (!OciArtifactMockedReference.TryParse( referencingFile, - alias.FileSystem, + mockAlias.MapToFilePath, referencingFile.Configuration.ConfigFileUri, reference, this.fileExplorer, aliasName).IsSuccess(out var mockedRef, out var mockedFailureBuilder)) { - return new(mockedFailureBuilder); + return new(mockedFailureBuilder!); } - return new(mockedRef); + return new(mockedRef!); } } @@ -93,10 +94,10 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR { return new(failureBuilder); } + return new(@ref); } - public override bool IsArtifactRestoreRequired(OciArtifactReference reference) { /* diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index e2ba631ef46..ab4e33e74be 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -159,11 +159,11 @@ "type": "object", "additionalProperties": false, "required": [ - "fileSystem" + "mapToFilePath" ], "properties": { - "fileSystem": { - "title": "File System", + "mapToFilePath": { + "title": "Map To File Path", "description": "The path relative to bicepconfig.json used to emulate a registry alias", "type": "string", "minLength": 1 From d5406d651164e3da537ae360c7295974a0b82a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sat, 13 Jun 2026 07:42:38 +0200 Subject: [PATCH 20/21] Clean up unused code --- .../Configuration/ModuleAliasesConfiguration.cs | 8 ++------ .../Configuration/ModuleAliasesMockConfiguration.cs | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index 4003d9f2bb9..65a08e0b516 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -8,7 +8,6 @@ using System.Text.RegularExpressions; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; -using Bicep.Core.Json; using Bicep.IO.Abstraction; using static Bicep.Core.Diagnostics.DiagnosticBuilder; @@ -47,16 +46,13 @@ public partial class ModuleAliasesConfiguration : ConfigurationSection new(element.ToNonNullObject(), configFileUri, serializeOciArtifactAliasesOnly: false); + public static ModuleAliasesConfiguration Bind(JsonElement element, IOUri? configFileUri) => new(element.ToNonNullObject(), configFileUri); public ImmutableSortedDictionary GetOciArtifactModuleAliases() { diff --git a/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs index c82b74c596d..289b7d0a145 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs @@ -5,10 +5,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; -using Bicep.Core.Json; using Bicep.IO.Abstraction; using static Bicep.Core.Diagnostics.DiagnosticBuilder; From 1e62f76e3793505c91b85a6284ae1b3bafc65069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 14 Jun 2026 20:38:35 +0200 Subject: [PATCH 21/21] Remove methods made redundant I had mock resolution logic here before, now they are just wrapping other methods and are redundant. --- .../ConfigurationManagerTests.cs | 6 +-- .../Configuration/RootConfiguration.cs | 47 ------------------- .../Modules/TemplateSpecModuleReference.cs | 2 +- .../Registry/Oci/OciArtifactReference.cs | 2 +- .../Registry/OciArtifactRegistry.cs | 2 +- .../ModuleReferenceCompletionProvider.cs | 6 +-- 6 files changed, 9 insertions(+), 56 deletions(-) diff --git a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs index 75c8b90d5bf..981f6a48981 100644 --- a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -895,11 +895,11 @@ public void GetConfiguration_ModuleAliasesMock_SupersedesModuleAliasesForSameAli var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); // Assert. - configuration.TryGetOciArtifactModuleAliasMock("myAlias").IsSuccess(out var mockAlias).Should().BeTrue(); + configuration.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock("myAlias").IsSuccess(out var mockAlias).Should().BeTrue(); mockAlias!.MapToFilePath.Should().Be("mock/path"); // The merged view should expose the mock definition without throwing on duplicate keys. - var mocks = configuration.GetOciArtifactModuleAliasMocks(); + var mocks = configuration.ModuleAliasesMock.GetOciArtifactModuleAliasesMock(); mocks.Should().ContainKey("myAlias"); mocks["myAlias"].MapToFilePath.Should().Be("mock/path"); } @@ -928,7 +928,7 @@ public void GetConfiguration_ModuleAliasesMock_FallsBackToModuleAliasesWhenAlias var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); // Assert. - configuration.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue(); + configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue(); alias!.Registry.Should().Be("real.azurecr.io"); } } diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index b048228df7f..496b8480da6 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -107,53 +107,6 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = public bool IsBuiltIn => ConfigFileUri is null; - /// - /// Gets an OCI artifact module alias. - /// - public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAlias(string aliasName) - { - return this.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName); - } - - /// - /// Gets a template spec module alias - /// - public ResultWithDiagnosticBuilder TryGetTemplateSpecModuleAlias(string aliasName) - { - return this.ModuleAliases.TryGetTemplateSpecModuleAlias(aliasName); - } - /// - /// Gets all OCI artifact module aliases from the configuration. - /// - public ImmutableSortedDictionary GetOciArtifactModuleAliases() - { - return this.ModuleAliases.GetOciArtifactModuleAliases(); - } - - /// - /// Gets all template spec module aliases from the configuration. - /// - public ImmutableSortedDictionary GetTemplateSpecModuleAliases() - { - return this.ModuleAliases.GetTemplateSpecModuleAliases(); - } - - /// - /// Gets all OCI artifact module alias mocks from the moduleAliasesMock configuration. - /// - public ImmutableSortedDictionary GetOciArtifactModuleAliasMocks() - { - return this.ModuleAliasesMock.GetOciArtifactModuleAliasesMock(); - } - - /// - /// Gets an OCI artifact module alias mock. Returns null if the alias is not defined in moduleAliasesMock. - /// - public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAliasMock(string aliasName) - { - return this.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock(aliasName); - } - public RootConfiguration With( CloudConfiguration? cloud = null, ModuleAliasesConfiguration? moduleAliases = null, diff --git a/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs b/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs index 339b8894924..30fe759531b 100644 --- a/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs +++ b/src/Bicep.Core/Modules/TemplateSpecModuleReference.cs @@ -74,7 +74,7 @@ public static ResultWithDiagnosticBuilder TryParse( { if (aliasName is not null) { - if (!configuration.TryGetTemplateSpecModuleAlias(aliasName).IsSuccess(out var alias, out var errorBuilder)) + if (!configuration.ModuleAliases.TryGetTemplateSpecModuleAlias(aliasName).IsSuccess(out var alias, out var errorBuilder)) { return new(errorBuilder); } diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs index 3d0b0ee1fa0..993b4347032 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs @@ -123,7 +123,7 @@ private static ResultWithDiagnosticBuilder TryPars switch (type) { case ArtifactType.Module: - if (!configuration.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var moduleAlias, out var moduleFailureBuilder)) + if (!configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var moduleAlias, out var moduleFailureBuilder)) { return new(moduleFailureBuilder); } diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 172ed0eceff..a838d08661b 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -57,7 +57,7 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR // Check if the alias resolves to a mocked alias if (aliasName is not null) { - if (referencingFile.Configuration.TryGetOciArtifactModuleAliasMock(aliasName).IsSuccess(out var mockAlias, out var _)) + if (referencingFile.Configuration.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock(aliasName).IsSuccess(out var mockAlias, out var _)) { // Mock aliases only support modules, not extensions. if (artifactType != ArtifactType.Module) diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 9e4c80356a9..b6a3cc57b58 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -251,7 +251,7 @@ private IEnumerable GetTopLevelCompletions(BicepCompletionContex List completionItems = new(); - var templateSpecModuleAliases = rootConfiguration.GetTemplateSpecModuleAliases(); + var templateSpecModuleAliases = rootConfiguration.ModuleAliases.GetTemplateSpecModuleAliases(); var bicepModuleAliases = GetModuleAliases(rootConfiguration); // Top-level TemplateSpec completions @@ -375,7 +375,7 @@ private async Task> GetVersionCompletions(BicepCompl private static ImmutableSortedDictionary GetModuleAliases(RootConfiguration configuration) { - return configuration.GetOciArtifactModuleAliases(); + return configuration.ModuleAliases.GetOciArtifactModuleAliases(); } private static bool TryGetValidModuleAlias( @@ -388,7 +388,7 @@ private static bool TryGetValidModuleAlias( modulePath = null; // Mock aliases supersede real aliases with the same name. - if (configuration.GetOciArtifactModuleAliases().TryGetValue(aliasName, out var aliasConfig) + if (configuration.ModuleAliases.GetOciArtifactModuleAliases().TryGetValue(aliasName, out var aliasConfig) && !string.IsNullOrWhiteSpace(aliasConfig.Registry)) { registry = aliasConfig.Registry;