From 53e85d8ceee811b28870c372e67d78288c617683 Mon Sep 17 00:00:00 2001 From: Likhitha Kodali Date: Tue, 18 Aug 2026 11:59:11 -0400 Subject: [PATCH 1/6] add 'extends' schema support and surface config chain diagnostics in VS Code --- .../BicepConfigChangeHandlerTests.cs | 7 +- .../BicepConfigLifecycleManager.cs | 178 +++++++++++++++++- .../configuration/bicepconfig.schema.json | 5 + 3 files changed, 187 insertions(+), 3 deletions(-) diff --git a/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs index 73fa16e228c..b02db66b119 100644 --- a/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs @@ -246,7 +246,8 @@ private void RefreshCompilationOfSourceFilesInWorkspace(string bicepFileContents var workspace = new ActiveSourceFileSet(); var fileExplorer = new FileSystemFileExplorer(mockFileSystem); - var configurationManager = new ConfigurationManager(fileExplorer, new BicepConfigurationManager(fileExplorer)); + var bicepConfigManager = new BicepConfigurationManager(fileExplorer); + var configurationManager = new ConfigurationManager(fileExplorer, bicepConfigManager); var sourceFileFactory = new SourceFileFactory(configurationManager, BicepTestConstants.FeatureProviderFactory, BicepTestConstants.AuxiliaryFileCache, BicepTestConstants.FileExplorer); var bicepCompilationManager = new BicepCompilationManager( server, @@ -259,7 +260,9 @@ private void RefreshCompilationOfSourceFilesInWorkspace(string bicepFileContents var bicepConfigLifecycleManager = new BicepConfigLifecycleManager( bicepCompilationManager, - configurationManager); + configurationManager, + bicepConfigManager, + server); bicepConfigLifecycleManager.RefreshCompilationOfSourceFilesInWorkspace(); diff --git a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs index 1eaa98646e5..149db9c9336 100644 --- a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs +++ b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs @@ -2,8 +2,14 @@ // Licensed under the MIT License. using Bicep.Core.Configuration; +using Bicep.Core.Diagnostics; using Bicep.LanguageServer.Compilation; +using Bicep.LanguageServer.Extensions; using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using LspDiagnostic = OmniSharp.Extensions.LanguageServer.Protocol.Models.Diagnostic; namespace Bicep.LanguageServer.BicepConfig { @@ -11,12 +17,131 @@ public class BicepConfigLifecycleManager : IBicepConfigLifecycleManager { private readonly ICompilationManager compilationManager; private readonly ConfigurationManager configurationManager; + private readonly IBicepConfigurationManager bicepConfigurationManager; + private readonly ILanguageServerFacade server; + + public BicepConfigLifecycleManager(ICompilationManager compilationManager, + ConfigurationManager configurationManager, + IBicepConfigurationManager bicepConfigurationManager, + ILanguageServerFacade server) + { + this.compilationManager = compilationManager; + this.configurationManager = configurationManager; + this.bicepConfigurationManager = bicepConfigurationManager; + this.server = server; + } + + public void RefreshCompilationOfSourceFilesInWorkspace() + { + configurationManager.PurgeCache(); + // We shouldn't need to reload auxiliary files if a configuration file has changed. + compilationManager.RefreshAllActiveCompilations(forceReloadAuxiliaryFiles: false); + } + + public void HandleBicepConfigOpenEvent(DocumentUri documentUri) + { + HandleBicepConfigOpenOrChangeEvent(documentUri); + PublishConfigDiagnostics(documentUri); + } + + public void HandleBicepConfigChangeEvent(DocumentUri documentUri) + { + // A change event can represent file creation, modification, or deletion. + // Creation and deletion change config file discovery (the lookup cache), so we + // must do a full purge rather than a targeted invalidation. + configurationManager.PurgeCache(); + HandleBicepConfigOpenOrChangeEvent(documentUri); + PublishConfigDiagnostics(documentUri); + } + + private void HandleBicepConfigOpenOrChangeEvent(DocumentUri documentUri) + => configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()); + + public void HandleBicepConfigSaveEvent(DocumentUri documentUri) + { + configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()); + PublishConfigDiagnostics(documentUri); + } + + public void HandleBicepConfigCloseEvent(DocumentUri documentUri) + { + configurationManager.RemoveConfigCacheEntry(documentUri.ToIOUri()); + + // Clear squiggles when the file is closed. + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = documentUri, + Diagnostics = new Container() + }); + } + + private void PublishConfigDiagnostics(DocumentUri documentUri) + { + var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); + var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); + + var lspDiagnostics = diagnostics.Select(d => new LspDiagnostic + { + Severity = ToLspSeverity(d.Level), + Code = new DiagnosticCode(d.Code), + Message = d.Message, + Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), + Source = "bicep" + }); + + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = documentUri, + Diagnostics = new Container(lspDiagnostics) + }); + } + + private static DiagnosticSeverity ToLspSeverity(DiagnosticLevel level) => level switch + { + DiagnosticLevel.Error => DiagnosticSeverity.Error, + DiagnosticLevel.Warning => DiagnosticSeverity.Warning, + DiagnosticLevel.Info => DiagnosticSeverity.Information, + _ => DiagnosticSeverity.Hint + }; + } +} + +namespace Bicep.LanguageServer.BicepConfig +{ + public class BicepConfigLifecycleManager : IBicepConfigLifecycleManager + { + private readonly ICompilationManager compilationManager; + private readonly ConfigurationManager configurationManager; +<<<<<<< HEAD public BicepConfigLifecycleManager(ICompilationManager compilationManager, ConfigurationManager configurationManager) { this.compilationManager = compilationManager; this.configurationManager = configurationManager; +======= + private readonly IBicepConfigurationManager bicepConfigurationManager; + private readonly ILanguageServerFacade server; + private readonly ILinterRulesProvider linterRulesProvider; + private readonly ITelemetryProvider telemetryProvider; + private readonly IActiveSourceFileSet workspace; + + public BicepConfigLifecycleManager(ICompilationManager compilationManager, + ConfigurationManager configurationManager, + IBicepConfigurationManager bicepConfigurationManager, + ILanguageServerFacade server, + ILinterRulesProvider linterRulesProvider, + ITelemetryProvider telemetryProvider, + IActiveSourceFileSet workspace) + { + this.compilationManager = compilationManager; + this.configurationManager = configurationManager; + this.bicepConfigurationManager = bicepConfigurationManager; + this.server = server; + this.linterRulesProvider = linterRulesProvider; + this.telemetryProvider = telemetryProvider; + this.workspace = workspace; +>>>>>>> af7513a9a (add 'extends' schema support and surface config chain diagnostics in VS Code) } public void RefreshCompilationOfSourceFilesInWorkspace() @@ -29,6 +154,7 @@ public void RefreshCompilationOfSourceFilesInWorkspace() public void HandleBicepConfigOpenEvent(DocumentUri documentUri) { HandleBicepConfigOpenOrChangeEvent(documentUri); + PublishConfigDiagnostics(documentUri); } public void HandleBicepConfigChangeEvent(DocumentUri documentUri) @@ -38,15 +164,65 @@ public void HandleBicepConfigChangeEvent(DocumentUri documentUri) // must do a full purge rather than a targeted invalidation. configurationManager.PurgeCache(); HandleBicepConfigOpenOrChangeEvent(documentUri); + PublishConfigDiagnostics(documentUri); } private void HandleBicepConfigOpenOrChangeEvent(DocumentUri documentUri) => configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()); public void HandleBicepConfigSaveEvent(DocumentUri documentUri) +<<<<<<< HEAD => configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()); +======= + { + if (configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()) is { } update) + { + TelemetryHelper.SendTelemetryOnBicepConfigChange(update.prevConfiguration, update.newConfiguration, linterRulesProvider, telemetryProvider); + } + + PublishConfigDiagnostics(documentUri); + } +>>>>>>> af7513a9a (add 'extends' schema support and surface config chain diagnostics in VS Code) public void HandleBicepConfigCloseEvent(DocumentUri documentUri) - => configurationManager.RemoveConfigCacheEntry(documentUri.ToIOUri()); + { + configurationManager.RemoveConfigCacheEntry(documentUri.ToIOUri()); + + // Clear squiggles when the file is closed. + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = documentUri, + Diagnostics = new Container() + }); + } + + private void PublishConfigDiagnostics(DocumentUri documentUri) + { + var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); + var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); + + var lspDiagnostics = diagnostics.Select(d => new LspDiagnostic + { + Severity = ToLspSeverity(d.Level), + Code = new DiagnosticCode(d.Code), + Message = d.Message, + Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), + Source = "bicep" + }); + + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = documentUri, + Diagnostics = new Container(lspDiagnostics) + }); + } + + private static DiagnosticSeverity ToLspSeverity(DiagnosticLevel level) => level switch + { + DiagnosticLevel.Error => DiagnosticSeverity.Error, + DiagnosticLevel.Warning => DiagnosticSeverity.Warning, + DiagnosticLevel.Info => DiagnosticSeverity.Information, + _ => DiagnosticSeverity.Hint + }; } } diff --git a/src/vscode-bicep/resources/configuration/bicepconfig.schema.json b/src/vscode-bicep/resources/configuration/bicepconfig.schema.json index 4f25890ac4a..4fea562e64d 100644 --- a/src/vscode-bicep/resources/configuration/bicepconfig.schema.json +++ b/src/vscode-bicep/resources/configuration/bicepconfig.schema.json @@ -157,6 +157,11 @@ "type": "object", "default": {}, "properties": { + "extends": { + "title": "Extends", + "description": "Relative path to a base bicepconfig.json to inherit settings from. Absolute paths are not allowed.", + "type": "string" + }, "cloud": { "title": "Cloud", "type": "object", From 3897daa3e6fad6e742b1f6e47a43fada84878be1 Mon Sep 17 00:00:00 2001 From: Likhitha Kodali Date: Tue, 18 Aug 2026 13:49:41 -0400 Subject: [PATCH 2/6] remove publish diagnostics on unsaved bicepconfig.json edits --- src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs index 149db9c9336..b19ca473bcf 100644 --- a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs +++ b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs @@ -51,7 +51,6 @@ public void HandleBicepConfigChangeEvent(DocumentUri documentUri) // must do a full purge rather than a targeted invalidation. configurationManager.PurgeCache(); HandleBicepConfigOpenOrChangeEvent(documentUri); - PublishConfigDiagnostics(documentUri); } private void HandleBicepConfigOpenOrChangeEvent(DocumentUri documentUri) From 1287a929311cc65d947ae9054eb5a11d02bfa82c Mon Sep 17 00:00:00 2001 From: Likhitha Kodali Date: Tue, 18 Aug 2026 17:14:25 -0400 Subject: [PATCH 3/6] Group by IDiagnostic.uri --- .../BicepConfigLifecycleManager.cs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs index b19ca473bcf..4aeb143d8f4 100644 --- a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs +++ b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs @@ -77,22 +77,38 @@ public void HandleBicepConfigCloseEvent(DocumentUri documentUri) private void PublishConfigDiagnostics(DocumentUri documentUri) { var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); - var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); + var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics().ToList(); - var lspDiagnostics = diagnostics.Select(d => new LspDiagnostic + if (diagnostics.Count == 0) { - Severity = ToLspSeverity(d.Level), - Code = new DiagnosticCode(d.Code), - Message = d.Message, - Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), - Source = "bicep" - }); + // Clear any stale squiggles on the active file. + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = documentUri, + Diagnostics = new Container() + }); + return; + } - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + // Group by IDiagnostic.Uri so each bicepconfig.json in the chain gets its own + // PublishDiagnostics call — squiggles appear on the specific file that has the + // error, not always on the leaf. + // Diagnostics without a URI fall back to the active documentUri. + foreach (var group in diagnostics.GroupBy(d => d.Uri is { } u ? DocumentUri.From(u) : documentUri)) { - Uri = documentUri, - Diagnostics = new Container(lspDiagnostics) - }); + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = group.Key, + Diagnostics = new Container(group.Select(d => new LspDiagnostic + { + Severity = ToLspSeverity(d.Level), + Code = new DiagnosticCode(d.Code), + Message = d.Message, + Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), + Source = "bicep" + })) + }); + } } private static DiagnosticSeverity ToLspSeverity(DiagnosticLevel level) => level switch From ba8588a61a4fe218b7a8ddf742c533148fbbf20d Mon Sep 17 00:00:00 2001 From: Likhitha Kodali Date: Tue, 18 Aug 2026 17:24:12 -0400 Subject: [PATCH 4/6] Revert IDiagnostic.Uri changes --- .../BicepConfigLifecycleManager.cs | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs index 4aeb143d8f4..b19ca473bcf 100644 --- a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs +++ b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs @@ -77,38 +77,22 @@ public void HandleBicepConfigCloseEvent(DocumentUri documentUri) private void PublishConfigDiagnostics(DocumentUri documentUri) { var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); - var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics().ToList(); + var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); - if (diagnostics.Count == 0) + var lspDiagnostics = diagnostics.Select(d => new LspDiagnostic { - // Clear any stale squiggles on the active file. - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams - { - Uri = documentUri, - Diagnostics = new Container() - }); - return; - } + Severity = ToLspSeverity(d.Level), + Code = new DiagnosticCode(d.Code), + Message = d.Message, + Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), + Source = "bicep" + }); - // Group by IDiagnostic.Uri so each bicepconfig.json in the chain gets its own - // PublishDiagnostics call — squiggles appear on the specific file that has the - // error, not always on the leaf. - // Diagnostics without a URI fall back to the active documentUri. - foreach (var group in diagnostics.GroupBy(d => d.Uri is { } u ? DocumentUri.From(u) : documentUri)) + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams { - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams - { - Uri = group.Key, - Diagnostics = new Container(group.Select(d => new LspDiagnostic - { - Severity = ToLspSeverity(d.Level), - Code = new DiagnosticCode(d.Code), - Message = d.Message, - Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), - Source = "bicep" - })) - }); - } + Uri = documentUri, + Diagnostics = new Container(lspDiagnostics) + }); } private static DiagnosticSeverity ToLspSeverity(DiagnosticLevel level) => level switch From 8645b5fdabf658f3df7f14741c7a68c9014b6436 Mon Sep 17 00:00:00 2001 From: Likhitha Kodali Date: Wed, 19 Aug 2026 10:46:51 -0400 Subject: [PATCH 5/6] route config chain squiggles to the exact bicepconfig.json that has the error --- .../Configuration/BicepConfigurationChain.cs | 12 ++++++ .../Configuration/IBicepConfigurationChain.cs | 6 +++ src/Bicep.Core/Diagnostics/GlobalUsings.cs | 8 ++++ .../BicepConfigLifecycleManager.cs | 39 +++++++++++++------ 4 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 src/Bicep.Core/Diagnostics/GlobalUsings.cs diff --git a/src/Bicep.Core/Configuration/BicepConfigurationChain.cs b/src/Bicep.Core/Configuration/BicepConfigurationChain.cs index 0cc1a84181e..e8b07254b78 100644 --- a/src/Bicep.Core/Configuration/BicepConfigurationChain.cs +++ b/src/Bicep.Core/Configuration/BicepConfigurationChain.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using Bicep.Core.Diagnostics; +using Bicep.IO.Abstraction; namespace Bicep.Core.Configuration; @@ -69,6 +70,17 @@ public IEnumerable GetAllDiagnostics() return this.aggregatedDiagnostics.Value; } + /// + /// Returns diagnostics grouped by the config file URI they originated from. + /// Built-in default layers (no ) are excluded. + /// + public DiagnosticsPerFile GetDiagnosticsByConfigFile() + => this.layers + .Where(layer => !layer.IsBuiltIn) + .Select(layer => new KeyValuePair>( + layer.ConfigFileUri!, + layer.GetDiagnostics().ToImmutableArray())); + /// /// The number of configuration files in the chain, including the built-in defaults. /// diff --git a/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs b/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs index 698dcf9d6b7..42a04e0d8e8 100644 --- a/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs +++ b/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs @@ -17,6 +17,12 @@ public interface IBicepConfigurationChain /// IBicepConfiguration GetEffectiveConfiguration(); + /// + /// Returns diagnostics grouped by the config file URI they originated from. + /// Only user-defined config files are included (built-in defaults are excluded). + /// + DiagnosticsPerFile GetDiagnosticsByConfigFile(); + /// /// The number of configuration files in the chain. /// diff --git a/src/Bicep.Core/Diagnostics/GlobalUsings.cs b/src/Bicep.Core/Diagnostics/GlobalUsings.cs new file mode 100644 index 00000000000..ac6c8bf5e62 --- /dev/null +++ b/src/Bicep.Core/Diagnostics/GlobalUsings.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using DiagnosticsPerFile = + System.Collections.Generic.IEnumerable< + System.Collections.Generic.KeyValuePair< + Bicep.IO.Abstraction.IOUri, + System.Collections.Immutable.ImmutableArray>>; diff --git a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs index b19ca473bcf..1a3f4d6e3cb 100644 --- a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs +++ b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs @@ -77,22 +77,37 @@ public void HandleBicepConfigCloseEvent(DocumentUri documentUri) private void PublishConfigDiagnostics(DocumentUri documentUri) { var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); - var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); + var diagnosticsByFile = chain.GetDiagnosticsByConfigFile().ToList(); - var lspDiagnostics = diagnostics.Select(d => new LspDiagnostic + if (diagnosticsByFile.All(kvp => kvp.Value.IsEmpty)) { - Severity = ToLspSeverity(d.Level), - Code = new DiagnosticCode(d.Code), - Message = d.Message, - Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), - Source = "bicep" - }); + // No diagnostics in any layer — clear stale squiggles on the active file. + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = documentUri, + Diagnostics = new Container() + }); + return; + } - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + // Publish squiggles to each config file that has diagnostics. + foreach (var (fileUri, diagnostics) in diagnosticsByFile) { - Uri = documentUri, - Diagnostics = new Container(lspDiagnostics) - }); + if (diagnostics.IsEmpty) { continue; } + + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + { + Uri = DocumentUri.From(fileUri.ToUri()), + Diagnostics = new Container(diagnostics.Select(d => new LspDiagnostic + { + Severity = ToLspSeverity(d.Level), + Code = new DiagnosticCode(d.Code), + Message = d.Message, + Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), + Source = "bicep" + })) + }); + } } private static DiagnosticSeverity ToLspSeverity(DiagnosticLevel level) => level switch From f0c6d7b9dbb2524f4b3b0b4ee455bfebffc1bfe1 Mon Sep 17 00:00:00 2001 From: Likhitha Kodali Date: Wed, 19 Aug 2026 19:07:51 -0400 Subject: [PATCH 6/6] =?UTF-8?q?Renamed=20=C2=A0GetDiagnosticsByConfigFile?= =?UTF-8?q?=20to=C2=A0EnumerateDiagnosticsPerFile=20,=20updated=20PublishC?= =?UTF-8?q?onfigDiagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Configuration/BicepConfigurationChain.cs | 2 +- .../Configuration/IBicepConfigurationChain.cs | 2 +- .../BicepConfigChangeHandlerTests.cs | 36 +++++ .../BicepConfigLifecycleManager.cs | 150 ++---------------- .../IServiceCollectionExtensions.cs | 1 + 5 files changed, 55 insertions(+), 136 deletions(-) diff --git a/src/Bicep.Core/Configuration/BicepConfigurationChain.cs b/src/Bicep.Core/Configuration/BicepConfigurationChain.cs index e8b07254b78..8c9993b6603 100644 --- a/src/Bicep.Core/Configuration/BicepConfigurationChain.cs +++ b/src/Bicep.Core/Configuration/BicepConfigurationChain.cs @@ -74,7 +74,7 @@ public IEnumerable GetAllDiagnostics() /// Returns diagnostics grouped by the config file URI they originated from. /// Built-in default layers (no ) are excluded. /// - public DiagnosticsPerFile GetDiagnosticsByConfigFile() + public DiagnosticsPerFile EnumerateDiagnosticsPerFile() => this.layers .Where(layer => !layer.IsBuiltIn) .Select(layer => new KeyValuePair>( diff --git a/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs b/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs index 42a04e0d8e8..fa8be21723e 100644 --- a/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs +++ b/src/Bicep.Core/Configuration/IBicepConfigurationChain.cs @@ -21,7 +21,7 @@ public interface IBicepConfigurationChain /// Returns diagnostics grouped by the config file URI they originated from. /// Only user-defined config files are included (built-in defaults are excluded). /// - DiagnosticsPerFile GetDiagnosticsByConfigFile(); + DiagnosticsPerFile EnumerateDiagnosticsPerFile(); /// /// The number of configuration files in the chain. diff --git a/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs b/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs index b02db66b119..1eb186d3605 100644 --- a/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/Configuration/BicepConfigChangeHandlerTests.cs @@ -194,6 +194,42 @@ public void RefreshCompilationOfSourceFilesInWorkspace_WithEmptySourceFile_Shoul diagnostics.Should().BeNullOrEmpty(); } + [TestMethod] + public void HandleBicepConfigOpenEvent_WithCyclicExtends_PublishesBCP454ToConfigFileUri() + { + var mockFileSystem = new MockFileSystem(); + mockFileSystem.AddFile("/a/bicepconfig.json", """{ "extends": "./b/bicepconfig.json" }"""); + mockFileSystem.AddFile("/a/b/bicepconfig.json", """{ "extends": "../bicepconfig.json" }"""); + + PublishDiagnosticsParams? receivedParams = null; + var document = BicepCompilationManagerHelper.CreateMockDocument(p => receivedParams = p); + ILanguageServerFacade server = BicepCompilationManagerHelper.CreateMockServer(document).Object; + + var fileExplorer = new FileSystemFileExplorer(mockFileSystem); + var bicepConfigManager = new BicepConfigurationManager(fileExplorer); + var configurationManager = new ConfigurationManager(fileExplorer, bicepConfigManager); + var workspace = new ActiveSourceFileSet(); + var bicepCompilationManager = new BicepCompilationManager( + server, + BicepCompilationManagerHelper.CreateEmptyCompilationProvider(configurationManager), + workspace, + BicepCompilationManagerHelper.CreateMockScheduler().Object, + new SourceFileFactory(configurationManager, BicepTestConstants.FeatureProviderFactory, BicepTestConstants.AuxiliaryFileCache, BicepTestConstants.FileExplorer), + BicepTestConstants.AuxiliaryFileCache); + + var lifecycleManager = new BicepConfigLifecycleManager( + bicepCompilationManager, + configurationManager, + bicepConfigManager, + server); + + var configUri = DocumentUri.From(InMemoryFileResolver.GetFileUri("/a/bicepconfig.json")); + lifecycleManager.HandleBicepConfigOpenEvent(configUri); + + receivedParams.Should().NotBeNull(); + receivedParams!.Diagnostics.Should().ContainSingle(d => d.Code!.Value.String == "BCP454"); + } + [TestMethod] public void RefreshCompilationOfSourceFilesInWorkspace_WithoutBicepConfigFile_ShouldUseDefaultConfigAndRefreshCompilation() { diff --git a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs index 1a3f4d6e3cb..e28f35e9882 100644 --- a/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs +++ b/src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs @@ -77,24 +77,27 @@ public void HandleBicepConfigCloseEvent(DocumentUri documentUri) private void PublishConfigDiagnostics(DocumentUri documentUri) { var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); - var diagnosticsByFile = chain.GetDiagnosticsByConfigFile().ToList(); - if (diagnosticsByFile.All(kvp => kvp.Value.IsEmpty)) + // Always publish to documentUri first — covers chain-level errors (cycle, too deep) + // stored on the built-in fallback layer, and clears stale squiggles on the active file. + var effectiveDiagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); + server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams { - // No diagnostics in any layer — clear stale squiggles on the active file. - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams + Uri = documentUri, + Diagnostics = new Container(effectiveDiagnostics.Select(d => new LspDiagnostic { - Uri = documentUri, - Diagnostics = new Container() - }); - return; - } + Severity = ToLspSeverity(d.Level), + Code = new DiagnosticCode(d.Code), + Message = d.Message, + Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), + Source = "bicep" + })) + }); - // Publish squiggles to each config file that has diagnostics. - foreach (var (fileUri, diagnostics) in diagnosticsByFile) + // Publish diagnostics for every user-layer config file in the chain — including empty + // arrays so the client clears stale squiggles for files that no longer have errors. + foreach (var (fileUri, diagnostics) in chain.EnumerateDiagnosticsPerFile()) { - if (diagnostics.IsEmpty) { continue; } - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams { Uri = DocumentUri.From(fileUri.ToUri()), @@ -119,124 +122,3 @@ private void PublishConfigDiagnostics(DocumentUri documentUri) }; } } - -namespace Bicep.LanguageServer.BicepConfig -{ - public class BicepConfigLifecycleManager : IBicepConfigLifecycleManager - { - private readonly ICompilationManager compilationManager; - private readonly ConfigurationManager configurationManager; -<<<<<<< HEAD - - public BicepConfigLifecycleManager(ICompilationManager compilationManager, - ConfigurationManager configurationManager) - { - this.compilationManager = compilationManager; - this.configurationManager = configurationManager; -======= - private readonly IBicepConfigurationManager bicepConfigurationManager; - private readonly ILanguageServerFacade server; - private readonly ILinterRulesProvider linterRulesProvider; - private readonly ITelemetryProvider telemetryProvider; - private readonly IActiveSourceFileSet workspace; - - public BicepConfigLifecycleManager(ICompilationManager compilationManager, - ConfigurationManager configurationManager, - IBicepConfigurationManager bicepConfigurationManager, - ILanguageServerFacade server, - ILinterRulesProvider linterRulesProvider, - ITelemetryProvider telemetryProvider, - IActiveSourceFileSet workspace) - { - this.compilationManager = compilationManager; - this.configurationManager = configurationManager; - this.bicepConfigurationManager = bicepConfigurationManager; - this.server = server; - this.linterRulesProvider = linterRulesProvider; - this.telemetryProvider = telemetryProvider; - this.workspace = workspace; ->>>>>>> af7513a9a (add 'extends' schema support and surface config chain diagnostics in VS Code) - } - - public void RefreshCompilationOfSourceFilesInWorkspace() - { - configurationManager.PurgeCache(); - // We shouldn't need to reload auxiliary files if a configuration file has changed. - compilationManager.RefreshAllActiveCompilations(forceReloadAuxiliaryFiles: false); - } - - public void HandleBicepConfigOpenEvent(DocumentUri documentUri) - { - HandleBicepConfigOpenOrChangeEvent(documentUri); - PublishConfigDiagnostics(documentUri); - } - - public void HandleBicepConfigChangeEvent(DocumentUri documentUri) - { - // A change event can represent file creation, modification, or deletion. - // Creation and deletion change config file discovery (the lookup cache), so we - // must do a full purge rather than a targeted invalidation. - configurationManager.PurgeCache(); - HandleBicepConfigOpenOrChangeEvent(documentUri); - PublishConfigDiagnostics(documentUri); - } - - private void HandleBicepConfigOpenOrChangeEvent(DocumentUri documentUri) - => configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()); - - public void HandleBicepConfigSaveEvent(DocumentUri documentUri) -<<<<<<< HEAD - => configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()); -======= - { - if (configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri()) is { } update) - { - TelemetryHelper.SendTelemetryOnBicepConfigChange(update.prevConfiguration, update.newConfiguration, linterRulesProvider, telemetryProvider); - } - - PublishConfigDiagnostics(documentUri); - } ->>>>>>> af7513a9a (add 'extends' schema support and surface config chain diagnostics in VS Code) - - public void HandleBicepConfigCloseEvent(DocumentUri documentUri) - { - configurationManager.RemoveConfigCacheEntry(documentUri.ToIOUri()); - - // Clear squiggles when the file is closed. - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams - { - Uri = documentUri, - Diagnostics = new Container() - }); - } - - private void PublishConfigDiagnostics(DocumentUri documentUri) - { - var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri()); - var diagnostics = chain.GetEffectiveConfiguration().GetDiagnostics(); - - var lspDiagnostics = diagnostics.Select(d => new LspDiagnostic - { - Severity = ToLspSeverity(d.Level), - Code = new DiagnosticCode(d.Code), - Message = d.Message, - Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(0, 0, 0, 0), - Source = "bicep" - }); - - server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams - { - Uri = documentUri, - Diagnostics = new Container(lspDiagnostics) - }); - } - - private static DiagnosticSeverity ToLspSeverity(DiagnosticLevel level) => level switch - { - DiagnosticLevel.Error => DiagnosticSeverity.Error, - DiagnosticLevel.Warning => DiagnosticSeverity.Warning, - DiagnosticLevel.Info => DiagnosticSeverity.Information, - _ => DiagnosticSeverity.Hint - }; - } -} diff --git a/src/Bicep.LangServer/IServiceCollectionExtensions.cs b/src/Bicep.LangServer/IServiceCollectionExtensions.cs index bf88b7eb17f..6dfa73b2521 100644 --- a/src/Bicep.LangServer/IServiceCollectionExtensions.cs +++ b/src/Bicep.LangServer/IServiceCollectionExtensions.cs @@ -54,6 +54,7 @@ BicepLangServerOptions bicepLangServerOptions .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton()