Skip to content
Draft
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
75 changes: 75 additions & 0 deletions src/Bicep.Cli.IntegrationTests/LintCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,81 @@ param notUsedParam int
error.Should().Contain($"{inputFile}(3,1) : Error BCP259: The parameter \"notUsedParm\" is assigned in the params file without being declared in the Bicep file.");
}

[TestMethod]
public async Task Lint_with_config_option_should_override_discovered_configuration()
{
var outputPath = FileHelper.GetUniqueTestOutputPath(TestContext);

var inputFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
param unusedParam string
""", outputPath);

// This config will be discovered automatically and should produce a warning.
FileHelper.SaveResultFile(TestContext, "bicepconfig.json", """
{
"analyzers": {
"core": {
"rules": {
"no-unused-params": {
"level": "warning"
}
}
}
}
}
""", outputPath);

// This config is supplied explicitly and disables the rule.
var overrideConfig = FileHelper.SaveResultFile(TestContext, "override.json", """
{
"analyzers": {
"core": {
"rules": {
"no-unused-params": {
"level": "off"
}
}
}
}
}
""", outputPath);

var (output, error, result) = await Bicep(
"lint",
inputFile,
"--config",
overrideConfig);

result.Should().Be(0);
output.Should().BeEmpty();

// Rule should NOT fire because override.json was used.
error.Should().BeEmpty();
}

[TestMethod]
public async Task Lint_with_missing_config_should_report_configuration_error()
{
var outputPath = FileHelper.GetUniqueTestOutputPath(TestContext);

var inputFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
param foo string
""", outputPath);

var missingConfig = Path.Combine(outputPath, "doesnotexist.json");

var (output, error, result) = await Bicep(
"lint",
inputFile,
"--config",
missingConfig);

result.Should().Be(1);
output.Should().BeEmpty();

error.Should().Contain("doesnotexist.json");
}

[TestMethod]
public async Task Lint_with_sarif_diagnostics_format_should_output_valid_sarif()
{
Expand Down
3 changes: 2 additions & 1 deletion src/Bicep.Cli/Arguments/LintArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public record LintArguments(
string? InputFile,
string? FilePattern,
DiagnosticsFormat? DiagnosticsFormat,
bool NoRestore) : IFilePatternInputArguments;
bool NoRestore,
string? Config) : IFilePatternInputArguments;
40 changes: 35 additions & 5 deletions src/Bicep.Cli/Commands/LintCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Bicep.Cli.Helpers;
using Bicep.Cli.Logging;
using Bicep.Core;
using Bicep.Core.Configuration;
using Bicep.Core.Features;
using Bicep.Core.Utils;
using Bicep.IO.Abstraction;
Expand All @@ -20,7 +21,8 @@ public class LintCommand(
ILogger logger,
DiagnosticLogger diagnosticLogger,
BicepCompiler compiler,
InputOutputArgumentsResolver inputOutputArgumentsResolver) : ICommand
InputOutputArgumentsResolver inputOutputArgumentsResolver,
IConfigurationManager configurationManager) : ICommand
{
public async Task<int> RunAsync(LintArguments args)
{
Expand All @@ -30,16 +32,20 @@ public async Task<int> RunAsync(LintArguments args)
{
ArgumentHelper.ValidateBicepOrBicepParamFile(inputUri);

var result = await Lint(inputUri, args.NoRestore, args.DiagnosticsFormat);
var result = await Lint(inputUri, args.NoRestore, args.DiagnosticsFormat, args.Config);
hasErrors |= result.HasErrors;
}

return CommandHelper.GetExitCode(new(hasErrors));
}

private async Task<DiagnosticSummary> Lint(IOUri inputUri, bool noRestore, DiagnosticsFormat? diagnosticsFormat)
private async Task<DiagnosticSummary> Lint(IOUri inputUri, bool noRestore, DiagnosticsFormat? diagnosticsFormat, string? config)
{
var compilation = await compiler.CreateCompilation(inputUri, skipRestore: noRestore);

var lintCompiler = GetCompiler(config);

var compilation = await lintCompiler.CreateCompilation(inputUri, skipRestore: noRestore);

CommandHelper.LogExperimentalWarning(logger, compilation);

var summary = diagnosticLogger.LogDiagnostics(ArgumentHelper.GetDiagnosticOptions(diagnosticsFormat) with { SarifToStdout = true }, compilation);
Expand Down Expand Up @@ -68,11 +74,16 @@ internal static System.CommandLine.Command CreateCommand(CommandLineBuilderConte
{
Description = "Sets the diagnostics format. Valid values are (Default, SARIF).",
};
var configOption = new System.CommandLine.Option<string?>(Option.Config)
{
Description = "Specifies the path to a bicepconfig.json file to use.",
};

command.Add(inputFileArgument);
command.Add(filePatternOption);
command.Add(noRestoreOption);
command.Add(diagnosticsFormatOption);
command.Add(configOption);
command.Validators.Add((System.CommandLine.Parsing.CommandResult result) => CommandLineBuilderContext.ValidatePositionalArgument(result, inputFileArgument));

command.SetAction((result, ct) => context.RunCommandAsync(async () =>
Expand All @@ -82,11 +93,30 @@ internal static System.CommandLine.Command CreateCommand(CommandLineBuilderConte
result.GetValue(inputFileArgument),
result.GetValue(filePatternOption),
diagnosticsFormat,
result.GetValue(noRestoreOption));
result.GetValue(noRestoreOption),
result.GetValue(configOption));

return await context.GetCommand<LintCommand>().RunAsync(args);
}));

return command;
}

private BicepCompiler GetCompiler(string? config)
{
if (config is null)
{
return compiler;
}

var configUri = IOUri.FromFilePath(config);

var configuration = configurationManager.LoadConfiguration(configUri);

return BicepCompiler.Create(services =>
{
services.AddSingleton<IConfigurationManager>(
IConfigurationManager.WithStaticConfiguration(configuration));
});
}
}
3 changes: 3 additions & 0 deletions src/Bicep.Cli/Constants/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public static class Option
public const string IndentSize = "--indent-size";
public const string InsertFinalNewline = "--insert-final-newline";

// Lint
public const string Config = "--config";

// Publish
public const string Target = "--target";
public const string DocumentationUri = "--documentation-uri";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -958,5 +958,45 @@ public void GetConfiguration_ModuleAliasesMock_FallsBackToModuleAliasesWhenAlias
configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue();
alias!.Registry.Should().Be("real.azurecr.io");
}

[TestMethod]
public void LoadConfiguration_LoadsExplicitConfigFile()
{
// Arrange.
var fileExplorer = new InMemoryFileExplorer();
var configFileUri = TestFileUri.FromInMemoryPath("path/to/bicepconfig.json");

var configFile = fileExplorer.GetFile(configFileUri);

using (var stream = configFile.OpenWrite())
using (var writer = new StreamWriter(stream))
{
writer.Write("{}");
}

var sut = new ConfigurationManager(fileExplorer);

// Act.
var configuration = sut.LoadConfiguration(configFileUri);

// Assert.
configuration.Should().NotBeNull();
}

[TestMethod]
public void LoadConfiguration_MissingFileReturnsDiagnostic()
{
// Arrange.
var fileExplorer = new InMemoryFileExplorer();
var sut = new ConfigurationManager(fileExplorer);

var configFileUri = TestFileUri.FromInMemoryPath("path/to/missing/bicepconfig.json");

// Act.
var configuration = sut.LoadConfiguration(configFileUri);

// Assert.
configuration.Diagnostics.Should().NotBeEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public PatchingConfigurationManager(ConfigurationManager configurationManager, F
}

public RootConfiguration GetConfiguration(IOUri sourceFileUri) => patchFunc(configurationManager.GetConfiguration(sourceFileUri));

public RootConfiguration LoadConfiguration(IOUri configFileUri) => patchFunc(configurationManager.LoadConfiguration(configFileUri));
}
40 changes: 30 additions & 10 deletions src/Bicep.Core/Configuration/ConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
// Licensed under the MIT License.

