Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Bicep.Core/Configuration/BicepConfigurationChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Immutable;
using Bicep.Core.Diagnostics;
using Bicep.IO.Abstraction;

namespace Bicep.Core.Configuration;

Expand Down Expand Up @@ -69,6 +70,17 @@ public IEnumerable<IDiagnostic> GetAllDiagnostics()
return this.aggregatedDiagnostics.Value;
}

/// <summary>
/// Returns diagnostics grouped by the config file URI they originated from.
/// Built-in default layers (no <see cref="IBicepConfiguration.ConfigFileUri"/>) are excluded.
/// </summary>
public DiagnosticsPerFile EnumerateDiagnosticsPerFile()
=> this.layers
.Where(layer => !layer.IsBuiltIn)
.Select(layer => new KeyValuePair<IOUri, ImmutableArray<IDiagnostic>>(
layer.ConfigFileUri!,
layer.GetDiagnostics().ToImmutableArray()));

/// <summary>
/// The number of configuration files in the chain, including the built-in defaults.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Bicep.Core/Configuration/IBicepConfigurationChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public interface IBicepConfigurationChain
/// </summary>
IBicepConfiguration GetEffectiveConfiguration();

/// <summary>
/// Returns diagnostics grouped by the config file URI they originated from.
/// Only user-defined config files are included (built-in defaults are excluded).
/// </summary>
DiagnosticsPerFile EnumerateDiagnosticsPerFile();

/// <summary>
/// The number of configuration files in the chain.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Bicep.Core/Diagnostics/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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<Bicep.Core.Diagnostics.IDiagnostic>>>;
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -246,7 +282,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,
Expand All @@ -259,7 +296,9 @@ private void RefreshCompilationOfSourceFilesInWorkspace(string bicepFileContents

var bicepConfigLifecycleManager = new BicepConfigLifecycleManager(
bicepCompilationManager,
configurationManager);
configurationManager,
bicepConfigManager,
server);

bicepConfigLifecycleManager.RefreshCompilationOfSourceFilesInWorkspace();

Expand Down
78 changes: 75 additions & 3 deletions src/Bicep.LangServer/BicepConfig/BicepConfigLifecycleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,33 @@
// 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
{
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)
ConfigurationManager configurationManager,
IBicepConfigurationManager bicepConfigurationManager,
ILanguageServerFacade server)
{
this.compilationManager = compilationManager;
this.configurationManager = configurationManager;
this.bicepConfigurationManager = bicepConfigurationManager;
this.server = server;
}

public void RefreshCompilationOfSourceFilesInWorkspace()
Expand All @@ -29,6 +41,7 @@ public void RefreshCompilationOfSourceFilesInWorkspace()
public void HandleBicepConfigOpenEvent(DocumentUri documentUri)
{
HandleBicepConfigOpenOrChangeEvent(documentUri);
PublishConfigDiagnostics(documentUri);
}

public void HandleBicepConfigChangeEvent(DocumentUri documentUri)
Expand All @@ -44,9 +57,68 @@ private void HandleBicepConfigOpenOrChangeEvent(DocumentUri documentUri)
=> configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri());

public void HandleBicepConfigSaveEvent(DocumentUri documentUri)
=> configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri());
{
configurationManager.RefreshConfigCacheEntry(documentUri.ToIOUri());
PublishConfigDiagnostics(documentUri);
}

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<LspDiagnostic>()
});
}

private void PublishConfigDiagnostics(DocumentUri documentUri)
{
var chain = bicepConfigurationManager.GetConfigurationChain(documentUri.ToIOUri());

// 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
{
Uri = documentUri,
Diagnostics = new Container<LspDiagnostic>(effectiveDiagnostics.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"
}))
});

// 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())
{
server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams
{
Uri = DocumentUri.From(fileUri.ToUri()),
Diagnostics = new Container<LspDiagnostic>(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
{
DiagnosticLevel.Error => DiagnosticSeverity.Error,
DiagnosticLevel.Warning => DiagnosticSeverity.Warning,
DiagnosticLevel.Info => DiagnosticSeverity.Information,
_ => DiagnosticSeverity.Hint
};
}
}
1 change: 1 addition & 0 deletions src/Bicep.LangServer/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ BicepLangServerOptions bicepLangServerOptions
.AddSingleton<IModuleRestoreScheduler, ModuleRestoreScheduler>()
.AddSingleton<IAzResourceProvider, AzResourceProvider>()
.AddSingleton<IBicepConfigLifecycleManager, BicepConfigLifecycleManager>()
.AddSingleton<IBicepConfigurationManager, BicepConfigurationManager>()
.AddSingleton<IDeploymentCollectionProvider, DeploymentCollectionProvider>()
.AddSingleton<IDeploymentOperationsCache, DeploymentOperationsCache>()
.AddSingleton<IDeploymentFileCompilationCache, DeploymentFileCompilationCache>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading