-
Notifications
You must be signed in to change notification settings - Fork 753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Watch for creation/deletion/modification of local bicepconfig.json file #4038
Conversation
{ | ||
// We want to push error dignostics in case of invalid bicepconfig.json. | ||
// We won't be able to apply validation until user fixes issues in config file | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return true
if it's unparseable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will enable to re trigger compilation and push error diagnostics. RetriggerCompilationOfSourceFilesInWorkspace_WithInvalidBicepConfigFile_ShouldRetriggerCompilation unit test covers this scenario. Let me know if I am missing something here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code doesn't exist anymore. Made updates here:
7c53107
@@ -39,7 +37,7 @@ private IConfigurationRoot BuildConfig(string? localFolder) | |||
// load the default settings from file embedded as resource | |||
var assembly = Assembly.GetExecutingAssembly(); | |||
var names = assembly.GetManifestResourceNames(); | |||
var defaultConfigResourceName = names.FirstOrDefault(n => n.EndsWith(SettingsFileName)); | |||
var defaultConfigResourceName = names.FirstOrDefault(n => n.EndsWith(LanguageConstants.BicepConfigSettingsFileName)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is existing code, but I think it would be better to be precise here and hard-code the manifest resource path, rather than searching for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated here:
6992577
try | ||
{ | ||
JObject jObject = JObject.Parse(bicepConfig); | ||
return jObject.IsValid(bicepConfigSchema); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to do server-side schema validation? Performance on schema validation can be quite slow.
If so, shouldn't this be built into the Bicep core libraries to provide a consistent experience for CLI & LSP users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this to avoid re triggering compilation on every key stroke. I couldn't think of a better way to handle the scenario. I don't quite understand your question on moving to bicep core libraries. Do you mean the validation logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So at present, ConfigHelper
handles the loading and reading of config deep inside the semantic model:
this.linterAnalyzerLazy = new Lazy<LinterAnalyzer>(() => new LinterAnalyzer()); |
->
private readonly ConfigHelper defaultConfigHelper = new ConfigHelper(); |
->
this.Config = BuildConfig(Directory.GetCurrentDirectory()); |
->
bicep/src/Bicep.Core/Configuration/ConfigHelper.cs
Lines 51 to 57 in 3e6e581
if (DiscoverLocalConfigurationFile(localFolder) is string localConfig) | |
{ | |
// we must set reloadOnChange to false here - if it set to true, then ConfigurationBuilder will initialize | |
// a FileSystem.Watcher instance - which has severe performance impact on non-Windows OSes (https://github.com/dotnet/runtime/issues/42036) | |
configBuilder.AddJsonFile(localConfig, optional: true, reloadOnChange: false); | |
this.CustomSettingsFileName = localConfig; | |
} |
I'm wondering if the creator of the SemanticModel
instance should be responsible for creating the config and passing it in as a constructor argument, to avoid unneccessary file reads. The ConfigHelper
itself could also be responsible for validating the config file, rather than relying on a JSON schema validator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated as part of this commit:
7c53107
@@ -11,11 +11,18 @@ | |||
</ItemGroup> | |||
|
|||
<ItemGroup> | |||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This library has some strange commercial licensing setup attached to it which I've never fully understood. Are there any issues using it here? See https://www.newtonsoft.com/jsonschema ("Licensing & Pricing") for more info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At one point it’d be nice to have errors on invalid values, but viscose can provide that out of the box, I think we don’t need to build validation in bicep itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this. Looks like it was pulled out of Json.Net(which is under MIT license) and is not free anymore.
I am trying couple other libraries. Let me know if you have any suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I can use this one instead: https://www.nuget.org/packages/NJsonSchema/. It's under MIT license.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, removed the validation as part of this commit:
7c53107
// Retrigger compilation of source files in workspace when local bicepconfig.json file is edited | ||
if (bicepConfigFileChangeEvents.Any()) | ||
{ | ||
bicepConfigChangeHandler.RetriggerCompilationOfSourceFilesInWorkspace(compilationManager, bicepConfigFileChangeEvents.First(), workspace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently every time we rebuild a semantic model, we reload configuration from disk. Should we cache the deserialized configuration and pass it down to the semantic model, to avoid this?
This is technically a different issue, but it feels like it might be simpler to address under this PR as it may change the design significantly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated here:
7c53107
Let me know if you have any questions/concerns. Thanks!
IEnumerable<FileEvent> bicepConfigFileChangeEvents = fileEvents.Where(x => x.Uri.Path.EndsWith(LanguageConstants.BicepConfigSettingsFileName)); | ||
|
||
// Retrigger compilation of source files in workspace when local bicepconfig.json file is edited | ||
if (bicepConfigFileChangeEvents.Any()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's OK to assume that any config file change => recompile everything, but is it worth trying to be more precise (e.g. figuring out which files will actually be impacted by a given config file)?
Might not be worth optimizing for a relatively rare scenario, just wanted to raise it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made some changes as part of below commit, which should cover this scenario. Recompilation will only be triggered when LanguageId is "bicep":
7c53107
Let me know if I am missing something.
@@ -41,9 +41,9 @@ public CompilationContext Update(IReadOnlyWorkspace workspace, CompilationContex | |||
return this.CreateContext(syntaxTreeGrouping, modelLookup); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to transfer the previous configHelper
here?
FWIW, I generally feel like default parameters are worth avoiding in C#, as they can be very easy to miss when refactoring, and in this case - it's very important to get it right. IMO it would be worth making configHelper
a non-default param on the CreateContext
method and Compilation
constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Updated here:
74db298
ConfigHelper? configHelper = null; | ||
|
||
if (!reloadBicepConfig && | ||
activeContexts.TryGetValue(documentUri, out CompilationContext? compilationContext) && | ||
compilationContext is not null && | ||
compilationContext.Compilation.ConfigHelper is ConfigHelper previousConfigHelper) | ||
{ | ||
configHelper = previousConfigHelper; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be simplified to:
var configHelper = activeContexts.TryGetValue(documentUri)?.Compilation.ConfigHelper;
if (reloadBicepConfig)
{
configHelper = null;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: In fact, looks like you already have access to the previous context with the prevContext
variable, so you shouldn't need to fetch it back with TryGetValue()
.
Generally - we need to be super careful accessing this dictionary, as we're inside the AddOrUpdate
method of a concurrent dictionary - need to be 100% sure what we're doing is thread-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplified as part of this commit:
74db298
if (fileResolver.TryRead(sourceFileUri, out string? bicepFileContents, out ErrorBuilderDelegate _) && | ||
!string.IsNullOrWhiteSpace(bicepFileContents)) | ||
{ | ||
compilationManager.UpsertCompilation(DocumentUri.From(sourceFileUri), null, bicepFileContents, reloadBicepConfig: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had an offline conversation with @majastrz. I'll be making changes to use RefreshCompilation(..) instead of UpsertCompilation(..) as the latter doesn't handle unsaved changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated here:
58045fa
if (configHelper is null) | ||
{ | ||
ConfigHelper = new ConfigHelper(); | ||
} | ||
else | ||
{ | ||
ConfigHelper = configHelper; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Can simplify to just:
ConfigHelper = configHelper ?? new ConfigHelper();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated as part of this commit:
b90b61e
IEnumerable<IDiagnostic> diagnostics; | ||
|
||
try | ||
{ | ||
diagnostics = compilationContext.Compilation.GetEntrypointSemanticModel().GetAllDiagnostics(); | ||
} | ||
catch | ||
{ | ||
return Task.FromResult(new CommandOrCodeActionContainer()); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this code block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm I don't recollect why I added this. Reverted the change here:
765c098
if (bicepConfigFileChangeEvents.Any()) | ||
{ | ||
Uri uri = bicepConfigFileChangeEvents.First().Uri.ToUri(); | ||
BicepConfigChangeHandler.RefreshCompilationOfSourceFilesInWorkspace(compilationManager, uri, workspace, string.Empty); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use null
here - otherwise you can't differentiate between "empty file" and "deleted file"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, what's the meaning of string.Empty
? It seems from the implementation of RefreshCompilationOfSourceFilesInWorkspace()
that this signifies a file removal - but are you checking whether it's a delete here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, updated to use null here and added comments:
d3cf19a
{ | ||
public class BicepConfigChangeHandler | ||
{ | ||
public static void RefreshCompilationOfSourceFilesInWorkspace(ICompilationManager compilationManager, Uri bicepConfigUri, IWorkspace workspace, string bicepConfigFileContents) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the bicepConfigFileContents
param nullable (string?
), and change the below check from string.IsNullOrWhiteSpace(bicepConfigFileContents)
-> bicepConfigFileContents is null
- it's worth being precise to be able to differentiate between "deleted file" and "file with whitespace"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, updated to use null here and added comments:
d3cf19a
@@ -26,6 +28,13 @@ public IEnumerable<ISourceFile> GetSourceFilesForDirectory(Uri fileUri) | |||
public ImmutableDictionary<Uri, ISourceFile> GetActiveSourceFilesByUri() | |||
=> activeFiles.ToImmutableDictionary(); | |||
|
|||
public void UpsertActiveBicepConfig(BicepConfig? bicepConfig) | |||
{ | |||
activeConfig = bicepConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible for there to be multiple bicepconfig.json
files in a workspace? How do you decide which one to replace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With multiple config files, we should always pick one in current directory or closer to current directory correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing in folder information as part of this commit:
bd34647
Added few integration tests to test multiple config files scenario. Let me know if you have any questions.
@@ -33,6 +34,8 @@ public class BicepCompilationManager : ICompilationManager | |||
// represents compilations of open bicep files | |||
private readonly ConcurrentDictionary<DocumentUri, CompilationContext> activeContexts = new ConcurrentDictionary<DocumentUri, CompilationContext>(); | |||
|
|||
private ConfigHelper? configHelper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is going to need some change - some things to be aware of:
- This class can handle concurrent requests so has to be race-condition safe; a mutable field without locking feels like it's going to be problematic.
- There can be multiple
bicepconfig.json
files active in the workspace, so storing the value in a single field feels like it can yield incorrect results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated here:
5f750d2
54920d8
to
740b7f8
Compare
@@ -77,23 +74,13 @@ public LinterAnalyzer() | |||
|
|||
internal IEnumerable<IDiagnostic> Analyze(SemanticModel semanticModel, ConfigHelper? overrideConfig = default) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we have activeConfigHelper
set via the constructor, do we need to keep this overload which accepts overrideConfig
? If so, do you know which code paths require it? It's always felt like an anti-pattern, as it negates the ability to cache and reuse semantic models for diagnostics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was used in tests. I removed passing overrideConfig as part of this commit:
1e2de9b
{ | ||
this.activeConfigHelper = this.defaultConfigHelper; | ||
this.activeConfigHelper = configHelper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] rename activeConfigHelper
to just configHelper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated as part of this commit:
1e2de9b
@@ -2,13 +2,19 @@ | |||
// Licensed under the MIT License. | |||
|
|||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
|
|||
namespace Bicep.Core.Configuration | |||
{ | |||
public class BicepConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad you found a way to reuse this strange class 😄
37e6507
to
ff64dd3
Compare
if (activeBicepConfigs.ContainsKey(uri)) | ||
{ | ||
activeBicepConfigs.TryRemove(uri, out _); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need the ContainsKey
check - TryRemove
does this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed here:
bbb280e
if (reloadBicepConfig && | ||
workspace.TryGetSourceFile(documentUri.ToUri(), out ISourceFile? sourceFile) && | ||
sourceFile is BicepFile) | ||
{ | ||
UpsertCompilationInternal(documentUri, null, sourceFile, reloadBicepConfig); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like @majastrz to take a look at the changes to this class - as the mechanism he added for the module registry work may be useful here for triggering changes on a source file with no syntax differences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was the mechanism. Although, I'm planning to change the refresh part to take a module reference and have logic to compute which document URIs need to updated and then recompile them in optimal order.
} | ||
else | ||
{ | ||
string bicepConfigInCurrentDirectory = Path.Combine(Directory.GetCurrentDirectory(), LanguageConstants.BicepConfigSettingsFileName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right - won't Directory.GetCurrentDirectory()
give you the current working directory? I'd expect there to be some code you can reuse for fetching the correct bicepconfig.json
to use for a given Bicep file - would be better to commonize it and use it here rather than reimplementing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: I think this implementation is in ConfigHelper.DiscoverLocalConfigurationFile()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to use ConfigHelper.DiscoverLocalConfigurationFile(..) here:
bbb280e
5376c82
to
5142ece
Compare
using System.Reflection; | ||
|
||
namespace Bicep.Core.Configuration | ||
{ | ||
public class ConfigHelper | ||
{ | ||
private const string SettingsFileName = "bicepconfig.json"; | ||
private const string bicepConfigResourceName = "Bicep.Core.Configuration.bicepconfig.json"; | ||
private readonly IFileResolver fileResolver; | ||
|
||
/// <summary> | ||
/// Property exposes the configuration root | ||
/// that is currently loaded | ||
/// </summary> | ||
public IConfigurationRoot Config { get; private set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One question (non blocking, not related to this PR) - is there any reason why we are using the raw IConfigurationRoot
interface? Would it be possible to use ConfigurationBinder.Bind
to bind the IConfigurationRoot
to an instance of a strongly typed config class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The linter rules currently read dynamically from the config:
bicep/src/Bicep.Core/Analyzers/Linter/Rules/NoHardcodedEnvironmentUrlsRule.cs
Lines 40 to 44 in de8b847
this.disallowedHosts = GetArray(nameof(DisallowedHosts), Array.Empty<string>()) | |
.ToImmutableArray(); | |
this.MinimumHostLength = this.disallowedHosts.Value.Min(h => h.Length); | |
this.excludedHosts = GetArray(nameof(ExcludedHosts), Array.Empty<string>()) | |
.ToImmutableArray(); |
It would be nice to have a strongly-typed config class (e.g. for the alias work you're doing), but might need to keep some dynamic-ness for linters if we want 3rd parties to be able to author their own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll sync up with you guys offline to see if we can improve something here that will help alias work. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Fixes:
#4009
#4027