using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO.Abstractions;
using System.Security;
using System.ServiceModel;
using System.Text.Json;
using Bicep.Core.Diagnostics;
using Bicep.Core.Extensions;
Expand Down Expand Up @@ -46,12 +42,35 @@ public RootConfiguration GetConfiguration(IOUri sourceFileUri)
return GetDefaultConfiguration();
}

if (!loadedConfigCache.GetOrAdd(configFileHandle, LoadConfiguration).IsSuccess(out var configuration, out var loadDiagnostic))
if (!loadedConfigCache.GetOrAdd(configFileHandle, LoadConfigurationInternal).IsSuccess(out var configuration, out var diagnostic))
{
return GetDefaultConfiguration().With(diagnostics: [loadDiagnostic]);
return GetDefaultConfiguration()
.With(diagnostics: [diagnostic]);
}

return configuration;
return configuration!;
}

public RootConfiguration LoadConfiguration(IOUri configFileUri)
{
var configFileHandle = this.fileExplorer.GetFile(configFileUri);

if (!configFileHandle.Exists())
{
return GetDefaultConfiguration()
.With(diagnostics:
[
ConfigDiagnosticBuilder.ConfigurationFileNotFound(configFileHandle.Uri)
]);
}

if (!loadedConfigCache.GetOrAdd(configFileHandle, LoadConfigurationInternal).IsSuccess(out var configuration, out var diagnostic))
{
return GetDefaultConfiguration()
.With(diagnostics: [diagnostic]);
}

return configuration!;
}

public void PurgeCache()
Expand All @@ -66,13 +85,14 @@ public void PurgeCache()
{
(RootConfiguration, RootConfiguration)? returnVal = null;
var configFileHandle = this.fileExplorer.GetFile(configFileIdentifier);
loadedConfigCache.AddOrUpdate(configFileHandle, LoadConfiguration, (handle, prev) =>
loadedConfigCache.AddOrUpdate(configFileHandle, LoadConfigurationInternal, (handle, prev) =>
{
var reloaded = LoadConfiguration(handle);
var reloaded = LoadConfigurationInternal(handle);
if (prev.IsSuccess(out var prevConfig) && reloaded.IsSuccess(out var newConfig))
{
returnVal = (prevConfig, newConfig);
}

return reloaded;
});

Expand All @@ -91,7 +111,7 @@ public void RemoveConfigCacheEntry(IOUri configFileUri)

private static RootConfiguration GetDefaultConfiguration() => IConfigurationManager.GetBuiltInConfiguration();

private static ResultWithDiagnostic<RootConfiguration> LoadConfiguration(IFileHandle configFileHandle)
private static ResultWithDiagnostic<RootConfiguration> LoadConfigurationInternal(IFileHandle configFileHandle)
{
try
{
Expand Down
10 changes: 10 additions & 0 deletions src/Bicep.Core/Configuration/IConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public interface IConfigurationManager
/// <returns>The configuration for the source file.</returns>
RootConfiguration GetConfiguration(IOUri sourceFileUri);

/// <summary>
/// Gets the configuration from the specified configuration file.
/// If the configuration file cannot be loaded, the built-in configuration is returned with diagnostics.
/// </summary>
/// <param name="configFileUri">The URI of the configuration file to load.</param>
/// <returns>The configuration loaded from the specified file.</returns>
RootConfiguration LoadConfiguration(IOUri configFileUri);

/// <summary>
/// Gets the built-in configuration.
/// </summary>
Expand Down Expand Up @@ -56,6 +64,8 @@ internal ConstantConfigurationManager(RootConfiguration configuration)
}

public RootConfiguration GetConfiguration(IOUri sourceFileUri) => configuration;

public RootConfiguration LoadConfiguration(IOUri configFileUri) => configuration;
}
}
}