Skip to content
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

Update the severities value of BuildCheck results #10330

Merged
4 changes: 1 addition & 3 deletions documentation/specs/proposed/BuildCheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,13 @@ Majority of following cases are included in appropriate context within the scena
```ini
# I expect this to apply to all projects within my solution, but not to projects which are not part of the solution
[ContosoFrontEnd.sln]
build_check.BC0101.IsEnabled=true
build_check.BC0101.Severity=warning
f-alizada marked this conversation as resolved.
Show resolved Hide resolved
```
* Attributing `.editorconfig` configurations to lower granularity than whole projects. E.g.:
```ini
# I expect this to apply only to a scope of the imported file. Or possibly I expect this to apply to all projects importing this project.
[ContosoCommonImport.proj]
buildcheck.BC0101.IsEnabled=true
buildcheck.BC0101.Severity=warning
build_check.BC0101.Severity=warning
```
* Respecting `.editorconfig` file in msbuild import locations (unless they are in the parent folders hierarchy of particular project file).
* CodeFixes are not supported in V1
Expand Down
44 changes: 14 additions & 30 deletions src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ public class BuildAnalyzerConfiguration
public static BuildAnalyzerConfiguration Default { get; } = new()
{
EvaluationAnalysisScope = BuildCheck.EvaluationAnalysisScope.ProjectOnly,
Severity = BuildAnalyzerResultSeverity.Info,
IsEnabled = false,
Severity = BuildAnalyzerResultSeverity.None
};

public static BuildAnalyzerConfiguration Null { get; } = new();
Expand All @@ -45,7 +44,18 @@ public class BuildAnalyzerConfiguration
/// If all rules within the analyzer are not enabled, it will not be run.
/// If some rules are enabled and some are not, the analyzer will be run and reports will be post-filtered.
/// </summary>
public bool? IsEnabled { get; internal init; }
public bool? IsEnabled {
get
{
// Do not consider Default as enabled, because the default severity of the rule coule be set to None
if (Severity.HasValue && Severity.Value != BuildAnalyzerResultSeverity.Default)
{
return !Severity.Value.Equals(BuildAnalyzerResultSeverity.None);
}

return null;
}
}

