|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.CodeAnalysis.Shared.Extensions;
internal static class DiagnosticDescriptorExtensions
{
private const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic";
private const string DotnetDiagnosticPrefix = "dotnet_diagnostic";
private const string CategoryPrefix = "category";
private const string SeveritySuffix = "severity";
private const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix;
public static ImmutableArray<string> ImmutableCustomTags(this DiagnosticDescriptor descriptor)
{
Debug.Assert(descriptor.CustomTags is ImmutableArray<string>);
return (ImmutableArray<string>)descriptor.CustomTags;
}
/// <summary>
/// Gets project-level effective severity of the given <paramref name="descriptor"/> accounting for severity configurations from both the following sources:
/// 1. Compilation options from ruleset file, if any, and command line options such as /nowarn, /warnaserror, etc.
/// 2. Analyzer config documents at the project root directory or in ancestor directories.
/// </summary>
public static ReportDiagnostic GetEffectiveSeverity(
this DiagnosticDescriptor descriptor,
CompilationOptions compilationOptions,
AnalyzerConfigOptions? analyzerOptions,
ImmutableDictionary<string, ReportDiagnostic>? treeOptions)
{
var effectiveSeverity = descriptor.GetEffectiveSeverity(compilationOptions);
// Apply analyzer config options, unless configured with a non-default value in compilation options.
// Note that compilation options (/nowarn, /warnaserror) override analyzer config options.
if (treeOptions != null && analyzerOptions != null &&
(!compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var reportDiagnostic) ||
reportDiagnostic == ReportDiagnostic.Default))
{
if (treeOptions.TryGetValue(descriptor.Id, out reportDiagnostic) && reportDiagnostic != ReportDiagnostic.Default ||
TryGetSeverityFromBulkConfiguration(descriptor, analyzerOptions, out reportDiagnostic))
{
Debug.Assert(reportDiagnostic != ReportDiagnostic.Default);
effectiveSeverity = reportDiagnostic;
}
}
return effectiveSeverity;
}
/// <summary>
/// Gets document-level effective severity of the given <paramref name="descriptor"/> accounting for severity configurations from both the following sources:
/// 1. Compilation options from ruleset file, if any, and command line options such as /nowarn, /warnaserror, etc.
/// 2. Analyzer config documents at the document root directory or in ancestor directories.
/// </summary>
public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor descriptor, CompilationOptions compilationOptions, SyntaxTree tree, AnalyzerOptions analyzerOptions)
{
var effectiveSeverity = descriptor.GetEffectiveSeverity(compilationOptions);
// Apply analyzer config options, unless configured with a non-default value in compilation options.
// Note that compilation options (/nowarn, /warnaserror) override analyzer config options.
if (!compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var reportDiagnostic) ||
reportDiagnostic == ReportDiagnostic.Default)
{
// First check for tree-level analyzer config options.
var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree);
var providerAndTree = compilationOptions.SyntaxTreeOptionsProvider != null
? (compilationOptions.SyntaxTreeOptionsProvider, tree)
: default;
var severityInEditorConfig = descriptor.GetEffectiveSeverity(analyzerConfigOptions, providerAndTree);
if (severityInEditorConfig != ReportDiagnostic.Default)
{
effectiveSeverity = severityInEditorConfig;
}
else
{
// If not found, check for global analyzer config options.
var severityInGlobalConfig = descriptor.GetEffectiveSeverity(analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions, providerAndTree);
if (severityInGlobalConfig != ReportDiagnostic.Default)
{
effectiveSeverity = severityInGlobalConfig;
}
}
}
return effectiveSeverity;
}
public static bool IsDefinedInEditorConfig(this DiagnosticDescriptor descriptor, AnalyzerConfigOptions analyzerConfigOptions)
{
// Check if the option is defined explicitly in the editorconfig
var diagnosticKey = $"{DotnetDiagnosticPrefix}.{descriptor.Id}.{SeveritySuffix}";
if (analyzerConfigOptions.TryGetValue(diagnosticKey, out var value) &&
EditorConfigSeverityStrings.TryParse(value, out var severity))
{
return true;
}
// Check if the option is defined as part of a bulk configuration
// Analyzer bulk configuration does not apply to:
// 1. Disabled by default diagnostics
// 2. Compiler diagnostics
// 3. Non-configurable diagnostics
if (!descriptor.IsEnabledByDefault ||
descriptor.ImmutableCustomTags().Any(static tag => tag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable))
{
return false;
}
// If user has explicitly configured default severity for the diagnostic category, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}";
if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return true;
}
// Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.severity = error'
if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return true;
}
// option not defined in editorconfig, assumed to be the default
return false;
}
/// <summary>
/// Gets the effective diagnostic severity for the diagnostic ID corresponding to the
/// given <paramref name="descriptor"/> by looking up the severity settings in the options.
/// If the provided options are specific to a particular tree, provide a non-null value
/// for <paramref name="providerAndTree"/> to look up tree specific severity options.
/// </summary>
public static ReportDiagnostic GetEffectiveSeverity(
this DiagnosticDescriptor descriptor,
AnalyzerConfigOptions analyzerConfigOptions,
(SyntaxTreeOptionsProvider provider, SyntaxTree tree)? providerAndTree = null)
{
ReportDiagnostic severity;
string? value;
// Check if the option is defined explicitly in the editorconfig
if (providerAndTree.HasValue)
{
var provider = providerAndTree.Value.provider;
var tree = providerAndTree.Value.tree;
if (provider.TryGetDiagnosticValue(tree, descriptor.Id, CancellationToken.None, out severity) ||
provider.TryGetGlobalDiagnosticValue(descriptor.Id, CancellationToken.None, out severity))
{
return severity;
}
}
else
{
var diagnosticKey = $"{DotnetDiagnosticPrefix}.{descriptor.Id}.{SeveritySuffix}";
if (analyzerConfigOptions.TryGetValue(diagnosticKey, out value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return severity;
}
}
// Check if the option is defined as part of a bulk configuration
// Analyzer bulk configuration does not apply to:
// 1. Disabled by default diagnostics
// 2. Compiler diagnostics
// 3. Non-configurable diagnostics
if (!descriptor.IsEnabledByDefault ||
descriptor.ImmutableCustomTags().Any(static tag => tag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable))
{
return ReportDiagnostic.Default;
}
// If user has explicitly configured default severity for the diagnostic category, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}";
if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return severity;
}
// Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.severity = error'
if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return severity;
}
// option not defined in editorconfig, assumed to be the default
return ReportDiagnostic.Default;
}
/// <summary>
/// Tries to get configured severity for the given <paramref name="descriptor"/>
/// from bulk configuration analyzer config options, i.e.
/// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%'
/// or
/// 'dotnet_analyzer_diagnostic.severity = %severity%'
/// Docs: https://docs.microsoft.com/visualstudio/code-quality/use-roslyn-analyzers?view=vs-2019#set-rule-severity-of-multiple-analyzer-rules-at-once-in-an-editorconfig-file for details
/// </summary>
private static bool TryGetSeverityFromBulkConfiguration(
DiagnosticDescriptor descriptor,
AnalyzerConfigOptions analyzerOptions,
out ReportDiagnostic severity)
{
// Analyzer bulk configuration does not apply to:
// 1. Disabled by default diagnostics
// 2. Compiler diagnostics
// 3. Non-configurable diagnostics
if (!descriptor.IsEnabledByDefault ||
descriptor.ImmutableCustomTags().Any(static tag => tag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable))
{
severity = default;
return false;
}
// If user has explicitly configured default severity for the diagnostic category, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}";
if (analyzerOptions.TryGetValue(categoryBasedKey, out var value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return true;
}
// Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.severity = error'
if (analyzerOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
EditorConfigSeverityStrings.TryParse(value, out severity))
{
return true;
}
severity = default;
return false;
}
public static bool IsCompilationEnd(this DiagnosticDescriptor descriptor)
=> descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.CompilationEnd);
// TODO: the value stored in descriptor should already be valid URI (https://github.com/dotnet/roslyn/issues/59205)
internal static Uri? GetValidHelpLinkUri(this DiagnosticDescriptor descriptor)
=> Uri.TryCreate(descriptor.HelpLinkUri, UriKind.Absolute, out var uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) ? uri : null;
public static DiagnosticDescriptor WithMessageFormat(this DiagnosticDescriptor descriptor, LocalizableString messageFormat)
{
#pragma warning disable RS0030 // Do not used banned APIs - DiagnosticDescriptor .ctor is banned in this project, but fine to use here.
return new DiagnosticDescriptor(descriptor.Id, descriptor.Title, messageFormat,
descriptor.Category, descriptor.DefaultSeverity, descriptor.IsEnabledByDefault,
descriptor.Description, descriptor.HelpLinkUri, descriptor.CustomTags.ToArray());
#pragma warning restore RS0030 // Do not used banned APIs
}
}
|