/// <summary>
/// Creates a <see cref="BuildAnalyzerConfiguration"/> object based on the provided configuration dictionary.
Expand All @@ -59,8 +69,7 @@ internal static BuildAnalyzerConfiguration Create(Dictionary<string, string>? co
return new()
{
EvaluationAnalysisScope = TryExtractValue(nameof(EvaluationAnalysisScope), configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null,
Severity = TryExtractValue(nameof(Severity), configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null,
IsEnabled = TryExtractValue(nameof(IsEnabled), configDictionary, out bool isEnabled) ? isEnabled : null,
Severity = TryExtractValue(nameof(Severity), configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null
};
}

Expand All @@ -83,31 +92,6 @@ private static bool TryExtractValue<T>(string key, Dictionary<string, string>? c
return isParsed;
}

private static bool TryExtractValue(string key, Dictionary<string, string>? config, out bool value)
{
value = default;

if (config == null || !config.TryGetValue(key.ToLower(), out var stringValue) || stringValue is null)
{
return false;
}

bool isParsed = false;

if (bool.TryParse(stringValue, out bool boolValue))
{
value = boolValue;
isParsed = true;
}

if (!isParsed)
{
ThrowIncorrectValueException(key, stringValue);
}

return isParsed;
}

private static void ThrowIncorrectValueException(string key, string value)
{
// TODO: It will be nice to have the filename where the incorrect configuration was placed.
Expand Down
25 changes: 23 additions & 2 deletions src/Build/BuildCheck/API/BuildAnalyzerResultSeverity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,28 @@ namespace Microsoft.Build.Experimental.BuildCheck;
/// </summary>
public enum BuildAnalyzerResultSeverity
{
Info,
/// <summary>
/// When set, the default value of the BuildCheck rule will be used.
/// </summary>
Default,

/// <summary>
/// When set to None the rule will not run.
/// </summary>
None,

/// <summary>
/// Information level message.
/// </summary>
Suggestion,

/// <summary>
/// Results a warning in build if the BuildCheck rule applied.
/// </summary>
Warning,
Error,

/// <summary>
/// Results an error in build if the BuildCheck rule applied.
/// </summary>
Error
}
f-alizada marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion src/Build/BuildCheck/API/BuildCheckResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public BuildCheckResult(BuildAnalyzerRule buildAnalyzerRule, ElementLocation loc
internal BuildEventArgs ToEventArgs(BuildAnalyzerResultSeverity severity)
=> severity switch
{
BuildAnalyzerResultSeverity.Info => new BuildCheckResultMessage(this),
BuildAnalyzerResultSeverity.Suggestion => new BuildCheckResultMessage(this),
BuildAnalyzerResultSeverity.Warning => new BuildCheckResultWarning(this),
BuildAnalyzerResultSeverity.Error => new BuildCheckResultError(this),
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null),
Expand Down
2 changes: 1 addition & 1 deletion src/Build/BuildCheck/Analyzers/DoubleWritesAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal sealed class DoubleWritesAnalyzer : BuildAnalyzer
public static BuildAnalyzerRule SupportedRule = new BuildAnalyzerRule("BC0102", "DoubleWrites",
"Two tasks should not write the same file",
"Tasks {0} and {1} from projects {2} and {3} write the same file: {4}.",
new BuildAnalyzerConfiguration() { Severity = BuildAnalyzerResultSeverity.Warning, IsEnabled = true });
new BuildAnalyzerConfiguration() { Severity = BuildAnalyzerResultSeverity.Warning });

public override string FriendlyName => "MSBuild.DoubleWritesAnalyzer";

Expand Down
2 changes: 1 addition & 1 deletion src/Build/BuildCheck/Analyzers/SharedOutputPathAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal sealed class SharedOutputPathAnalyzer : BuildAnalyzer
public static BuildAnalyzerRule SupportedRule = new BuildAnalyzerRule("BC0101", "ConflictingOutputPath",
"Two projects should not share their OutputPath nor IntermediateOutputPath locations",
"Projects {0} and {1} have conflicting output paths: {2}.",
new BuildAnalyzerConfiguration() { Severity = BuildAnalyzerResultSeverity.Warning, IsEnabled = true });
new BuildAnalyzerConfiguration() { Severity = BuildAnalyzerResultSeverity.Warning });

public override string FriendlyName => "MSBuild.SharedOutputPathAnalyzer";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Build.Experimental.BuildCheck;

namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
Expand All @@ -10,25 +11,31 @@ namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
/// </summary>
internal sealed class BuildAnalyzerConfigurationInternal
{
public BuildAnalyzerConfigurationInternal(string ruleId, EvaluationAnalysisScope evaluationAnalysisScope, BuildAnalyzerResultSeverity severity, bool isEnabled)
public BuildAnalyzerConfigurationInternal(string ruleId, EvaluationAnalysisScope evaluationAnalysisScope, BuildAnalyzerResultSeverity severity)
{
if (severity == BuildAnalyzerResultSeverity.Default)
{
throw new ArgumentOutOfRangeException(nameof(severity), severity, "Severity 'Default' is not recognized by the BuilcCheck reports infrastructure");
f-alizada marked this conversation as resolved.
Show resolved Hide resolved
}

RuleId = ruleId;
EvaluationAnalysisScope = evaluationAnalysisScope;
Severity = severity;
IsEnabled = isEnabled;
}

public string RuleId { get; }

public EvaluationAnalysisScope EvaluationAnalysisScope { get; }

public BuildAnalyzerResultSeverity Severity { get; }
public bool IsEnabled { get; }

public bool IsEnabled => Severity >= BuildAnalyzerResultSeverity.Suggestion;

// Intentionally not checking the RuleId
// as for analyzers with multiple rules, we can squash config to a single one,
// if the ruleId is the only thing differing.
public bool IsSameConfigurationAs(BuildAnalyzerConfigurationInternal? other) =>
other != null &&
Severity == other.Severity &&
IsEnabled == other.IsEnabled &&
EvaluationAnalysisScope == other.EvaluationAnalysisScope;
}
20 changes: 17 additions & 3 deletions src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ internal sealed class ConfigurationProvider

private readonly string[] _infrastructureConfigurationKeys = new string[] {
nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(),
nameof(BuildAnalyzerConfiguration.IsEnabled).ToLower(),
nameof(BuildAnalyzerConfiguration.Severity).ToLower()
};

Expand Down Expand Up @@ -263,8 +262,7 @@ internal BuildAnalyzerConfigurationInternal MergeConfiguration(
=> new BuildAnalyzerConfigurationInternal(
ruleId: ruleId,
evaluationAnalysisScope: GetConfigValue(editorConfig, defaultConfig, cfg => cfg.EvaluationAnalysisScope),
isEnabled: GetConfigValue(editorConfig, defaultConfig, cfg => cfg.IsEnabled),
severity: GetConfigValue(editorConfig, defaultConfig, cfg => cfg.Severity));
severity: GetSeverityValue(editorConfig, defaultConfig));
f-alizada marked this conversation as resolved.
Show resolved Hide resolved

private BuildAnalyzerConfigurationInternal GetMergedConfiguration(
string projectFullPath,
Expand All @@ -280,6 +278,22 @@ private T GetConfigValue<T>(
propertyGetter(defaultValue) ??
EnsureNonNull(propertyGetter(BuildAnalyzerConfiguration.Default));

private BuildAnalyzerResultSeverity GetSeverityValue(BuildAnalyzerConfiguration editorConfigValue, BuildAnalyzerConfiguration defaultValue)
{
BuildAnalyzerResultSeverity? resultSeverity = null;

// Consider Default as null, so the severity from the default value could be selected.
// Default severity is not recognized by the infrastructure and serves for configuration purpuses only.
if (editorConfigValue.Severity != null && editorConfigValue.Severity != BuildAnalyzerResultSeverity.Default)
{
resultSeverity = editorConfigValue.Severity;
}

resultSeverity ??= defaultValue.Severity ?? EnsureNonNull(BuildAnalyzerConfiguration.Default.Severity);

return resultSeverity.Value;
}

private static T EnsureNonNull<T>(T? value) where T : struct
{
if (value is null)
Expand Down
88 changes: 88 additions & 0 deletions src/Build/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Info</Target>
<Left>lib/net472/Microsoft.Build.dll</Left>
<Right>lib/net472/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Info</Target>
<Left>lib/net8.0/Microsoft.Build.dll</Left>
<Right>lib/net8.0/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Info</Target>
<Left>ref/net472/Microsoft.Build.dll</Left>
<Right>ref/net472/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Info</Target>
<Left>ref/net8.0/Microsoft.Build.dll</Left>
<Right>ref/net8.0/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Error</Target>
<Left>lib/net472/Microsoft.Build.dll</Left>
<Right>lib/net472/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Warning</Target>
<Left>lib/net472/Microsoft.Build.dll</Left>
<Right>lib/net472/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Error</Target>
<Left>lib/net8.0/Microsoft.Build.dll</Left>
<Right>lib/net8.0/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Warning</Target>
<Left>lib/net8.0/Microsoft.Build.dll</Left>
<Right>lib/net8.0/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Error</Target>
<Left>ref/net472/Microsoft.Build.dll</Left>
<Right>ref/net472/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Warning</Target>
<Left>ref/net472/Microsoft.Build.dll</Left>
<Right>ref/net472/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Error</Target>
<Left>ref/net8.0/Microsoft.Build.dll</Left>
<Right>ref/net8.0/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0011</DiagnosticId>
<Target>F:Microsoft.Build.Experimental.BuildCheck.BuildAnalyzerResultSeverity.Warning</Target>
<Left>ref/net8.0/Microsoft.Build.dll</Left>
<Right>ref/net8.0/Microsoft.Build.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,59 @@
using Microsoft.Build.Experimental.BuildCheck.Infrastructure;
using Microsoft.Build.Experimental.BuildCheck;
using Shouldly;
using System;

namespace Microsoft.Build.BuildCheck.UnitTests;

public class BuildAnalyzerConfigurationInternalTests
{
[Theory]
[InlineData("ruleId", EvaluationAnalysisScope.ProjectOnly, BuildAnalyzerResultSeverity.Warning, true, true)]
[InlineData("ruleId2", EvaluationAnalysisScope.ProjectOnly, BuildAnalyzerResultSeverity.Warning, true, true)]
[InlineData("ruleId", EvaluationAnalysisScope.ProjectOnly, BuildAnalyzerResultSeverity.Error, true, false)]
[InlineData("ruleId", EvaluationAnalysisScope.ProjectOnly, BuildAnalyzerResultSeverity.Warning, true)]
[InlineData("ruleId2", EvaluationAnalysisScope.ProjectOnly, BuildAnalyzerResultSeverity.Warning, true)]
[InlineData("ruleId", EvaluationAnalysisScope.ProjectOnly, BuildAnalyzerResultSeverity.Error, false)]
public void IsSameConfigurationAsTest(
string secondRuleId,
EvaluationAnalysisScope secondScope,
BuildAnalyzerResultSeverity secondSeverity,
bool secondEnabled,
bool isExpectedToBeSame)
{
BuildAnalyzerConfigurationInternal configuration1 = new BuildAnalyzerConfigurationInternal(
ruleId: "ruleId",
evaluationAnalysisScope: EvaluationAnalysisScope.ProjectOnly,
severity: BuildAnalyzerResultSeverity.Warning,
isEnabled: true);
severity: BuildAnalyzerResultSeverity.Warning);

BuildAnalyzerConfigurationInternal configuration2 = new BuildAnalyzerConfigurationInternal(
ruleId: secondRuleId,
evaluationAnalysisScope: secondScope,
severity: secondSeverity,
isEnabled: secondEnabled);
severity: secondSeverity);

configuration1.IsSameConfigurationAs(configuration2).ShouldBe(isExpectedToBeSame);
}

[Theory]
[InlineData( BuildAnalyzerResultSeverity.Warning, true)]
[InlineData(BuildAnalyzerResultSeverity.Suggestion, true)]
[InlineData(BuildAnalyzerResultSeverity.Error, true)]
[InlineData(BuildAnalyzerResultSeverity.None, false)]
public void BuildAnalyzerConfigurationInternal_Constructor_SeverityConfig(BuildAnalyzerResultSeverity severity, bool isEnabledExpected)
{
BuildAnalyzerConfigurationInternal configuration = new BuildAnalyzerConfigurationInternal(
ruleId: "ruleId",
evaluationAnalysisScope: EvaluationAnalysisScope.ProjectOnly,
severity: severity);

configuration.IsEnabled.ShouldBe(isEnabledExpected);
}

[Fact]
public void BuildAnalyzerConfigurationInternal_Constructor_SeverityConfig_Fails()
{
Should.Throw<ArgumentOutOfRangeException>(() =>
{
new BuildAnalyzerConfigurationInternal(
ruleId: "ruleId",
evaluationAnalysisScope: EvaluationAnalysisScope.ProjectOnly,
severity: BuildAnalyzerResultSeverity.Default);
});
}
}
Loading
Loading